智能家居 ·

从自定义蓝牙广播到 Mesh 组网:多跳转发之外,还要补齐什么?

自定义蓝牙广播怎样演进为多跳 Mesh?从消息身份、去重、转发预算、确认与幂等,到掉电防重放、低功耗和故障验证,梳理自研组网需要承担的设计工作。

假设你已经有了一套蓝牙广播控制:控制器发出“开灯”,附近的灯收到消息,亮起来。设备和应用都在自己手里,协议也不复杂。

现在想把覆盖范围扩大一点。客厅的控制器够不到卧室,让走廊里的灯帮忙转发一次,似乎就能解决。

第一版实现很容易想到:收到包,检查是不是自己认识的协议,再广播出去。

但只要走廊里不止一盏灯,问题就来了。两盏灯都转发,同一条消息从两个方向到达;转发出去的消息又被邻居收到,邻居再转回来。原来只需要判断“是不是发给我的”,现在还得回答:这条消息我见过没有,该不该继续转,执行过没有,发起者知不知道结果?

上一篇讲了自定义 Mesh、SIG Mesh 和小米 Mesh 的关系。这篇顺着一个具体场景往下拆:如果已有自定义蓝牙广播协议,要增加真正的多跳能力,需要补齐哪些东西。

下面用“控制器—中继—灯”的假设网络说明设计取舍,不对应某款产品的实测。我们先讨论如何把系统做可靠,再判断是否值得自己维护一套 Mesh。

第一步,给消息一个不会随着转发改变的身份

单跳控制时,很多人首先关心命令格式:哪个字节表示开关,哪个字节表示亮度。增加中继以后,消息身份同样重要。

比如控制器 A 发了一次开灯请求,走廊里的 B 和 C 都收到并转发,卧室灯 D 就可能收到两份。D 必须知道,这是同一次发送的两个副本。

中继不能把消息当成自己新发的一条业务命令。否则 B 转发时换一个来源,C 再换一个来源,接收端就无法按原始消息判断重复。无线链路上看到的发送者可以变化,但协议里需要保留原始来源和消息标识。

在自定义协议设计阶段,我会把这些信息分别列出来:

信息 回答的问题
网络或安全域标识 这条消息属于哪个系统?不能仅凭这个字段就信任它
原始来源与目标地址 谁发起,发给单个设备还是一个组?
消息序号及必要的代际信息 它是新的传输消息,还是见过的副本?
转发预算 还允许继续经过多少次中继?
业务请求标识 多次传输是否属于同一次业务操作?
操作、参数与认证信息 要做什么,消息是否可信、是否被篡改?

这是一张职责清单,不是可以直接照抄的报文格式。哪些字段放在头部、占多少字节、哪些需要认证,要和寻址范围、密钥管理、分包方式一起设计。

尤其要分清两种“重复”:网络里出现同一个包的副本,与应用层为了同一个操作重新发包,并不总是一回事。 后面处理确认和重试时,这个区别会直接影响灯会不会执行两次。

第二步,把“再转一次”变成有限的动作

最直接的办法是设置一个转发预算。每经过一次中继,预算减少,耗尽就不再向外传播。这样即使网络里存在环,也能限制一条消息的传播范围。

但预算解决的是“还能走多远”,没有解决“同一个节点会转多少次”。B 已经转过 A 的消息,随后从 C 又收到同一个副本,如果再次转发,仍然会产生多余流量。

因此还需要消息缓存:记住近期处理过的消息身份,重复收到时,通常不再启动一次新的转发流程。

这里不能简单写成“收到重复包就全部丢掉”。网络层可以抑制重复传播,业务层可能还需要对重试返回已保存的处理结果。两层各有职责,不能只用一个全局的“见过了”判断覆盖所有情况。

缓存大小和保留时间也有代价。太短,较晚到达的副本可能又被当成新消息;太长,占用更多内存,消息编号复用时也更难处理。应该根据消息产生速率、允许的传播时间和重试窗口估算,再用负载测试确认。

另一个细节是接收顺序。至少应完成必要的格式、所属网络和认证检查,再把消息纳入可信的处理状态。不能让伪造的消息提前占据有效记录,导致后面真正的命令被当成重复包。

Bluetooth SIG 的 Mesh 入门文档介绍了标准中的中继和 TTL。我们可以借鉴其解决的问题,但自定义报文里的“转发预算”并不自动具有 SIG Mesh 的字段语义,也不能直接拿两者的数字作比较。

第三步,决定谁应该中继,什么时候中继

能收到消息的设备很多,不代表它们都应该立刻转发。

假设几台设备几乎同时收到一个广播,又几乎同时发出自己的副本。原本想提高成功率,结果可能增加冲突,让接收更困难。

