4. Friend节点缓存机制:如何为LPN暂存消息

好,咱们进入第四章节。前面聊了LPN怎么睡、怎么醒,但有个关键问题一直没细说——LPN睡着的时候,别人发给它的消息去哪了?

答案就是:Friend节点。Friend节点就像LPN的“消息收发室”。LPN睡觉时,所有发给它的消息都由Friend节点暂存。等LPN醒来,再去Friend节点那里取。

这个机制,说白了就是用空间换时间,用缓存换功耗。我在做智能照明项目时,一开始没把缓存机制吃透,结果LPN醒来后取消息超时,灯控指令丢了,用户按开关灯不亮……嗯,后来老老实实把Friend缓存调优了一遍,问题才解决。

4.1 Friend节点是谁?凭什么它能当“收发室”

Friend节点不是随便选的。它得满足几个硬性条件:

  • 不能是LPN本身——Friend和LPN是两种角色,互斥。
  • 必须一直在线——Friend不能休眠,得24小时待命。
  • 得有足够的存储空间——缓存消息是要占内存的。
  • 得在LPN的射频范围内——离太远,通信就断了。

我习惯把Friend节点比作“小区快递柜”。LPN是业主,白天上班(休眠),快递员(其他节点)把包裹塞进柜子。业主下班回家(唤醒),扫码取件。快递柜本身得有电、有空间、还得在小区里。

核心要点:一个Friend节点可以同时服务多个LPN,但一个LPN只能绑定一个Friend。这是蓝牙Mesh协议写死的规则。

4.2 缓存什么?——Friend节点的消息队列

Friend节点为每个LPN维护一个消息队列。队列里存的是什么呢?

  • 单播消息:发给这个LPN自己的。
  • 组播/虚拟地址消息:LPN订阅了某个组,组里有消息来了。
  • 配置消息:比如网络密钥更新、模型配置变更。

但注意,不是所有消息都缓存。Friend节点只缓存低功耗节点感兴趣的消息。怎么判断感不感兴趣?看LPN订阅的地址列表。

举个例子:

// LPN订阅的地址列表(伪代码)
lpn_sub_list = {
  0x0001,       // 自己的单播地址
  0xC000,       // 组播地址:客厅灯组
  0xC001        // 组播地址:卧室灯组
}

// Friend节点收到消息后,检查目标地址
if (msg.dst in lpn_sub_list) {
    enqueue_to_friend_cache(msg);
} else {
    // 不感兴趣,直接丢弃或转发
    drop_or_forward(msg);
}

我在项目中遇到过一个问题:LPN订阅了太多组地址,Friend节点的缓存队列很快就满了。结果新消息进不来,旧消息被覆盖。后来我限制了每个LPN的订阅数量,才稳定下来。

4.3 缓存怎么存?——Friend Cache的内部结构

Friend节点的缓存,本质上是一个环形缓冲区。每个LPN对应一个独立的缓存实例。结构大致如下:

字段 说明 大小(典型值)
lpn_address LPN的单播地址 2字节
friend_counter Friend更新计数器,防重放 2字节
queue_size 队列最大容量(消息条数) 1字节
head_index 队列头指针 1字节
tail_index 队列尾指针 1字节
messages[] 消息体数组,每条包含SEQ、SRC、DST、Payload 可变

你想想看,为什么用环形缓冲区?因为内存有限。新消息来了,如果队列满了,就覆盖最旧的消息。这叫“丢老保新”策略。蓝牙Mesh协议就是这么规定的。

注意:如果LPN长时间不唤醒,Friend节点的缓存会被新消息逐步覆盖。LPN醒来后,可能发现某些消息已经丢了。所以LPN的唤醒间隔不能太长,否则消息可靠性无法保证。

4.4 缓存怎么取?——LPN的“取件”流程

LPN唤醒后,会发一条Friend Poll消息给Friend节点。Friend节点收到后,把缓存队列里的消息一条一条发给LPN。流程如下:

  1. LPN唤醒:从休眠状态进入接收模式。
  2. 发送Friend Poll:LPN发一条Poll消息,告诉Friend“我醒了,有消息吗?”
  3. Friend响应:Friend收到Poll后,从队列里取出最早的一条消息,封装成Friend Update消息发出去。
  4. LPN确认:LPN收到消息后,如果还有更多消息,继续发Poll;如果没有了,就发一条Friend Clear消息。
  5. 继续休眠:LPN处理完所有消息,再次进入休眠。

这个流程,我画了一张图,你一看就明白:

LPN取消息流程(Friend Poll交互) LPN(休眠) Friend节点 Friend Cache 消息1:灯控指令 消息2:色温调整 消息3:场景切换 ① 唤醒 ② 发送Poll ③ 返回消息1 ④ 再发Poll ⑤ 返回消息2 ⑥ 取完,发Clear LPN每次Poll只能取一条消息,取完再Poll,直到队列清空

4.5 缓存容量怎么定?——实战中的权衡

缓存容量不是越大越好。我见过有人把队列设成100条,结果Friend节点的RAM直接爆了。蓝牙Mesh协议规定,Friend缓存的最小容量是1条消息,最大没有上限,但实际受限于硬件资源。

我个人习惯这样估算:

  • 每条消息平均大小:约30~50字节(含网络层和传输层开销)。
  • LPN唤醒间隔:假设5秒唤醒一次。
  • 网络消息速率:假设每秒最多10条消息。
  • 所需缓存:5秒 × 10条/秒 = 50条。

所以,如果LPN每5秒唤醒一次,Friend节点至少需要50条消息的缓存空间。换算成内存:50 × 50字节 = 2500字节,约2.5KB。对于大多数MCU来说,这个开销可以接受。

实战建议:如果你用的是Nordic nRF52840或Silicon Labs EFR32系列,建议把Friend缓存设为32~64条。太小了容易丢消息,太大了浪费内存。我曾经在一个项目中把缓存设为128条,结果Friend节点的堆栈溢出了……嗯,后来改回64条,稳如老狗。

4.6 避坑指南——我踩过的三个坑

做Friend缓存,有几个坑我替你们踩过了,直接说:

  1. 坑一:Friend节点掉电重启,缓存全丢。 缓存是存在RAM里的,掉电就没了。LPN醒来后Poll不到消息,还以为网络出问题了。解决方案:Friend节点掉电后,LPN要重新建立Friend关系。
  2. 坑二:LPN唤醒太频繁,Friend节点来不及处理。 如果LPN每秒唤醒一次,Friend节点每秒都要处理Poll和Update,CPU负载飙升。我建议LPN唤醒间隔至少2秒以上。
  3. 坑三:多个LPN共用一个Friend,缓存互相挤占。 每个LPN的缓存是独立的,但总内存是共享的。如果A LPN的消息特别多,可能会挤占B LPN的缓存空间。解决办法:给每个LPN设置最大缓存上限。

4.7 小结

Friend节点缓存机制,是蓝牙Mesh低功耗设计的核心。没有它,LPN根本没法睡觉。记住三个关键点:

  • 缓存什么:只缓存LPN订阅的消息。
  • 怎么存:环形缓冲区,丢老保新。
  • 怎么取:Poll一次取一条,取完为止。