4. Mesh Proxy Service详解:UUID、特征值与节点身份识别

好,咱们进入第四章。这一章我打算把 Mesh Proxy Service 彻底讲透。说白了,这个服务就是蓝牙 Mesh 和标准 BLE 之间的那座桥。没有它,你的手机 App 根本没法跟 Mesh 网络里的节点说话。

我记得刚接触 Mesh 时,第一反应就是:这不就是个 GATT 服务吗?有什么难的?结果真上手调的时候,发现坑还真不少。尤其是 Proxy Data In/Out 这两个特征值,读写时机搞错了,节点直接掉线。嗯,咱们今天就把这些细节掰开揉碎了讲。

4.1 Mesh Proxy Service 的 UUID

先看最基础的东西——UUID。Mesh Proxy Service 的官方 UUID 是:

Service UUID: 0x1828

这个 16-bit UUID 在 Bluetooth SIG 里是专门分配给 Mesh Proxy Service 的。你想想看,整个 BLE 协议栈里服务那么多,0x1828 就是 Mesh 的专属门牌号。

我个人的习惯是,在代码里不要硬编码这个值,而是用宏定义或者常量。为什么呢?因为项目后期如果要升级协议栈版本,UUID 定义的位置统一了,改起来方便。我曾经在一个项目里看到同事把 UUID 散落在七八个文件里,那叫一个酸爽。

关键点:Mesh Proxy Service 的 UUID 是 0x1828,所有支持 Proxy 功能的节点都必须声明这个服务。

4.2 Proxy Data In 特征值

这个特征值负责接收数据。说白了,它就是 Mesh 网络的入站通道。手机 App 通过这个特征值把 Mesh 协议数据单元(PDU)发给节点。

属性 值
UUID 0x2ADD
属性 Write Without Response(优先)、Write
安全要求 需要加密连接(LE Secure Connections)
最大长度 MTU - 3 字节(默认 20 字节,可扩展)

这里有个容易踩的坑:Write Without Response 是推荐方式。为什么?因为 Mesh 网络本身就有重传机制,不需要 GATT 层再确认一次。如果你用 Write Request,每次写都要等回应,延迟会明显增加。

我曾经犯过的错:在早期一个项目中,我用了 Write Request 来写 Proxy Data In。结果节点数量一多,手机端频繁等待响应,整个配网流程慢得像蜗牛。后来改成 Write Without Response,速度直接翻了三倍。

4.3 Proxy Data Out 特征值

有进就有出。Proxy Data Out 是节点向手机发送 Mesh 数据的通道。它的配置和 Data In 正好互补:

属性 值
UUID 0x2ADE
属性 Notify(必须)、Read(可选)
CCCD 要求 必须使能通知
数据格式 Network PDU 或 Mesh Message

注意看,Data Out 用的是 Notify,不是 Indicate。这意味着节点主动推送数据,手机端不需要确认。你想想看,Mesh 网络里一个消息可能被中继多次,如果每跳都确认,那延迟就爆炸了。

我建议你在实现 Data Out 时,一定要处理好 CCCD(Client Characteristic Configuration Descriptor)。手机端必须先写 CCCD 使能通知,节点才能发数据。这个顺序搞反了,数据就丢了。

小技巧:在调试阶段,可以在 Data Out 的回调里加个打印,每次收到通知请求时输出当前 CCCD 值。这样能快速定位是手机没使能通知,还是节点没发出来。

4.4 节点身份识别

这部分我觉得是 Proxy Service 里最容易被忽视的。节点身份识别,说白了就是让手机知道:你是谁?你属于哪个网络?

Mesh 规范里定义了两种身份识别方式:

  • Network ID:基于网络密钥(NetKey)生成的 8 字节标识。手机端如果已经知道 NetKey,就能通过 Network ID 确认节点属于同一个网络。
  • Node Identity:基于节点密钥(Node Key)生成的 8 字节标识。这个更精确,能直接识别出具体是哪个节点。

我画了一张图,帮你理清这两种识别方式的流程:

节点身份识别流程 手机 App Mesh 节点 扫描广播包 是否已知 NetKey? 是 使用 Network ID 识别 否 使用 Node Identity 识别 建立 Proxy 连接 说明 Network ID:8 字节 Node Identity:8 字节 两者都基于密钥派生

实际项目中,我建议你这样处理:

  1. 先尝试 Network ID:如果手机已经配过网,大概率知道 NetKey。直接匹配 Network ID 最快。
  2. 匹配失败再用 Node Identity:如果 Network ID 对不上,说明节点可能属于另一个网络,或者手机还没拿到 NetKey。这时候用 Node Identity 做精确匹配。
  3. 都匹配不上:节点进入未配网状态,等待 Provisioning。

核心要点:节点身份识别不是可选项,是 Proxy 服务的必选功能。没有它,手机端无法判断该跟谁建立 Proxy 连接。

4.5 实际代码片段

最后,我贴一段实际项目中用过的初始化代码。这是基于 Zephyr 协议栈的,但思路通用:

// Mesh Proxy Service 初始化
static struct bt_gatt_attr proxy_attrs[] = {
    BT_GATT_PRIMARY_SERVICE(BT_UUID_MESH_PROXY),
    
    // Proxy Data In - 只写
    BT_GATT_CHARACTERISTIC(BT_UUID_MESH_PROXY_DATA_IN,
                           BT_GATT_CHRC_WRITE_WITHOUT_RESP |
                           BT_GATT_CHRC_WRITE,
                           BT_GATT_PERM_WRITE_ENCRYPT,
                           NULL, proxy_data_in_write, NULL),
    
    // Proxy Data Out - 通知
    BT_GATT_CHARACTERISTIC(BT_UUID_MESH_PROXY_DATA_OUT,
                           BT_GATT_CHRC_NOTIFY,
                           BT_GATT_PERM_NONE,
                           NULL, NULL, NULL),
    BT_GATT_CCC(proxy_data_out_ccc_cb,
                BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
};

// 注册服务
bt_gatt_register(proxy_attrs, ARRAY_SIZE(proxy_attrs));

注意看 Data Out 的 CCCB 回调。我习惯在里面加个标志位,记录手机是否使能了通知。这样在发送数据时可以先检查,避免无效发送。

避坑指南:我曾经在 Data In 的写回调里直接处理 Mesh 消息,结果导致协议栈死锁。后来改成用消息队列,把收到的数据先丢到队列里,再由主循环处理。这样既安全又高效。

好了,这一章的内容就到这里。Mesh Proxy Service 看似简单,但 Data In/Out 的读写时机、节点身份识别的匹配逻辑,都是实际开发中容易出问题的地方。希望今天的分享能帮你少走弯路。