
刚入蓝牙Mesh这一摊的时候我踩过最典型的一个坑手机端明明已经通过代理连上了某个节点GATT连得稳稳的可上层就是收不到任何Mesh消息。折腾了一整天最后才发现问题出在第七章“Mesh GATT Services”里那个不起眼的Proxy过滤机制上。当时真想把这页规范打印出来贴工位上。所以如果你正在啃《Mesh Profile_v1.0》规范或者正在调试PB-GATT配网、开发手机端配网工具、做网关类的代理节点这一章基本决定了你调试效率的上限。第七章“Mesh GATT Services”看着只有两个服务背后的协议细节却藏了不少坑。这篇文章就顺着规范第七章的骨架把Mesh GATT服务涉及的UUID、特征值、Proxy协议、SAR分段、过滤器这些内容拆开讲清楚同时把我实际调试中遇到的坑一起列出来。1. 为什么Mesh非得在GATT上“开小门”那点广播瓶颈撑不起配网1.1 广播中继的天花板手机为啥“听不见”Mesh节点Mesh网络最底层的承载是PB-ADV和广播中继节点之间通过BLE广播包互传。广播机制天生有个短板一次只能传20多字节的有效数据而且扫描窗口、扫描间隔、后台限制这些因素都会影响接收概率。手机这种通用设备系统级蓝牙扫描并不总是全速跑尤其屏幕熄灭或者App进了后台广播包丢失率会急剧上升。你在嘈杂的射频环境里扫描一个Mesh网络MTU和重发逻辑再好也无济于事。所以在Mesh体系里广播承载只适合节点与节点之间的短距离低功耗通信而手机想稳定接入Mesh网络做配网、诊断、节点控制光靠广播基本不现实。这也是Mesh Profile规范单独用整整一章来规定GATT相关服务的根本原因。1.2 两个GATT服务各管一段配网前和配网后第七章把GATT服务分成两类Mesh Provisioning Service简称MESH_PROV和Mesh Proxy Service简称MESH_PROXY。MESH_PROV负责的是设备还没入网时Provisioner通过GATT连接给未配网设备做配网MESH_PROXY负责的是设备已经入网后为那些不适合直接监听广播的客户端手机、网关、调试器提供一条GATT通道让它们能以连接方式收发Mesh网络消息。配网阶段走PB-GATT承载用的是MESH_PROV服务配网完成后的日常通信走Proxy协议用的是MESH_PROXY服务。两者不是同一个服务也不是同一套特征很多初学者容易把它们混为一谈后面我会把每个服务的UUID和特征值属性掰开讲清楚。1.3 读第七章之前需要先明确的一个边界第七章虽然命名为“Mesh GATT Services”但它不是一份GATT层的完整开发手册。它只规定了服务和特征值的行为、PDU格式和承载转换逻辑。至于GATT连接怎么建立、MTU怎么协商、连接参数怎么配置规范假设你已经熟悉BLE协议栈底层。我建议读这章的时候手里同时准备三个东西一份Mesh Profile v1.0文档、一份蓝牙核心规范里关于Attribute Protocol和L2CAP的章节、再加一台能抓包的设备。抓包工具一定要有后面很多坑光看代码是看不出来的。2. MESH_PROXY服务解剖UUID、特征值属性与连接模型2.1 服务UUID和两个特征值的分工MESH_PROXY服务的UUID是0x1828要注意这是16位UUID在蓝牙技术联盟申请分配的。它包含两个特征值特征值UUID属性方向Mesh Proxy Data In0x2ADDWrite Without Response部分实现兼容Write客户端 → 服务端Mesh Proxy Data Out0x2ADENotify服务端 → 客户端Data In是客户端向服务端发送数据的通道Data Out是服务端向客户端推送数据的通道。所有要通过代理转发的Mesh网络消息都封装成Proxy PDU之后从这个服务进出。我见过一些项目在实现时自作主张把Data In做成普通的Write带响应属性虽然GATT层面也能写但这并不符合规范对吞吐的要求。规范里面明确Data In至少应该支持Write Without Response因为配网和Proxy场景下连续快速发送控制消息的场景很多每一包都等ATT响应会卡住整条链路。2.2 Write Without Response为什么是“唯一”选择为什么这里必须用Write Without Response而不是普通Write这样说吧一条GATT Write操作客户端要等服务器返回ATT响应一来一回至少多个RTT而Write Without Response只需要客户端把包发出去底层链路层有ACK机制保证传输可靠性但ATT层不需要等待应用响应。对于Proxy链路来说上层Mesh协议本身有序列号、重传和确认机制不需要ATT层的逐包确认。这两层职责要是混在一起反而会拖累吞吐。实际用手机测试的时候同样一段数据用带响应的Write发送和用Write Without Response发送在连接间隔稍微放大一点的场景下吞吐差距不止两倍。无线环境本身就不稳定能少一次往返就少一次。2.3 Proxy Server与Proxy Client的角色边界运行MESH_PROXY服务并处于已入网状态的节点就是Proxy Server。连接它的手机、网关、调试工具就是Proxy Client。Proxy Server收到Client通过Data In发来的Proxy PDU后解开封装如果是Network PDU就把这个网络包放进Mesh广播网络里让相邻节点继续中继反过来Server从Mesh网络里收到目标地址匹配的消息也会封装成Proxy PDU通过Data Out通知给Client。需要注意的一个工程细节Proxy Server可以同时挂多个Proxy Client但规范对分段消息的转发有说明服务端在给不同客户端发送分段消息时要避免把不同分段交织混发。建议实现上给每个客户端维护独立的发送队列串行处理分段消息。这点很多自研协议栈容易忽略一旦两个客户端同时接收分段数据重组就全乱了。3. MESH_PROV服务PB-GATT配网承载的专用通道3.1 MESH_PROV的特征设计Mesh Provisioning Service的UUID是0x1827也是16位UUID。它同样有两个特征值特征值UUID属性方向Mesh Provisioning Data In0x2ADBWrite Without ResponseProvisioner → 未配网设备Mesh Provisioning Data Out0x2ADCNotify未配网设备 → Provisioner这个服务的生命周期很有意思它只在设备处于未配网状态时存在。设备一旦完成配网进入Provisioned状态MESH_PROV服务就应该停掉设备重启后也不再广播这个服务。你可以把它理解成一把“一次性钥匙”只用来打开入网这扇门。3.2 配网PDU怎么“塞”进GATT连接PB-GATT承载的传输对象是Provisioning Protocol里的各种PDU比如Invite、Capabilities、Start、Public Key、Confirm、Random、Data等。这些PDU用GATT特征值直接承载。需要注意配网PDU里有几个长度比较大的比如公钥交换是64字节配网Data也可能超过默认ATT MTU 23字节。如果设备还是默认23字节MTU一包根本放不下底层就会拆成多个ATT包。好在规范在Provisioning阶段没有额外定义一套复杂的SAR分段逻辑工程上依赖GATT的MTU协商和协议栈底层自动分包就够了。前提是你得把MTU协商上去别停留在默认值。我在实际测试中就遇到过未配网设备广播了自己的MTU能力但手机端没有做MTU请求导致64字节公钥被拆成好几包时序一紧张就偶发失败。3.3 和Proxy Service的本质区别MESH_PROV和MESH_PROXY虽然长得像但使用场景完全不同MESH_PROV是配网前用的服务端是未配网设备传输的是Provisioning PDU。MESH_PROXY是配网后用的服务端是已配网节点传输的是Proxy PDU里面主要是Network PDU。MESH_PROV只有Provisioner连上它配网流程结束后使命完成MESH_PROXY则是长期服务只要节点开着代理功能就一直存在。如果把这两个搞混最常见的现象就是配网已经完成了手机还执着地去找MESH_PROV服务结果发现扫不到然后怀疑设备进不了配网模式。实际上设备已经入网了服务已经从广播里消失了。4. 代理协议拆包Proxy PDU格式、SAR分段和消息类型4.1 第一个字节背后的信息MESH_PROXY服务上传送的数据是一个个Proxy PDU。Proxy PDU的第一个字节是分段的SAR字段同时也是完整消息的消息类型字段这是初学者最容易绕晕的点。高2位表示SAR状态低6位表示消息类型或段序号具体取决于SAR状态。SAR值高2位含义低6位的作用00完整消息Complete Message表示消息类型01分段消息的第一段First Segment表示段序号10分段消息的中间段Continuation Segment表示段序号11分段消息的最后一段Last Segment表示段序号段序号占低5位范围是0到31。所以一个被分段的消息最多可以有32个分段。对于Mesh网络里常见的Network PDU来说这个数量绰绰有余哪怕将来要承载更长的配网PDU32段也够用。4.2 四种Message Type各管一摊当SAR为00完整消息时第一个字节的低6位就是消息类型。规范定义了四种类型值名称用途0x00Network-PDU承载网络层PDUProxy转发的主要对象0x01Mesh-Bearer承载Mesh Bearer上的消息如配网承载内容0x02Proxy Configuration代理配置消息用于过滤器的设置0x03Provisioning-PDU直接承载配网PDU用于经代理转发配网流程之前调试时我看过一个很迷惑的抓包Data In特征上收到的第一个字节是0x02我当时以为是网络消息类型结果怎么解析都不对。翻规范才发现0x02是Proxy Configuration是客户端在配过滤器。理解这四种类型后很多解析问题就迎刃而解。4.3 分段重组与ATT MTU的那笔账为什么要分段因为ATT MTU限制了单包能承载的数据量。默认MTU是23字节扣除ATT操作码和句柄占用的3字节一次写操作最多带20字节用户数据再扣除Proxy PDU自己占用的1字节SAR头实际上每个分段最多只能放19字节。所以只要消息长度超过19字节就必须拆包。每个分段长度不应当超过ATT_MTU - 4字节这是规范明确建议的。我建议产品实现时把连接MTU尽量协商到最大比如247字节这样每个分段能放243字节绝大多数Network PDU可以一条完整消息直接发出去不需要分段还能省掉接收端重组的时间。MTU协商不上来明明是完整消息愣是被拆成两包出问题的概率直线上升。5. 代理配置和过滤器怎么连上了还是收不到消息5.1 过滤机制的原理Proxy Server为了不让Client被Mesh网络里的广播风暴淹没设计了过滤器Filter。过滤器决定哪些Network PDU会被Server转发给Client。它的规则是Server拿到一个Network PDU后读取PDU里明文的DST目的地址然后用这个地址去匹配过滤器。过滤器有两种工作模式白名单和黑名单。白名单模式下只有目的地址在白名单里的消息才会被转发黑名单模式下目的地址在黑名单里的消息不会被转发。这里必须强调一句过滤器匹配的是目的地址不是源地址。有一个常见误解是我想收来自某个节点的消息就往过滤器里加那个节点的源地址结果怎么都收不到。因为Server判断的是这条消息要去哪不是它从哪来。5.2 四条配置指令的组合使用Proxy Configuration消息类型0x02里第一个字节是Opcode后面跟参数。完整的操作码如下0x00Set Filter Type设置过滤器类型参数1字节0x00白名单/0x01黑名单0x01Add Addresses to Filter添加地址参数为2字节一个的地址列表0x02Remove Addresses from Filter移除地址0x03Set Filter Type and Add Addresses一步完成设置类型和添加地址如果只想配置白名单并添加两个地址可以构造一个完整的Proxy PDU比如02 03 00 01 00 0C 00这里第一个字节0x02表示完整消息且类型是Proxy Configuration第二个字节0x03是组合操作码第三个字节0x00表示白名单后面0x0100和0x000C就是两个2字节地址。实际地址要按你自己的Mesh单播地址和组播地址填。5.3 初始化代理过程的典型顺序一个典型Client连上Proxy Server之后应该先做一件事设置过滤器。因为规范里说得很清楚过滤器初始状态是一个“空的白名单”也就是默认不转发任何消息。你如果连上后什么都不做Server不会主动推任何东西过来这不是连接坏了是规则本身就是空的。合理的初始化顺序是连接MESH_PROXY服务扫描到0x1828。使能Data Out的Notification写CCCD。协商MTU尽量提到较高值。发一条Proxy Configuration把过滤器类型设为白名单并把本机单播地址和感兴趣的组播地址加进去。完成之后Server才会开始转发匹配的Network PDU。我自己的习惯是把“设置过滤器”这一步放在建立连接的早期而不是等业务消息发不出去的时候才想起。nRF Mesh这类成熟App连上节点后实际上也是自动做了过滤器的初始化你只是看不见而已。6. 工程实践排错MTU、抓包、常见协议实现错误6.1 MTU协商不上去导致的“拆包玄学”产品里MTU协商不上去是常态尤其是第三方协议栈默认值保守的时候。MTU协商不上去表现就是偶尔一条消息能收到偶尔收到半截甚至有时候重组超时被丢。我见过同事排查了半天最后发现是Client没发Exchange MTU请求两边都是默认23字节64字节的公钥被拆成4段重新组装的时序稍微一乱就完蛋。建议在GATT连接建立之后立刻发起MTU交换请求别等业务需要了再去查。在Zephyr里一般是bt_gatt_exchange_mtunRF5 SDK里也有对应API。配网场景尤其要优先处理这一步配网PDU普遍偏大。6.2 调试Mesh GATT链路的方法与工具调试这两个服务我推荐两条路硬件抓包器加Wireshark或者直接用手机端nRF Connect的GATT工具做手动读写。nRF Connect能看到0x1828服务的两个特征值。我经常这么干先写CCCD使能Notification然后给Data In发一条Proxy Configuration记得第一个字节用0x02开头再观察Notification里是否有数据。如果啥都没有优先怀疑过滤器和MTU。如果要用Wireshark抓包建议把“Mesh Proxy PDU”解析打开这样第一字节的SAR和消息类型会自动解析出来省去自己手算。调试时有个小技巧看Notification数据流的第一个字节如果一直是0x00开头说明多半是Network PDU如果看到0x02开头说明是配置消息。别傻乎乎地把0x02当作网络消息解析。6.3 常见实现错误和安全注意一个很常见的错误多个Proxy Client同时连接Proxy Server多路分段消息并发如果Server不为每个Client单独管理SAR状态重组缓冲区就会互相污染。规范对分段传输的串行化要求要落实到代码里给每个Client一个独立发送队列。另一个容易被忽略的点是安全要求。规范和实际产品都建议MESH_PROXY和MESH_PROV服务的特征值访问必须在加密连接上进行配网阶段的公钥和认证值如果是在明文链路上传的附近设备抓包就能直接看到。我们做产品的时候至少要求连接达到LE Secure Connections级别的加密必要时再加MITM保护。这一条在规范第七章里虽然只是安全要求的一部分但工程上影响很大。另外提一句适配“配网完成后停用MESH_PROV、启用MESH_PROXY”这个状态切换建议加一个延时和重试机制。现实中很多节点是手机配网后立刻掉线原因就是服务切换得太急Client还没来得及重新发现0x1828服务连接就断了。留一个几百毫秒的窗口能让手机端顺利从配网状态切到代理连接状态。最后再分享一个实践心得我每次调试Mesh GATT链路都会先确认三件事——服务UUID找对没有0x1827还是0x1828、MTU协商到多少、过滤器配了哪些地址。这三件事一确认八成的问题都已经解决了。剩下两成靠抓包看第一字节基本也能定位。第七章篇幅不长但只要抠透这一章配网和代理这两条链路就算真正攥在手里了。