一、概述
ATT(Attribute Protocol)协议是蓝牙低功耗(BLE)技术中用于发现、读取和写入对端设备属性的重要协议。在ATT协议中,Write Request/Response Packet是客户端向服务端发送写请求并接收响应的一种数据包格式。
1.1. Write Request Packet
Write Request Packet是由客户端(Client)发送给服务端(Server)的,用于请求修改服务端上的某个属性值。这个请求是“必达”的,即服务端在收到请求后必须回复一个响应,以确保通信的可靠性。
Write Request Packet的主要特点包括:
- Opcode:Write Request的Opcode用于标识这是一个写请求操作。
- Attribute Handle:用于指定要修改的属性的句柄(Handle),这是一个由服务端分配的唯一标识符。
- Attribute Value:包含要写入的新属性值。
1.2. Write Response Packet
Write Response Packet是由服务端发送给客户端的,用于确认Write Request Packet的接收和处理结果。
Write Response Packet的主要特点包括:
- Opcode:Write Response的Opcode用于标识这是一个写响应操作。
- Status:表示写操作的成功或失败状态。如果写操作成功,Status字段将包含成功代码;如果失败,则包含相应的错误代码。
1.3. Write Request/Response的交互流程
- 客户端发送Write Request:客户端构建Write Request Packet,包含要修改的属性的句柄和新的属性值,并通过BLE连接发送给服务端。
- 服务端处理Write Request:服务端收到Write Request Packet后,根据请求中的句柄和属性值进行相应处理。如果处理成功,服务端将准备Write Response Packet;如果失败,则准备包含错误代码的Write Response Packet。
- 服务端发送Write Response:服务端将Write Response Packet发送给客户端,以确认写操作的接收和处理结果。
- 客户端接收Write Response:客户端收到Write Response Packet后,根据Status字段判断写操作是否成功。如果成功,则可以继续后续操作;如果失败,则根据错误代码进行相应的错误处理。
1.4. 注意事项
- 在BLE通信中,每个ATT命令都是“必达”的,即发送方会等待接收方的确认(ACK)或响应(Response)。如果未收到确认或响应,发送方将重传该命令直到超时或连接断开。
- Write Request/Response Packet的交互遵循蓝牙核心规范中定义的ATT协议规则,包括PDU(Protocol Data Unit)格式、Opcode定义、错误处理等。
- 在实际应用中,我们需要确保客户端和服务端之间的通信协议一致,并正确处理各种异常情况,以确保通信的可靠性和稳定性。
二、客户端发送请求:ATT Write Request Packet (0x12)
ATT Write Request 是BLE设备间数据交换的基础之一,它允许客户端向服务器发送数据,从而实现将数据写入设备属性。
1.1 ATT Write Request Packet 格式
ATT Write Request 数据包是一个PDU(Protocol Data Unit),根据spec,ATT Write Request 的基本结构通常包括以下几个部分:
1. Opcode(操作码):
-
ATT Write Request 的操作码是 0x12。告诉接收方这是一个写请求。
2. Handle(句柄):
-
紧随操作码之后的是要写入数据的属性的句柄。句柄是一个唯一的标识符,用于指定要操作的属性。
3. Value(值):
-
句柄之后是实际要写入的数据值。这个值的长度和格式取决于要写入的数据类型以及该属性的定义。
1.2. 示例
我们有一个ATT Write Request数据包,它要将数据写入句柄为0x5D的属性,并且要写入的数据是0x00 0x01
Opcode: 0x12 (ATT Write Request)
Handle: 0x005D
Value: 0x00 0x01
1.3. 注意事项
-
权限:在写入数据之前,必须确保客户端有足够的权限来写入该属性。GATT服务器可以配置属性的安全设置,例如要求加密连接或签名。
-
大小限制:ATT Write Request 有一个最大负载大小限制,通常等于BLE的MTU减去ATT头部和句柄的大小。如果要写入的数据超过了这个限制,客户端需要使用ATT协议中的“长特征值写入”过程来分块发送数据。
三、服务端响应读请求
对于ATT Write Request,GATT服务器会根据请求的内容更新相应的属性值。之后,服务器会向客户端发送一个ATT Write Response Packet作为响应,以确认写入操作的成功或失败。
acl数据接收分析见Bluedroid协议栈L2CAP连接源码分析_bluedroid acl-CSDN博客
gatt_data_process函数解析从L2CAP层接收到的数据,并根据操作码(opcode)执行相应的处理逻辑。因为操作码Read Response(0x12)是偶数,调用gatt_server_handle_client_req函数来处理该响应。
3.1. gatt_server_handle_client_req
/packages/modules/Bluetooth/system/stack/gatt/gatt_sr.cc
/** This function is called to handle the client requests to server */
void gatt_server_handle_client_req(tGATT_TCB& tcb, uint16_t cid,
uint8_t op_code, uint16_t len,
uint8_t* p_data) {
/* there is pending command, discard this one */
if (!gatt_sr_cmd_empty(tcb, cid) && op_code != GATT_HANDLE_VALUE_CONF) return;
/* the size of the message may not be bigger than the local max PDU size*/
/* The message has to be smaller than the agreed MTU, len does not include op
* code */
uint16_t payload_size = gatt_tcb_get_payload_size(tcb, cid);
if (len >= payload_size) {
log::error("server receive invalid PDU size:{} pdu size:{}", len + 1,
payload_size);
/* for invalid request expecting response, send it now */
if (op_code != GATT_CMD_WRITE && op_code != GATT_SIGN_CMD_WRITE &&
op_code != GATT_HANDLE_VALUE_CONF) {
gatt_send_error_rsp(tcb, cid, GATT_INVALID_PDU, op_code, 0, false);
}
/* otherwise, ignore the pkt */
} else {
// handle database out of sync
if (gatts_process_db_out_of_sync(tcb, cid, op_code, len, p_data)) return;
switch (op_code) {
...
case GATT_REQ_READ: /* read char/char descriptor value */
case GATT_REQ_READ_BLOB:
case GATT_REQ_WRITE: /* write char/char descriptor value */
case GATT_CMD_WRITE:
case GATT_SIGN_CMD_WRITE:
case GATT_REQ_PREPARE_WRITE:
gatts_process_attribute_req(tcb, cid, op_code, len, p_data);
break;
...
default:
break;
}
}
}
gatt_server_handle_client_req负责解析来自客户端的请求,然后根据请求的类型进行相应的处理。
1. 检查是否有待处理的命令:
-
首先检查当前是否有针对这个客户端ID(cid)的待处理命令。如果有,并且这个新请求的操作码(op_code )不是GATT_HANDLE_VALUE_CONF(即句柄值确认),则直接返回,不处理这个新请求。因为BLE协议规定,在服务器处理完一个请求之前,不应接受新的请求(除了句柄值确认请求)。
-
ATT_HANDLE_VALUE_CFM
在BLE通信中,GATT协议定义了多种消息类型用于数据的读写和确认,其中ATT_HANDLE_VALUE_IND用于从服务器向客户端发送数据,而ATT_HANDLE_VALUE_CFM则是客户端对对ATT_HANDLE_VALUE_IND(指示)消息的接收和处理.
-
ATT_HANDLE_VALUE_IND与ATT_HANDLE_VALUE_CFM的关系
-
ATT_HANDLE_VALUE_IND:
-
这是一个从GATT服务器发送到GATT客户端的指示消息。
-
当服务器希望向客户端发送数据时,可以使用此消息类型。
-
与ATT_HANDLE_VALUE_NOTI(通知)消息类似,但指示消息需要客户端的确认。
-
-
ATT_HANDLE_VALUE_CFM:
-
这是一个从GATT客户端发送到GATT服务器的确认消息。
-
当客户端成功接收到ATT_HANDLE_VALUE_IND消息并处理完毕后,会发送此消息以确认。
-
此消息是可选的,但在需要确认数据已接收的场景中非常重要。
-
-
-
ATT_HANDLE_VALUE_CFM的发送与接收
在BLE通信过程中,当GATT服务器使用ATT_HANDLE_VALUE_IND向客户端发送数据时,客户端需要按照以下步骤处理:
-
接收ATT_HANDLE_VALUE_IND消息:
-
客户端的GATT事件处理函数会接收到这个消息。
-
消息中包含要发送的数据的句柄(Handle)和数据值(Value)。
-
-
处理数据:
-
客户端根据接收到的数据执行相应的操作。
-
可能包括更新UI、存储数据或进行进一步的数据处理。
-
-
发送ATT_HANDLE_VALUE_CFM(如果需要):
-
如果客户端需要向服务器确认数据的接收和处理,它将发送ATT_HANDLE_VALUE_CFM消息。
-
这个消息告诉服务器客户端已经成功接收到数据并进行了处理。
-
-
2. 确认的必要性:
-
并非所有ATT_HANDLE_VALUE_IND消息都需要客户端的确认。这取决于应用程序的具体需求和GATT服务器的实现。
-
如果应用程序不需要确认,或者服务器在发送数据后不需要等待确认,那么客户端可以选择不发送ATT_HANDLE_VALUE_CFM消息。
3. 性能考虑:
-
发送ATT_HANDLE_VALUE_CFM消息会增加通信开销和延迟。
-
因此,在设计BLE应用程序时,需要权衡确认的必要性和性能需求。
错误处理:
-
如果客户端在接收或处理ATT_HANDLE_VALUE_IND消息时发生错误,可能会发送ATT Error Response消息来通知服务器错误原因。
-
这有助于服务器了解问题所在并采取相应的措施。
-
检查PDU大小:
-
接下来,通过gatt_tcb_get_payload_size获取当前与客户端协商的MTU。然后,检查请求的数据长度是否超过了这个MTU。并且如果请求不是GATT_CMD_WRITE、GATT_SIGN_CMD_WRITE或GATT_HANDLE_VALUE_CONF(这些操作不需要响应),则向客户端发送一个错误响应。对于其他情况,则忽略这个数据包。
-
-
处理数据库不同步问题:在处理实际请求之前,函数会调用gatts_process_db_out_of_sync来检查并处理可能的数据库不同步问题。如果这个函数返回true,表示已经处理了这个问题并决定不继续处理这个请求,则函数直接返回。
-
处理GATT请求:本case对应GATT_REQ_WRITE,函数会调用gatts_process_attribute_req来处理与属性相关的请求。
3.2. gatts_process_attribute_req
/packages/modules/Bluetooth/system/stack/gatt/gatt_sr.cc
void gatts_process_attribute_req(tGATT_TCB& tcb, uint16_t cid, uint8_t op_code,
uint16_t len, uint8_t* p_data) {
uint16_t handle = 0;
uint8_t* p = p_data;
tGATT_STATUS status = GATT_INVALID_HANDLE;
if (len < 2) {
log::error("Illegal PDU length, discard request");
status = GATT_INVALID_PDU;
} else {
STREAM_TO_UINT16(handle, p);
len -= 2;
}
#if (GATT_CONFORMANCE_TESTING == TRUE)
gatt_cb.handle = handle;
if (gatt_cb.enable_err_rsp && gatt_cb.req_op_code == op_code) {
log::verbose("Conformance tst: forced err rsp: error status={}",
gatt_cb.err_status);
gatt_send_error_rsp(tcb, cid, gatt_cb.err_status, cid, gatt_cb.req_op_code,
handle, false);
return;
}
#endif
if (GATT_HANDLE_IS_VALID(handle)) {
for (auto& el : *gatt_cb.srv_list_info) {
if (el.s_hdl <= handle && el.e_hdl >= handle) {
for (const auto& attr : el.p_db->attr_list) {
if (attr.handle == handle) {
switch (op_code) {
case GATT_REQ_READ: /* read char/char descriptor value */
case GATT_REQ_READ_BLOB:
gatts_process_read_req(tcb, cid, el, op_code, handle, len, p);
break;
case GATT_REQ_WRITE: /* write char/char descriptor value */
case GATT_CMD