Skip to content

接入低功耗休眠唤醒

设备主控进入低功耗状态后,可以由休眠模块与 TiRTC 休眠唤醒服务器保持 TCP 连接。客户端呼叫设备或业务服务端提交唤醒请求时,休眠模块会收到唤醒数据,然后启动主控和 TiRTC SDK。

这种方式适合可视门铃、摄像机等需要长时间休眠、但仍需响应呼叫或业务指令的设备。完成接入后,你可以让设备在工作状态和休眠状态之间切换,并通过客户端呼叫或业务服务端的设备唤醒接口将设备唤醒。

接入前先确认设备架构是否适合:

设备情况是否使用本文方案
主控休眠后,独立休眠模块仍能联网并维持 TCP适合
主控和联网模块会同时断电或断网不适合,休眠期间无法接收远程唤醒
主控长期运行,TiRTC SDK 始终在线通常不需要额外的休眠连接
休眠模块只能匹配固定唤醒报文,没有编程解析能力可以接入,但只能使用默认唤醒包,不能使用自定义数据
休眠期间还需要处理复杂业务需要先确认休眠模块的计算、存储和网络能力;本文只解决保活和唤醒

工作流程

下图只展示接入方需要交互的公开边界。TiRTC 平台内部服务和路由过程不属于设备接入契约。

TiRTC 低功耗休眠唤醒时序

一次完整的休眠和唤醒流程如下:

  1. 主控启动 TiRTC SDK,并启用休眠唤醒能力。
  2. SDK 通过回调返回休眠服务器、登录数据和心跳数据。
  3. 设备准备休眠时,把最新参数交给休眠模块。
  4. 休眠模块选择一个或多个休眠服务器建立 TCP 连接,发送登录数据并定期发送心跳。
  5. 客户端连接设备时,TiRTC 自动提交唤醒;需要由业务事件主动唤醒设备时,业务服务端可以调用提交设备唤醒请求
  6. 休眠模块收到唤醒数据后关闭休眠连接,启动主控和 TiRTC SDK。

主控和休眠模块的职责如下:

阶段主控和 TiRTC SDK休眠模块
启动启用能力并取得最新休眠参数等待主控传递参数
准备休眠复制并传递完整参数校验参数,建立 TCP 并发送登录数据
休眠期间停止运行或进入低功耗状态发送心跳、接收唤醒包并处理重连
收到唤醒尚未运行幂等触发主控启动,关闭全部休眠连接
恢复在线重新启动 TiRTC SDK 并恢复业务停止本轮休眠连接和重连任务

开始前准备

开始接入前,确保已经:

  • 申请开通 TiRTC,并取得设备身份和应用级凭证;
  • 完成 C SDK 接入
  • 让设备在主控休眠期间仍有一个模块能够维持 TCP 连接。

如果设备休眠期间无法联网,或者主控可以一直运行 TiRTC SDK,则不需要单独建立休眠连接。

接入 SDK

1. 启用休眠唤醒

TiRtcStart() 之前设置 TIRTC_OPT_WAKEUP

c
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

c
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 指针只在当前回调期间有效,服务器列表和登录数据都是结构体内嵌数组。需要在主控关闭后继续使用时,应在回调返回前复制整个结构体或所需字段:

参数说明
serversserver_count休眠服务器地址,返回 1 到 3 个
login_datalogin_data_lenTCP 建连后立即发送的登录数据,长度为 1 到 64 字节,有效期为 7 天

休眠连接使用固定心跳协议:休眠模块发送 ASCII 数据 HB,长度为 2 字节,不包含 C 字符串结尾的 \0;建议心跳间隔为 60 秒,最大允许间隔为 300 秒。

不要解析或自行生成 login_data。SDK 再次回调新参数时,完整覆盖之前保存的服务器和登录数据,不能混用不同批次的数据。

保存参数前至少检查 info 非空、server_count1..3 内、login_data_len1..64 内,以及选用的服务器地址和端口有效。校验或保存失败时,不要让主控进入休眠。

当前接入流程只保证使用本次 SDK 启动后回调的最新参数。除非 TiRTC 明确确认参数可以跨启动复用,否则不要在设备重启后直接使用上一次持久化的登录数据;本次没有收到有效回调时,应保持主控运行并记录失败原因。

