接入低功耗休眠唤醒
设备主控进入低功耗状态后,可以由休眠模块与 TiRTC 休眠唤醒服务器保持 TCP 连接。客户端呼叫设备或业务服务端提交唤醒请求时,休眠模块会收到唤醒数据,然后启动主控和 TiRTC SDK。
这种方式适合可视门铃、摄像机等需要长时间休眠、但仍需响应呼叫或业务指令的设备。完成接入后,你可以让设备在工作状态和休眠状态之间切换,并通过客户端呼叫或业务服务端的设备唤醒接口将设备唤醒。
接入前先确认设备架构是否适合:
| 设备情况 | 是否使用本文方案 |
|---|---|
| 主控休眠后,独立休眠模块仍能联网并维持 TCP | 适合 |
| 主控和联网模块会同时断电或断网 | 不适合,休眠期间无法接收远程唤醒 |
| 主控长期运行,TiRTC SDK 始终在线 | 通常不需要额外的休眠连接 |
| 休眠模块只能匹配固定唤醒报文,没有编程解析能力 | 可以接入,但只能使用默认唤醒包,不能使用自定义数据 |
| 休眠期间还需要处理复杂业务 | 需要先确认休眠模块的计算、存储和网络能力;本文只解决保活和唤醒 |
工作流程
下图只展示接入方需要交互的公开边界。TiRTC 平台内部服务和路由过程不属于设备接入契约。
一次完整的休眠和唤醒流程如下:
- 主控启动 TiRTC SDK,并启用休眠唤醒能力。
- SDK 通过回调返回休眠服务器、登录数据和心跳数据。
- 设备准备休眠时,把最新参数交给休眠模块。
- 休眠模块选择一个或多个休眠服务器建立 TCP 连接,发送登录数据并定期发送心跳。
- 客户端连接设备时,TiRTC 自动提交唤醒;需要由业务事件主动唤醒设备时,业务服务端可以调用提交设备唤醒请求。
- 休眠模块收到唤醒数据后关闭休眠连接,启动主控和 TiRTC SDK。
主控和休眠模块的职责如下:
| 阶段 | 主控和 TiRTC SDK | 休眠模块 |
|---|---|---|
| 启动 | 启用能力并取得最新休眠参数 | 等待主控传递参数 |
| 准备休眠 | 复制并传递完整参数 | 校验参数,建立 TCP 并发送登录数据 |
| 休眠期间 | 停止运行或进入低功耗状态 | 发送心跳、接收唤醒包并处理重连 |
| 收到唤醒 | 尚未运行 | 幂等触发主控启动,关闭全部休眠连接 |
| 恢复在线 | 重新启动 TiRTC SDK 并恢复业务 | 停止本轮休眠连接和重连任务 |
开始前准备
开始接入前,确保已经:
- 申请开通 TiRTC,并取得设备身份和应用级凭证;
- 完成 C SDK 接入;
- 让设备在主控休眠期间仍有一个模块能够维持 TCP 连接。
如果设备休眠期间无法联网,或者主控可以一直运行 TiRTC SDK,则不需要单独建立休眠连接。
接入 SDK
1. 启用休眠唤醒
在 TiRtcStart() 之前设置 TIRTC_OPT_WAKEUP:
int enable_wakeup = 1;
int code = TiRtcSetOption(TIRTC_OPT_WAKEUP,
&enable_wakeup,
sizeof(enable_wakeup));
if (code != 0) {
/* 记录错误并停止启动流程 */
}这个选项表示设备本次启动需要使用休眠唤醒能力。平台会在设备启动时校验对应权限;校验通过后,SDK 才会取得休眠唤醒参数。
2. 注册参数回调
在 TIRTCCALLBACKS 中设置 on_sleep_wakeup_info:
static void on_sleep_wakeup_info(const TIRTC_SLEEP_WAKEUP_INFO *info)
{
save_sleep_wakeup_info(info); /* 校验后复制整个结构体,并原子替换旧参数 */
}
static const TIRTCCALLBACKS kCallbacks = {
.on_event = on_event,
.on_sleep_wakeup_info = on_sleep_wakeup_info,
};info 指针只在当前回调期间有效,服务器列表和登录数据都是结构体内嵌数组。需要在主控关闭后继续使用时,应在回调返回前复制整个结构体或所需字段:
| 参数 | 说明 |
|---|---|
servers、server_count | 休眠服务器地址,返回 1 到 3 个 |
login_data、login_data_len | TCP 建连后立即发送的登录数据,长度为 1 到 64 字节,有效期为 7 天 |
休眠连接使用固定心跳协议:休眠模块发送 ASCII 数据 HB,长度为 2 字节,不包含 C 字符串结尾的 \0;建议心跳间隔为 60 秒,最大允许间隔为 300 秒。
不要解析或自行生成 login_data。SDK 再次回调新参数时,完整覆盖之前保存的服务器和登录数据,不能混用不同批次的数据。
保存参数前至少检查 info 非空、server_count 在 1..3 内、login_data_len 在 1..64 内,以及选用的服务器地址和端口有效。校验或保存失败时,不要让主控进入休眠。
当前接入流程只保证使用本次 SDK 启动后回调的最新参数。除非 TiRTC 明确确认参数可以跨启动复用,否则不要在设备重启后直接使用上一次持久化的登录数据;本次没有收到有效回调时,应保持主控运行并记录失败原因。
3. 启动 SDK 并等待参数
int code = TiRtcStart(device_id, &kCallbacks);
if (code != 0) {
/* 启动请求提交失败 */
}如果服务端返回有效休眠参数,on_sleep_wakeup_info() 会在 TiRtcStart() 内由调用线程同步触发,并且可能早于 TiRtcStart() 返回。调用 TiRtcStart() 前,应先准备好保存参数所需的内存和同步对象;回调内只校验并复制参数,不要阻塞等待 SDK 启动完成。
SDK 启动成功时,on_event(TIRTC_EVENT_SYS_STARTED) 会在 TiRtcStart() 的调用线程同步触发,并且早于函数返回。调用前应同样准备好状态记录,不要在函数返回后才开始等待这个事件。TiRtcStart() 返回 0 后,还要确认本次启动已经保存有效休眠参数,才能让设备进入休眠。
4. 进入休眠前检查
按以下顺序切换到休眠状态:
- 确认当前没有通话、推流或其他不能中断的设备任务。
- 确认已取得、校验并复制本次启动返回的服务器和登录数据。
- 将同一批次的服务器和登录数据交给休眠模块,并配置固定心跳数据
HB、60 秒建议间隔和 300 秒最大间隔。 - 等待休眠模块至少建立一条 TCP 连接,并完整发送
login_data。服务器不会返回登录成功确认,因此不能等待成功回包。 - 确认休眠模块已经启动心跳、接收和重连任务。
- 停止不再需要的主控业务,再让主控进入休眠。
任一步失败时保持主控在线,不要进入一个既没有 TiRTC SDK、也没有有效休眠连接的状态。
接入休眠唤醒服务器
1. 选择服务器
SDK 最多返回 3 个休眠服务器。设备必须至少选择一个可用服务器连接,但不要求连接全部服务器:
- 只连接一个服务器时,实现最简单;该连接异常后,需要尝试其他返回的服务器。
- 连接多个服务器时,可以同时维持多条休眠连接;同一次唤醒可能通过每条连接各下发一次,设备必须自行保证唤醒操作幂等。
每条连接独立完成登录、心跳、读取和重连。不要把一条连接的登录数据发送结果当作其他连接的结果。
2. 登录并发送心跳
对选中的每台服务器执行:
- 使用
host和port建立 TCP 连接。 - 建连成功后立即完整发送
login_data。 - 不等待登录成功回包,按选定间隔发送固定心跳数据
HB。 - 持续读取服务器数据,等待唤醒指令或错误响应。
login_data 的有效期为 7 天。休眠服务器只在建立登录状态时校验有效期;连接登录成功后,即使 login_data 随后超过有效期,现有连接也不会因此被断开。
登录成功的连接发生断开后,如果在 5 分钟内重新连接原来的休眠服务器,服务器不会再次校验 login_data 的有效期。超过 5 分钟再重连,或者改连其他休眠服务器时,按正常登录流程校验有效期。重连时仍应完整发送原来的 login_data,不能省略登录数据。
下面的 C 示例适用于具备编程和数据解析能力的休眠模块,展示单条已连接 socket 的核心保活逻辑。connect_sleep_server()、save_sleep_wakeup_info() 和 wakeup_device_once(custom_data) 由设备根据自身网络栈和任务模型实现。只能匹配固定报文的模块不需要实现自定义数据解析,应按后文说明配置完整的默认唤醒包。
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>
#define SLEEP_HEARTBEAT_INTERVAL_SECONDS 60
#define SLEEP_HEARTBEAT_INTERVAL_MAX_SECONDS 300
static const uint8_t kSleepHeartbeatData[] = {'H', 'B'};
static int send_all(int fd, const void *data, uint32_t len)
{
const uint8_t *p = (const uint8_t *)data;
uint32_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, p + sent, len - sent, 0);
if (n > 0) {
sent += (uint32_t)n;
} else if (n < 0 && errno == EINTR) {
continue;
} else {
return -1;
}
}
return 0;
}
/* 唤醒包最后两字节按高字节在前组合为 uint16_t。 */
static uint16_t parse_custom_data(const uint8_t wakeup_packet[8])
{
return ((uint16_t)wakeup_packet[6] << 8) |
(uint16_t)wakeup_packet[7];
}
/* 返回 1 表示收到唤醒数据,返回 -1 表示连接异常。 */
static int keep_alive(int fd, const TIRTC_SLEEP_WAKEUP_INFO *info,
uint16_t *custom_data)
{
static const uint8_t wakeup_prefix[6] = {
0x98, 0x3b, 0x16, 0xf8, 0xf3, 0x9c
};
uint8_t recv_buf[128];
if (custom_data == NULL) {
return -1;
}
if (send_all(fd, info->login_data, info->login_data_len) != 0) {
return -1;
}
for (;;) {
fd_set read_fds;
struct timeval timeout;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
timeout.tv_sec = SLEEP_HEARTBEAT_INTERVAL_SECONDS;
timeout.tv_usec = 0;
int ready = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (ready < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
if (ready == 0) {
if (send_all(fd, kSleepHeartbeatData,
sizeof(kSleepHeartbeatData)) != 0) {
return -1;
}
continue;
}
uint32_t received = 0;
while (received < 8) {
ssize_t n = recv(fd, recv_buf + received,
8 - received, 0);
if (n > 0) {
received += (uint32_t)n;
} else if (n < 0 && errno == EINTR) {
continue;
} else {
return -1;
}
}
if (memcmp(recv_buf, "ERR|", 4) == 0) {
return -1; /* 登录数据校验失败 */
}
if (memcmp(recv_buf, wakeup_prefix, sizeof(wakeup_prefix)) != 0) {
return -1; /* 未知数据 */
}
*custom_data = parse_custom_data(recv_buf);
return 1; /* 收到唤醒数据 */
}
}
static int handle_sleep_connection(
int fd, const TIRTC_SLEEP_WAKEUP_INFO *info)
{
uint16_t custom_data = 0;
int result = keep_alive(fd, info, &custom_data);
if (result == 1) {
if (custom_data == 0x1234) {
/* 处理业务约定的 0x1234 唤醒原因或指令。 */
}
wakeup_device_once(custom_data);
}
return result;
}示例使用固定建议值 60 秒作为等待时间。等待超时后发送两字节 ASCII 心跳数据 HB;等待期间收到非错误数据,则进入唤醒流程。默认唤醒包解析得到 custom_data == 0x0000;包尾为 12 34 时解析得到 custom_data == 0x1234。
如何选择心跳间隔
- 建议心跳间隔固定为 60 秒。没有特殊功耗或网络要求时,按该周期发送
HB。 - 最大心跳间隔固定为 300 秒。设备可以根据功耗和网络情况调整实际周期,但不得超过 300 秒;最大值不是要求设备必须采用的周期。
休眠服务器不需要知道设备最终选择了哪个间隔,也不会把 300 秒最大间隔作为连接超时。服务器收到登录数据、心跳或其他数据包时,会更新该连接最近一次收到数据的时间;即使两个数据包之间的时间已经超过最大心跳间隔,也不会因此立即断开连接。只有连续超过 4 小时没有收到任何数据包时,服务器才会将连接下线。
因此,300 秒是设备侧的协议约束,不是服务端的离线判定时间。仍应按选定间隔持续发送心跳;网络异常导致心跳发送失败或 TCP 断开时,按后文规则重连,不要依赖服务端的 4 小时清理机制维持可用性。
3. 处理唤醒和重连
登录成功时服务器不会返回确认消息。登录失败时,服务器返回以下内容并关闭连接:
ERR|<CODE>|<message>处理唤醒包
连接成功进入保活后,服务器下发的唤醒包固定为 8 字节:
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 第 1~6 字节 | 98 3b 16 f8 f3 9c | 固定的唤醒包标识 |
| 第 7~8 字节 | 两字节自定义数据 | 未指定时为 00 00;业务服务端调用唤醒接口时可以通过 custom_data 指定 |
不携带自定义数据时,完整唤醒包为:
98 3b 16 f8 f3 9c 00 00例如业务服务端提交设备唤醒请求并传入 custom_data: "0x1234" 时,完整唤醒包为:
98 3b 16 f8 f3 9c 12 34两种唤醒包的协议和唤醒流程相同,区别仅是最后两字节。休眠模块都应先把它识别为唤醒包并触发幂等唤醒;业务需要区分唤醒原因或携带简短指令时,再读取最后两字节。不要把自定义数据作为唤醒包有效性的唯一判断条件,也不要假设默认的 00 00 代表某个固定业务原因。
根据休眠模块能力选择处理方式:
| 休眠模块能力 | 唤醒包处理方式 | 服务端唤醒请求要求 |
|---|---|---|
| 可以编程并解析收到的数据 | 校验前 6 字节固定标识,并按需解析最后两字节 | 可以省略 custom_data,也可以传入两字节自定义数据 |
| 没有编程解析能力,只能由硬件匹配固定报文 | 将完整默认包 98 3b 16 f8 f3 9c 00 00 配置为固定唤醒报文 | 必须省略 custom_data,确保平台下发默认唤醒包 |
固定报文匹配设备无法识别携带自定义数据的唤醒包。例如请求传入 0x1234 后,包尾会变为 12 34,与配置的默认包不再相同,设备可能不会触发唤醒。是否使用自定义数据应作为设备型号或产品能力进行管理,业务服务端提交请求前需要知道目标设备是否支持。
TCP 是字节流,一次 recv() 不保证正好返回 8 字节。接收实现需要累计数据,取得完整的 8 字节唤醒包后再读取最后两字节;同时应保留对 ERR|<CODE>|<message> 错误响应的处理。
收到唤醒数据后:
- 通过原子状态或互斥保护调用
wakeup_device_once(custom_data),只启动主控一次;业务需要时可以把解析出的自定义数据一并交给主控。 - 关闭当前设备已建立的所有休眠服务器连接。
- 取消尚未执行的重连任务。
- 启动主控和 TiRTC SDK,恢复正常在线流程。
连接断开、连接超时、心跳发送失败或登录失败时,应关闭当前 socket 并尝试重新连接:
- 为连接和接收操作设置有限超时,避免网络半开或只收到部分数据时永久阻塞。
- 一台服务器失败后优先重连当前服务器,但不要在同一地址上无间隔循环重试;达到设备设定的连续失败次数或重试时长后,再切换到本批次返回的其他服务器。
- 当前连接已经登录成功时,优先在断开后的 5 分钟内重连同一服务器,以使用该服务器提供的免有效期复检窗口;这个窗口不会延长
login_data本身的 7 天有效期。 - 多轮均失败时使用带上限的指数退避,并加入少量随机抖动,避免大量设备同时重连。超时、初始间隔和最大间隔应由设备根据网络类型和功耗目标配置。
- 只连接一个服务器时,可以依次切换到其他服务器;连接多个服务器时,只重建失败的连接。
- 任意连接收到有效唤醒包后,立即取消所有连接和重连任务,设备开始唤醒后不要再重连。
login_data 是二进制数据,必须按 login_data_len 发送,不能使用 strlen()。固定心跳数据必须发送 H、B 两个字节,不要把 C 字符串结尾的 \0 一并发送。日志中不要输出原始 login_data。
调用 HTTP API
完成 SDK 参数获取、休眠服务器登录和心跳保活后,业务服务端可以按需接入以下能力:
- 联调或排障时,调用查询设备连接信息,查看平台最近观察到的主控信令连接和休眠连接事件;查询结果不是设备最终业务状态。
- 需要由告警、门铃或其他业务事件主动启动主控时,调用提交设备唤醒请求。如果休眠模块只能匹配固定报文,请求中必须省略
custom_data。
服务地址、鉴权方法、完整参数和返回语义见 HTTP API。客户端呼叫触发的自动唤醒不需要业务服务端调用这些接口。
联调检查
按以下顺序确认接入结果:
- SDK 已通过
on_sleep_wakeup_info()返回有效参数; - 休眠模块已连接至少一个返回的服务器,并完成登录;
- 固定心跳数据
HB能按 60 秒建议间隔持续发送,实际间隔不超过 300 秒; - 客户端连接设备时,主控能够自动唤醒;
- 业务服务端提交唤醒请求时能收到
202 Accepted; - 如果同时连接多个服务器,同一次唤醒只会启动主控一次;
- 连接中断后,休眠模块能够改连其他服务器或重建失败连接;
- 服务器不可达时不会高频无限重试,退避结束后能够继续恢复连接;
- TCP 只返回部分唤醒包或连接进入半开状态时,接收超时能够结束阻塞并进入重连;
- 网络切换后可以重新建立休眠连接,旧连接不会继续发送心跳;
- 唤醒开始后,即使收到重复唤醒包或有待执行的重连任务,也不会重复启动主控;
- 唤醒后,所有已建立的休眠连接都会关闭。
排查问题时记录 device_id、请求时间、X-Request-ID、服务器地址、连接序号、当前连接阶段、连接和断开时间、socket 错误码、最近心跳时间、连续重连次数以及唤醒包最后两字节。登录失败时记录服务端返回的错误码和消息,但不要记录原始登录数据。提交日志前删除或打码应用级密钥、device_secret_key、连接 Token 和原始 login_data。