设计时通常要同时考虑中继资格、转发时机、有限次数的重复发送以及队列上限。可以引入受控的随机延迟,减少多个节点总在同一时刻竞争;也需要限制一次消息带来的发送量,避免队列持续膨胀。

随机延迟不是越大越好。它会增加响应时间,多跳还会累积。重发次数也不是越多越可靠:在拥挤的网络里,增加副本可能进一步挤占正常消息的机会。

所以“每台灯都开中继”“丢包就把重发加倍”,不适合作为默认答案。要看设备分布、供电条件和实际负载。

供电尤其重要。持续扫描和帮助别人转发,需要消耗能量。电池开关能够发送自己的按键事件,不等于适合长期充当中继。

SIG Mesh 的 Low Power 与 Friend 机制提供了一种分工:低功耗节点不必持续接收,由 Friend 暂存发给它的消息。自定义方案如果也要让休眠设备接收指令,就需要安排接收窗口、缓存或其他协调机制,不能只在协议表里写一句“支持电池设备”。

第四步,明确“成功”到底发生在哪一层

这是我最希望在协议设计前就讨论清楚的部分。

控制器把一个包交给蓝牙模块,模块返回“发送完成”。这能说明模块完成了相应发送操作,却不能证明卧室里的灯收到了,更不能证明灯已经亮了。

即使目标设备回复了,也要看回复代表什么:收到了命令、接受了命令、已经执行,还是检测到了实际状态?

例如继电器控制器可以报告输出已经置位,但如果没有电流或其他反馈,它并不知道负载是否真的工作。界面上应该展示哪种状态,必须与设备的观测能力一致。

确认消息也不应该默认“每个中继都替最终设备报成功”。那最多能说明消息到达了中间一跳。对于需要确认最终状态的操作,要有明确的端到端响应和请求关联。

不过,给每条消息都加确认,又会带来另一个问题:组播。

控制器让一组灯关掉,如果所有灯立刻回应,再经过中继返回,回程流量可能比下行命令多得多。Bluetooth SIG 的入门文档也指出,需要确认的消息适合单个目标,但面向大量目标时扩展性较差。

可以根据业务选择不同办法:重要配置逐台确认;普通组控使用有限重发,再分散查询状态;需要完整结果时,定义明确的汇总与超时机制。无论选哪种,都要承认它能提供什么保证。不能既省掉状态核对,又在界面上宣称“全部设备执行成功”。

第五步,重试时别让“开灯”变成“开了又关”

设想灯已经执行了命令,但响应在返回途中丢失。控制器超时,于是重新发送。

如果命令是“切换一次开关状态”,执行两次就会回到原来的状态。用户看到的可能是灯闪了一下,又灭了。

如果命令是“把开关状态设为开”,重复执行通常不会产生同样的问题。对于这类目标状态操作,让重复请求保持相同效果,是很有用的设计方式。

但不能因此把所有动作都看成安全可重试。“电机移动一步”“扣减一次计数”“启动一次定时任务”,都可能产生重复副作用。它们需要业务请求标识、结果记录和相应的恢复策略。

网络消息序号与业务请求标识可以分别承担两件事:新的传输尝试使用符合安全规则的消息身份,业务层仍然认出它属于之前的操作。如果已经处理过,就按协议返回结果,而不是再次执行。

还有一种更隐蔽的错误:先发“亮度设为 30%”,后来用户改成 80%;第一条命令的迟到重试最后才到,灯又退回 30%。两条命令分别都是目标状态操作,单独看都可以重复执行,却仍然发生了状态倒退。

这时需要处理的是时序与有效性,比如同一控制流里的版本关系、请求过期,或者新操作取消旧重试。多个控制器同时操作时,还要定义谁有权覆盖谁。

“可重复执行”和“结果一定正确”之间,还隔着这些条件。对于执行后立刻掉电、来不及记录结果的设备,也不能轻易承诺绝对只执行一次。

第六步,重启不能把安全状态一起清零

只要有人可以在附近接收广播,就应考虑旧消息被录下来再发送的情况。

加密本身不能完整解决这个问题。一条曾经合法的密文,即使没有被修改,仍然可能在不该执行的时候被再次提交。

这也是短期去重缓存和防重放状态不能混为一谈的原因。为了节省内存清掉几秒前的缓存,不应意味着几秒前的控制命令重新获得执行资格。

设计需要同时考虑发送端与接收端。发送端重启后如何避免安全相关序号或 nonce 被不安全地重复使用?接收端怎样避免遗忘已处理的安全状态后,再接受旧消息?恢复出厂设置、重新入网、换钥匙又怎样改变这些关系?