3. 启动 SDK 并等待参数

c
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. 进入休眠前检查

按以下顺序切换到休眠状态:

  1. 确认当前没有通话、推流或其他不能中断的设备任务。
  2. 确认已取得、校验并复制本次启动返回的服务器和登录数据。
  3. 将同一批次的服务器和登录数据交给休眠模块,并配置固定心跳数据 HB、60 秒建议间隔和 300 秒最大间隔。
  4. 等待休眠模块至少建立一条 TCP 连接,并完整发送 login_data。服务器不会返回登录成功确认,因此不能等待成功回包。
  5. 确认休眠模块已经启动心跳、接收和重连任务。
  6. 停止不再需要的主控业务,再让主控进入休眠。

任一步失败时保持主控在线,不要进入一个既没有 TiRTC SDK、也没有有效休眠连接的状态。

接入休眠唤醒服务器

1. 选择服务器

SDK 最多返回 3 个休眠服务器。设备必须至少选择一个可用服务器连接,但不要求连接全部服务器:

  • 只连接一个服务器时,实现最简单;该连接异常后,需要尝试其他返回的服务器。
  • 连接多个服务器时,可以同时维持多条休眠连接;同一次唤醒可能通过每条连接各下发一次,设备必须自行保证唤醒操作幂等。

每条连接独立完成登录、心跳、读取和重连。不要把一条连接的登录数据发送结果当作其他连接的结果。

2. 登录并发送心跳

对选中的每台服务器执行:

  1. 使用 hostport 建立 TCP 连接。
  2. 建连成功后立即完整发送 login_data
  3. 不等待登录成功回包,按选定间隔发送固定心跳数据 HB
  4. 持续读取服务器数据,等待唤醒指令或错误响应。

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) 由设备根据自身网络栈和任务模型实现。只能匹配固定报文的模块不需要实现自定义数据解析,应按后文说明配置完整的默认唤醒包。

c
#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. 处理唤醒和重连

登录成功时服务器不会返回确认消息。登录失败时,服务器返回以下内容并关闭连接:

text
ERR|<CODE>|<message>

处理唤醒包

连接成功进入保活后,服务器下发的唤醒包固定为 8 字节:

字节位置内容说明
第 1~6 字节98 3b 16 f8 f3 9c固定的唤醒包标识
第 7~8 字节两字节自定义数据未指定时为 00 00;业务服务端调用唤醒接口时可以通过 custom_data 指定

不携带自定义数据时,完整唤醒包为:

text
98 3b 16 f8 f3 9c 00 00

例如业务服务端提交设备唤醒请求并传入 custom_data: "0x1234" 时,完整唤醒包为:

text
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> 错误响应的处理。

收到唤醒数据后:

  1. 通过原子状态或互斥保护调用 wakeup_device_once(custom_data),只启动主控一次;业务需要时可以把解析出的自定义数据一并交给主控。
  2. 关闭当前设备已建立的所有休眠服务器连接。
  3. 取消尚未执行的重连任务。
  4. 启动主控和 TiRTC SDK,恢复正常在线流程。

连接断开、连接超时、心跳发送失败或登录失败时,应关闭当前 socket 并尝试重新连接:

  • 为连接和接收操作设置有限超时,避免网络半开或只收到部分数据时永久阻塞。
  • 一台服务器失败后优先重连当前服务器,但不要在同一地址上无间隔循环重试;达到设备设定的连续失败次数或重试时长后,再切换到本批次返回的其他服务器。
  • 当前连接已经登录成功时,优先在断开后的 5 分钟内重连同一服务器,以使用该服务器提供的免有效期复检窗口;这个窗口不会延长 login_data 本身的 7 天有效期。
  • 多轮均失败时使用带上限的指数退避,并加入少量随机抖动,避免大量设备同时重连。超时、初始间隔和最大间隔应由设备根据网络类型和功耗目标配置。
  • 只连接一个服务器时,可以依次切换到其他服务器;连接多个服务器时,只重建失败的连接。
  • 任意连接收到有效唤醒包后,立即取消所有连接和重连任务,设备开始唤醒后不要再重连。

login_data 是二进制数据,必须按 login_data_len 发送,不能使用 strlen()。固定心跳数据必须发送 HB 两个字节,不要把 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

TiRTC 开发文档