2. 蓝牙Mesh协议栈与低功耗关联

好,咱们进入正题。上一章聊了蓝牙Mesh低功耗的整体框架,这一章我们聚焦在核心的握手环节——Friend节点与LPN之间的建立过程。

说白了,LPN想睡觉,Friend帮它守着。但这两个节点怎么“认识”?怎么建立信任?怎么约定什么时候该叫醒?这些就是握手机制要解决的问题。

我当年第一次调这个握手流程的时候,踩了不少坑。有一次LPN死活连不上Friend,抓包一看,原来是订阅地址没对上。嗯,这种细节我们后面会讲到。

2.1 为什么需要握手?

你想想看,LPN大部分时间都在睡觉。如果它随便找个Friend就绑定,那Friend可能根本不在它的射频范围内,或者Friend的缓存已经满了。没有握手,就是盲人摸象。

握手的目的有三个:

  • 确认双方能力:LPN要知道Friend能存多少消息,Friend要知道LPN的唤醒间隔。
  • 建立逻辑连接:在Mesh网络里,这不是物理连接,而是一个逻辑上的“朋友关系”。
  • 协商参数:比如轮询超时时间、窗口大小,这些都得谈好。

核心概念:Friend关系是一对一的。一个LPN只能有一个Friend,但一个Friend可以同时服务多个LPN。我见过一个Friend挂了十几个LPN的场景,那家伙的RAM得够大才行。

2.2 握手流程全景图

先看一张流程图,把整个握手过程串起来。后面我们再拆开细讲。

Friend 与 LPN 握手流程 LPN 节点 Friend 节点 1. 扫描 Friend 公告 2. 发送 Friend 请求 3. 发送 Friend 响应 4. 轮询消息 5. 转发缓存消息 ✅ Friend 关系建立完成

这张图我建议你多看两眼。整个流程看起来简单,但每个步骤都有讲究。下面我们逐个拆解。

2.3 第一步:扫描Friend公告

LPN醒来后,第一件事就是听广播。Friend节点会定期发送Friend公告(Friend Advertisement),告诉周围节点:“我在这里,我能当Friend。”

公告里包含什么?

  • Friend的地址:单播地址,用于后续通信。
  • 接收窗口大小(Receive Window):这个很关键,决定了LPN必须在多长时间内来轮询。
  • 支持的订阅列表:Friend会声明它愿意转发哪些组地址的消息。

实战经验:我建议LPN在扫描时不要只盯一个Friend。多收几个公告,对比一下接收窗口大小。窗口越大,LPN的唤醒时间就越灵活。但窗口太大,Friend的缓存压力也大。这是个权衡。

2.4 第二步:发送Friend请求

LPN选定了目标Friend后,就发送Friend请求(Friend Request)。这个请求是单播发给Friend的。

请求里带的信息:

参数 说明 我的建议
轮询超时(PollTimeout) LPN最长能睡多久不轮询 别设太大,否则Friend以为你死了
订阅地址列表 LPN关心的组地址 只订阅必要的,减少Friend缓存压力
Friend选项(Friend Options) 一些标志位,比如是否支持大消息 按实际能力填,别吹牛

这里有个坑。我曾经遇到一个情况,LPN的轮询超时设了100秒,但Friend的缓存只能存60秒的消息。结果LPN每次醒来,消息已经被Friend扔掉了。嗯,所以参数一定要匹配。

2.5 第三步:Friend响应与参数协商

Friend收到请求后,会检查自己能不能接下这个活。如果资源够,就回复Friend响应(Friend Response)。

响应里包含:

  • 接收窗口大小:Friend实际能提供的窗口。
  • Friend的缓存大小:最多能存多少条消息。
  • 更新计数器:用于后续同步。

如果Friend觉得不行——比如缓存不够、订阅列表太长——它会直接拒绝。LPN收到拒绝后,就得找下一个Friend。

注意:Friend响应不是必须的。如果LPN发完请求后没收到响应,它会重试。我建议重试次数设3次,超过就换目标。别死磕一个。

2.6 第四步:轮询与消息转发

握手完成后,LPN就可以安心去睡了。但睡之前,它得做一次轮询(Poll)。

轮询是LPN主动发起的,目的是问Friend:“有没有我的消息?”

轮询的时机:

  1. 刚建立关系时:立即轮询一次,确认Friend有没有缓存的历史消息。
  2. 每次唤醒后:这是常规操作。
  3. 在接收窗口内:LPN必须在Friend的接收窗口内完成轮询,否则Friend会认为LPN失联了。

Friend收到轮询后,会把缓存中属于LPN订阅地址的消息全部转发过去。如果没消息,就回复一个空包。

关键点:轮询间隔必须小于等于轮询超时(PollTimeout)。我习惯把轮询间隔设为PollTimeout的80%,留点余量。比如PollTimeout是10秒,我就设8秒轮询一次。

2.7 握手失败的处理

不是每次握手都能成功。我总结了几种常见失败场景:

失败原因 现象 解决办法
Friend缓存不足 LPN收不到Friend响应 换一个缓存更大的Friend
订阅地址不匹配 Friend拒绝请求 检查LPN的订阅列表是否在Friend的支持范围内
接收窗口太小 LPN来不及轮询 增大接收窗口,或者缩短LPN的唤醒间隔
射频干扰 握手超时 增加重试次数,或者换信道

我记得有一次在工厂调试,LPN和Friend明明在同一个房间,但握手就是超时。后来发现是金属货架挡住了信号。嗯,射频这东西,有时候就是玄学。

2.8 参数配置建议

最后,给几个实际项目中用过的参数配置,供你参考:

  • 接收窗口:100ms ~ 500ms。太短LPN容易错过,太长Friend压力大。
  • 轮询超时:10s ~ 60s。取决于应用场景,传感器可以长一点,控制器要短一点。
  • 重试次数:3次。超过就换Friend。
  • 缓存大小:至少能存10条消息。如果订阅的组多,建议32条以上。

个人习惯:我一般先把接收窗口设成200ms,轮询超时设30s,然后在实际环境中跑一两天,看丢包率再微调。纸上谈兵不如实测。