3. 心跳机制与健康监测:配置心跳发布与订阅、心跳消息格式、基于心跳的节点存活检测算法
设备掉线了,你什么时候才知道?
很多做蓝牙Mesh的朋友,一开始都觉得配网成功就万事大吉了。结果呢?设备在角落里默默离线,用户那边灯控没反应了,后台还显示“在线”。这种问题我遇到过太多次了。
心跳机制,说白了就是让节点定期喊一嗓子:“我还活着!”
这一章,我们就来把心跳这件事聊透。从消息格式到发布订阅配置,再到我自己项目里用过的存活检测算法,全给你掰开揉碎了讲。
3.1 心跳消息长什么样?
蓝牙Mesh的心跳消息,不是你想发什么就发什么。它有一套固定的格式,咱们先把这个基础打牢。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 源地址 | 2 | 发送心跳的节点单播地址 |
| 目的地址 | 2 | 订阅了心跳的组播地址或单播地址 |
| 心跳消息 | 1 | 包含心跳计数和TTL信息 |
这个1字节的心跳消息,里面其实塞了两个东西:
- 心跳计数(4位):每次心跳发布时递增,从0到15循环。用来判断有没有漏掉心跳。
- 心跳TTL(4位):消息转发时经过的中继跳数。可以用来估算节点距离。
嗯,就这么简单。但简单归简单,配置起来有不少坑。
核心要点:心跳消息本身不携带业务数据,它就是一个“我还活着”的信号。别想着往里面塞传感器数据,那是另外的模型干的活。
3.2 心跳发布与订阅配置
配置心跳,你得操作两个东西:心跳发布和心跳订阅。
我个人习惯把发布端叫做“被监测节点”,订阅端叫做“监测中心”。
3.2.1 配置心跳发布
发布端要设置三个参数:
- 发布地址:心跳发到哪个组播地址或单播地址。
- 发布周期:多久发一次。单位是秒,一般建议10秒到60秒。
- 心跳消息初始TTL:默认用网络TTL就行,但如果你想控制传播范围,可以单独设。
配置命令长这样(以Config模型为例):
// 配置心跳发布
Config_Heartbeat_Publication_Set(
dst: 0xC000, // 组播地址,所有监测节点都订阅这个地址
period: 10, // 每10秒发一次
ttl: 7, // 初始TTL
features: 0x00 // 不携带功能特性
);
这里有个细节:features字段可以携带节点当前的中继、代理、朋友等功能状态。但我建议初期先设为0,等稳定了再考虑加。
3.2.2 配置心跳订阅
订阅端要设置的就少一些:
- 订阅地址:监听哪个地址上的心跳消息。
- 心跳计数预期:用来判断是否连续丢包。
// 配置心跳订阅
Config_Heartbeat_Subscription_Set(
src: 0x0001, // 订阅来自节点0x0001的心跳
period: 10, // 预期每10秒收到一次
count: 3 // 连续丢失3次判定为离线
);
我的经验:订阅端的count参数别设太小。我曾经设成2,结果网络稍微波动一下,设备就被误判离线了。后来改成3,误报率降了80%。
3.3 基于心跳的节点存活检测算法
配置好了,数据也收到了。接下来就是核心问题:怎么判断一个节点还活着?
我见过不少方案,有的简单粗暴,有的精细复杂。这里我分享三种,从简单到复杂,你自己选。
3.3.1 超时判定法(最简单)
这个法子,说白了就是:
- 记录上次收到心跳的时间。
- 如果超过N秒没收到,就判定离线。
伪代码:
uint32_t last_heartbeat_time = 0;
uint32_t timeout_threshold = 30; // 30秒超时
void on_heartbeat_received() {
last_heartbeat_time = get_system_time();
}
bool is_node_alive() {
uint32_t now = get_system_time();
if (now - last_heartbeat_time > timeout_threshold) {
return false; // 离线
}
return true; // 在线
}
优点:实现简单,CPU开销小。
缺点:容易误判。网络延迟高一点,或者心跳刚好卡在边界上,就误报了。
注意:超时阈值至少要设成心跳周期的3倍。比如心跳周期10秒,阈值就设30秒。别问我怎么知道的,问就是吃过亏。
3.3.2 心跳计数连续性检测法
这个方法比超时判定靠谱一些。它利用心跳消息里的4位计数。
原理:
- 每次收到心跳,检查计数是否连续递增。
- 如果计数跳变了(比如从5直接跳到8),说明中间丢了2个心跳。
- 连续丢包超过阈值,判定离线。
uint8_t expected_count = 0;
uint8_t lost_count = 0;
uint8_t max_lost = 3; // 最多允许连续丢3个
void on_heartbeat_received(uint8_t count) {
if (count != expected_count) {
// 计数不连续,说明丢了包
lost_count += (count - expected_count) & 0x0F;
} else {
// 正常收到,重置丢包计数
lost_count = 0;
}
expected_count = (count + 1) & 0x0F;
}
bool is_node_alive() {
return lost_count < max_lost;
}
这个方法好在哪里?它能感知到“丢包率”。就算心跳还在发,但如果丢包率突然升高,也能提前预警。
我在一个智能照明项目里用过这个方案。当时有个节点位置比较偏,信号时好时坏。用超时判定法,经常误报。换成计数检测法后,准确率提升了很多。
3.3.3 综合健康评分法(推荐)
这个方法是我在实际项目中打磨出来的。它不只看心跳,还结合了其他指标。
评分维度:
| 指标 | 权重 | 说明 |
|---|---|---|
| 心跳连续性 | 40% | 基于计数检测的丢包率 |
| 心跳延迟 | 30% | 从TTL估算的跳数,跳数越多延迟越高 |
| 响应成功率 | 30% | 主动发送消息后,节点回复的成功率 |
评分公式:
uint8_t calculate_health_score() {
uint8_t score = 0;
// 1. 心跳连续性评分(0-40分)
if (lost_count == 0) score += 40;
else if (lost_count == 1) score += 30;
else if (lost_count == 2) score += 15;
else score += 0;
// 2. 心跳延迟评分(0-30分)
if (ttl <= 3) score += 30;
else if (ttl <= 6) score += 20;
else score += 10;
// 3. 响应成功率评分(0-30分)
uint8_t response_rate = (success_responses * 100) / total_requests;
if (response_rate >= 90) score += 30;
else if (response_rate >= 70) score += 20;
else score += 10;
return score;
}
bool is_node_healthy() {
return calculate_health_score() >= 60; // 60分及格
}
这个方案的好处是:它不会因为一次网络抖动就判离线。而是综合多个维度,给出一个健康度评分。低于60分才触发告警。
实战建议:综合评分法的阈值可以动态调整。比如白天网络负载高,可以把阈值降到50分;晚上负载低,提到70分。我在一个商场项目里就是这么干的,误报率降到了1%以下。
3.4 心跳机制的整体流程
说了这么多,咱们用一张图把整个流程串起来。
3.5 避坑指南
最后,分享几个我在项目里踩过的坑。你遇到了,直接绕过去。
坑1:心跳周期设得太短
我曾经把心跳周期设成2秒,想着实时性高一点。结果网络拥塞了,心跳消息互相挤占带宽,业务消息反而发不出去。后来改成10秒,问题解决。
建议:心跳周期不要短于5秒。一般10-30秒比较合理。
坑2:所有节点都往同一个组播地址发心跳
100个节点,每10秒发一次,组播地址上每秒要处理10条心跳。如果节点更多,处理不过来就会丢包。
建议:按区域或功能分组,每个组用不同的组播地址。比如“一楼灯光”一个组,“二楼灯光”一个组。
坑3:忽略心跳消息的TTL字段
TTL不仅能用来控制传播范围,还能反映节点间的网络质量。如果某个节点的TTL突然变大,说明它可能绕路了,网络拓扑发生了变化。这个信息可以用来做故障预判。