【物联网】关于 GATT (Generic Attribute Profile)基本概念与三种操作(Read / Write / Notify)的理解
“BLE 读写”在这里具体指什么?
在你的系统里,树莓派是 BLE Central,Arduino 是 BLE Peripheral。
Central 和 Peripheral 通过 **GATT 特征(Characteristic)**交互:
-
读(Read):Central 主动读取某个特征当前的值。
在 bleak 里是await client.read_gatt_char(UUID)
。你当前并不依赖“主动读”,而是用 Notify 被动接收(更省电、实时)。
-
写(Write):Central 把数据写入一个“可写”的特征,驱动 Peripheral 执行操作。
在 bleak 里是await client.write_gatt_char(UUID, b"...")
(注意必须是 bytes)。
你现在的实现:
-
BLE 写(Central → Peripheral):
RPi 调用write_gatt_char(COMMAND_UUID, payload.encode())
,把命令写入 Arduino 的 commandCharacteristic(BLEWrite),Arduino 在commandCharacteristic.written()
分支里解析并控制 LED。 -
BLE 读(有两种做法):
- 主动读:
read_gatt_char(SENSOR_UUID)
……(你没用) - ✅ 订阅通知 Notify:
start_notify(SENSOR_UUID, on_notify)
,Arduino 每次sensorCharacteristic.writeValue(json)
,Central 就自动收到回调。
这就是你代码里的“读传感器数据”的实际完成方式。
- 主动读:
一句话:你用 write 做“下发命令”,用 notify 完成“读数据”。
“MQTT 桥(bridge)”是什么意思?
你的 Python 脚本同时连两张“网”:
- 一边是 BLE(和 Arduino 说话)
- 一边是 MQTT(和云端 Broker 说话)
脚本把两边的消息“转接”起来,所以叫 桥接(bridge):
- 从 BLE 收到的 传感器 JSON → publish 到 MQTT 主题(
iot/sensors/data
) - 从 MQTT 收到的 命令 → write_gatt_char 写到 BLE 的命令特征(
COMMAND_UUID
)
整个脚本不做复杂业务,只负责协议转换与转发,这就是“MQTT 桥”。
“上行 / 下行”到底指什么?
在物联网里通常以“设备(Node)→ 云”为上行(uplink),反方向为下行(downlink)。
对照你的 Topic 命名就一目了然:
上行 (Node → Edge → Cloud):
Arduino(传感器) --BLE Notify--> RPi --MQTT publish--> Broker
Topic: iot/sensors/data下行 (Cloud → Edge → Node):
Broker --MQTT publish--> RPi --BLE write--> Arduino(执行器)
Topic: iot/commands/arduino
所以你截图里注释是:
TOPIC_SENSOR = "iot/sensors/data"
→ 上行:RPi 把从 BLE 收到的数据发到 MQTTTOPIC_CMD = "iot/commands/arduino"
→ 下行:RPi 订阅这个主题,拿到命令再写回 BLE
和你的代码逐项对应
-
BLE Notify(读数据)
await client.start_notify(SENSOR_UUID, on_ble_notify) # 订阅上行特征 def on_ble_notify(sender, data):s = data.decode("utf-8")mqtt_client.publish("iot/sensors/data", s) # → 上行到云
Arduino 端对应:
BLEStringCharacteristic sensorCharacteristic(..., BLERead|BLENotify, 100); sensorCharacteristic.writeValue(json); // 改值 → Central 收到 Notify
-
BLE Write(下发命令)
mqtt_client.subscribe("iot/commands/arduino") # 订阅下行 await client.write_gatt_char(CMD_UUID, payload.encode("utf-8"))
Arduino 端对应:
BLEStringCharacteristic commandCharacteristic(..., BLEWrite, 100); if (commandCharacteristic.written()) { processCommand(commandCharacteristic.value()); }
什么时候会用“BLE Read”而不是“Notify”?
- Read:偶尔读一次静态信息(固件版本、电池电量快照等)。
- Notify:连续流式数据(传感器、心率、IMU),Central 只订阅一次即可被动接收,省电且实时。
你的 DHT11 场景显然属于 Notify 更合适。
- “BLE 读写就是对 GATT 特征的操作:Central 可以 read 当前值、write 新值;外设还有 notify 主动推送。我们用 notify 做上行数据,用 write 做下行命令。”
- “MQTT 桥是把 BLE 和 MQTT 两个世界打通的中间件:BLE→MQTT 发布传感器、MQTT→BLE 下发命令。”
- “上行就是从设备到云,下行就是从云到设备;在我们项目里上行用
iot/sensors/data
,下行用iot/commands/arduino
。”
下面把 GATT 基本概念 和 Read / Write / Notify 三种操作讲清楚,并且逐一映射到你现在的代码与项目里的作用。看完你就能用“协议层 → 代码 → 演示”的角度完整回答老师的问题。
0) 角色与协议栈(1 句话)
- Arduino Nano 33 IoT = Peripheral(外设):提供数据与控制接口。
- Raspberry Pi = Central(中心):连接外设,读数据、下发命令。
- ATT/GATT:BLE 连接后,以“属性表(ATT)”承载“服务/特征(GATT)”。
1) GATT 是什么?怎么放东西?
GATT(Generic Attribute Profile)是“把数据和操作放进一张属性表”的方式。表里每一行叫 Attribute(属性),最常见的 4 种行:
- Primary Service(主服务声明)
- Characteristic Declaration(特征声明)
- Characteristic Value(特征的值)← 真正的数据在这里
- Descriptor(描述符):最常用的是 CCCD(0x2902),专门用来开/关该特征的 Notify/Indicate
你在 Arduino 里写的
BLEService sensorService(SERVICE_UUID); BLEStringCharacteristic sensorCharacteristic(SENSOR_CHAR_UUID, BLERead|BLENotify, 100); BLEStringCharacteristic commandCharacteristic(COMMAND_CHAR_UUID, BLEWrite, 100); BLE.addService(sensorService);
ArduinoBLE 会自动把“服务 + 两个特征(及它们的声明/值/CCCD)”注册到这张属性表里,Central 一连上就能按 UUID 查到它们。
2) 三种操作:Read / Write / Notify(概念 → 报文 → 你的代码)
A. Read(Central 主动读)
- 概念:Central 发送“Read Request”,外设回“Read Response(带当前值)”。
- 什么时候用:偶尔拿一次“快照”数据(版本号、电量、一次性读取等)。
- 你项目里:不依赖主动 Read,而是用 Notify(更实时、省电)。但你给了
BLERead
权限,便于用 App 或脚本手动读试试。
你可以怎么演示(可选)
RPi 端:
value = await client.read_gatt_char(SENSOR_UUID) # 主动读一次
print(value.decode())
Arduino 端:无需额外代码,Characteristic 有 BLERead
权限即可。
B. Write(Central 写入)
- 概念:Central 把数据写到某个“可写”特征的 Characteristic Value 里。
(有两种写法:有应答/无应答。bleak
默认“有应答”,更稳。) - 典型用途:下发命令、设置参数。
- 在 ATT 层:Central 发“Write Request”,外设收后可更新本地状态、驱动硬件。
你的代码如何实现
-
外设(Arduino)定义“可写特征”
BLEStringCharacteristic commandCharacteristic(COMMAND_CHAR_UUID, BLEWrite, 100);
BLEWrite
表示这个特征接收写操作。 -
外设接收写入并执行(控制 LED)
if (commandCharacteristic.written()) { // 有人刚写过来String cmd = commandCharacteristic.value(); // 取到写入的数据(字符串/JSON)processCommand(cmd); // 解析 {"led":"on"} / LED_ON ... }
-
中央(RPi/bleak)写入命令
await client.write_gatt_char(CMD_UUID, payload.encode("utf-8")) # 注意:第二个参数必须是 bytes;JSON 字符串要 .encode()
-
项目中的作用:完成 rubric 里的
“Receive commands from RPi” + “Operate actuators (LED)”。
命令路径是:MQTT(云)→ RPi(订阅)→ BLE Write → Arduino(处理)→ LED 变更。
C. Notify(Peripheral 主动推送)
- 概念:外设并不会“无限制地广播值”,而是当 Central 订阅后,在值变化时主动推送“通知”。
订阅是通过写 CCCD(0x2902)为 0x0001 来“打开通知”。Central 的 API(如start_notify
)内部就是在写这个 CCCD。 - 优点:事件驱动、省电、实时。Central 不用不停轮询 Read。
- 在 ATT 层:外设每次更新这个特征的 Value,就发送一个 Handle Value Notification 给所有订阅者。
你的代码如何实现
-
外设(Arduino)允许通知并更新特征值
BLEStringCharacteristic sensorCharacteristic(SENSOR_CHAR_UUID, BLERead | BLENotify, 100); // 每 5 秒更新一次值;若有人订阅,就会收到 Notify sensorCharacteristic.writeValue(jsonString);
-
中央(RPi/bleak)开启订阅并处理回调
await client.start_notify(SENSOR_UUID, on_ble_notify) def on_ble_notify(sender, data):s = data.decode('utf-8') # data 是字节串mqtt_client.publish(TOPIC_SENSOR, s)
-
项目中的作用:完成 rubric 里的
“Send the data to RPi via BLE”(Arduino 把 DHT11 JSON 主动推过来),
同时与 “Publish the data to a MQTT topic” 无缝衔接(回调里直接publish
)。
小知识:如果 Central 没订阅,
writeValue()
只会更新本地的特征值,不会发网络通知;Read 仍能读到新值。只有订阅后,才会看到“被动推送”。
3) 把三者串在一起:一次完整数据/命令的“包级视角”
上行(传感器 → 云)
- Arduino 每 5 秒
sensorCharacteristic.writeValue(json)
- RPi 之前已
start_notify
,它会自动收到 Notification(ATT 的 Handle Value Notification) - RPi 在回调里
mqtt.publish("iot/sensors/data", json)
- Broker(AWS) 收到消息,前端或测试终端
mosquitto_sub
能看到
下行(云 → 执行器)
- AWS 发布到
iot/commands/arduino
:{"led":"on"}
或LED_ON
- RPi MQTT 回调触发,调用
write_gatt_char(CMD_UUID, bytes)
- Arduino 的
commandCharacteristic.written()
被触发,processCommand()
解析并
digitalWrite(LED_BUILTIN, HIGH)
→ LED 点亮
4) 代码里和 GATT 的“硬知识”细节
- UUID 的意义:Central 根据 UUID 定位到具体 Service / Characteristic。你用的自定义 UUID(不冲突即可)。
- 属性权限(Properties):
BLERead | BLENotify
与BLEWrite
是对 Characteristic Declaration 里 Properties 字段的具体配置。 - CCCD(0x2902):当你在 RPi 调用
start_notify()
时,底层其实是往该特征的 CCCD 写 0x0001。取消订阅则写回 0x0000。 - MTU/长度:BLE 经典 ATT MTU 23 字节(有效负载 20),ArduinoBLE 会做分片/重组;你把字符串特征长度设为 100,建议 JSON 保持精简(温度/湿度/时间戳足够),避免手机 App/不同 Central 的兼容问题。
- Write with / without response:
bleak
默认“有应答”(更稳),ArduinoBLE 也支持;如果后期要“更快但不在乎丢包”,可改为“无应答”版本,但课堂演示不需要。
- GATT 是什么?
“连接后设备以属性表的方式暴露数据和操作。我们用 1 个 Service,放了 2 个 Characteristic:一个温湿度(Read | Notify),一个命令(Write)。Central 按 UUID 找到它们。” - 为什么用 Notify 而不是不停 Read?
“Notify 是事件驱动、低延迟、低功耗;Central 订阅一次,外设有新值就主动推。我们的 DHT11 就属于流式数据场景。” - Write 的安全/可靠性?
“默认 Write with Response,Central 会收到写成功的确认。命令是 JSON/文本,外设解析后控制 LED。” - CCCD 是做什么的?
“是特征的“通知开关”。start_notify()
实际上是写它为 0x0001;取消订阅是 0x0000。”