对于要求同一密钥下 nonce 不重复的认证加密算法,不能简单把发送计数器放在 RAM 里,然后每次启动从零开始。多个发送者共享某些密钥时,也要避免它们使用的 nonce 相互冲突。具体构造应遵循所选算法和成熟协议的要求。

RFC 5116专门讨论了认证加密与 nonce 的使用要求,也明确说明其接口本身并不提供完整的防重放和访问控制。这一点很容易被一句“我们已经用了 AES”掩盖。

持久化还会引出 Flash 写入频率、掉电一致性和恢复策略。一种工程思路是预留一段计数空间,再从已持久化的边界继续使用,但具体落地仍要证明掉电后不会回退或复用,不能只靠正常重启测试。

密钥管理也不能等到最后再补。设备如何安全加入、丢失或转卖后如何移除、旧设备怎样失去权限,都应该从第一版协议开始考虑。只在 App 中删掉一条设备记录,并不自动撤销设备保存的密钥。

第七步,先做小消息,再考虑分包和升级

开关命令通常很短,所以原型阶段容易忽略消息大小。一旦加入批量配置、长属性或固件升级,就会遇到另一组问题。

一条业务消息装不进当前承载时,怎么分片?每片怎样标识,接收端怎样重组,缺一片怎么办?缓存多久,最多允许多少条消息同时重组?

不能把 BLE 广告、扩展广告或某个 SDK 的理论容量,直接等同于自己的可用业务载荷。协议头、安全字段、设备支持范围和承载方式都会占用或限制空间。

分片还会放大成本:一个业务操作变成多个无线包,每个包都可能需要中继。若重试策略不当,少量配置流量就可能压住正常控制消息。

我的倾向是先把短控制、状态上报和恢复机制做好;固件升级另行设计其传输、流量控制、签名校验及失败恢复。能够通过 Mesh 传递数据,不等于已经具备可靠的 OTA 系统。

怎么验证它已经超过“演示能跑”的阶段

不必一开始就追求很大的网络,先用少量节点把故障路径暴露出来。下面是我会安排的验证项目,而不是已经测出的结果。

场景 重点观察什么
同一命令经过两个中继到达 副本是否受到限制,目标是否重复执行
命令执行后丢失响应 超时重试是否产生第二次副作用,能否返回已知结果
新命令先到、旧重试后到 状态会不会被迟到请求覆盖
中继断电或移走 有替代路径时能否继续工作;没有路径时是否如实报失败
多设备同时上报、组控后集中应答 延迟、失败比例、重发量和队列是否明显恶化
发送端与接收端分别在关键时刻掉电 序号、nonce、请求记录和防重放状态能否正确恢复
设备被移除或重新入网 旧密钥、旧消息和旧绑定是否仍能产生不应有的操作

测量时也要先写清楚起点和终点。是从应用提交命令到模块发出,还是到目标设备报告执行完成?如果没有检测真实负载的传感器,就不要把后一种结果称为实际灯光响应时间。

除了平均延迟,还应记录较慢一端的分布、超时比例、重复执行次数和无线发送量。例如报告中位数与 P95 时,要同时给出样本量,并单独报告失败请求,不能把超时全部排除后,只展示成功样本的漂亮数字。

设备摆放、供电、Wi-Fi 干扰、扫描设置和业务频率也要记录。否则今天的结果和明天的结果可能根本不在同一条件下。

做到这里,再判断是不是要继续自研

把广播增加一次转发,可能只改几行代码。把它变成可维护的网络,则需要一套相互配合的规则:身份、寻址、有限转发、去重、确认、业务重试、安全状态、恢复和诊断。

这不意味着所有产品都必须照着 SIG Mesh 重新实现一遍。业务范围明确、设备完全可控、已有大量存量产品时,维护独自设计的方案仍然可能有价值。关键是清楚自己省略了哪些能力,以及这些省略是否符合产品需求。

如果只是因为现有广播协议很简单,就顺势觉得“加中继也应该很简单”,最好在原型阶段停下来算一遍后续成本。可以买现成的成熟方案,也可以采用标准协议栈,把精力留给设备功能和网关业务。

对我们的 Matter 网关方向而言,更值得保留在自己手里的,是产品行为、状态映射和异常处理。无线组网层到底自研多少,要看能否持续验证和维护,而不只是第一版能否跑通。

下一篇回到 SIG Mesh:一个新设备从被发现、完成 Provisioning,到真正响应开关命令,中间发生了什么? 把网络密钥、应用密钥、模型绑定和发布订阅连起来,前面这些问题就有了一套更具体的参照。

如果你手里也有一套正在运行的私有广播协议,可以从“为什么需要中继、哪些设备常供电、最不能接受哪种失败”开始梳理。欢迎通过这里交流。