调用未封装的云端接口

设备与涂鸦云之间的 HTTP 接口统称 ATOP 接口,由 api 名和 version 两个字段唯一确定,例如 tuya.device.upgrade.get v4.4。

SDK 为其中一部分接口提供了具名接口iot_ota_*iot_dp_* 等),返回类型化的结构体。云端可调的 ATOP 接口远多于此,为了不让业务被 SDK 的排期卡住,SDK 提供一个通用入口 iot_atop_call():你给出 apiversion 和 JSON 请求体,拿回 result 字段的 JSON 字符串。

签名、请求体 AES-GCM 加密、TLS、host 解析、信封解析都在 SDK 内部完成,设备密钥不会离开 SDK

先确认有没有具名接口

已经封装好的接口不要用通用入口重新实现——它们额外处理了云端返回的各种变体,并且有单测覆盖:

ATOP apiversion具名接口
tuya.device.upgrade.get4.4iot_ota_check_upgrade()
tuya.device.versions.update4.1iot_ota_report_version()
tuya.device.upgrade.status.update4.1iot_ota_report_status()
tuya.device.schema.newest.get1.0DP 层内部自动查询
thing.ai.agent.token.get1.0iot_client_get_session_token()
tuya.device.qrcode.info.get1.1iot_get_qrcode_info()
thing.device.opensdk.active2.0iot_client_init_on_boarding()
tuya.device.meta.save1.0激活流程内部调用

什么时候该要一个具名接口

通用入口是逃生通道,不是首选。满足下面任意一条时,这个接口值得进 SDK 变成具名接口,欢迎提 issue:

  • 多产品复用 —— 两个以上产品线都要调。否则等于把单个产品的业务逻辑塞进 SDK。
  • 协议语义不平凡 —— 有状态机、多步时序,或者云端返回形态需要宽容解析。这类知识重复实现必然出偏差。
  • 需要参与 SDK 内部状态 —— 要改 iot_client_t 的字段或触发 SDK 回调。业务层拿不到内部状态,物理上只能在 SDK 里做。

三条都不满足的接口,留在业务层用通用入口调是合适的终态,不是技术债。

API

#include "iot_atop.h"

typedef struct {
const char *api; /* 例 "tuya.device.upgrade.get",必填 */
const char *version; /* 例 "4.4",必填 */
const char *data; /* 请求体,JSON 对象字符串;NULL 或 "" 视为 "{}" */
} iot_atop_request_t;

typedef struct {
char *result; /* result 字段的 JSON 字符串;云端没返回则为 NULL */
char error_code[48]; /* 云端 errorCode;成功时为 "" */
char error_msg[128]; /* 云端 errorMsg;成功时为 "" */
int32_t server_time; /* 信封里的服务器时间 t */
} iot_atop_response_t;

int iot_atop_call(iot_client_t *client,
const iot_atop_request_t *request,
iot_atop_response_t *response);
void iot_atop_response_free(iot_client_t *client, iot_atop_response_t *response);

示例

#include "iot_atop.h"

char body[192];
snprintf(body, sizeof(body),
"{\"schemaId\":\"%s\",\"version\":\"\",\"t\":%u}",
schema_id, (unsigned)time(NULL));

iot_atop_request_t req = { .api = "tuya.device.schema.newest.get",
.version = "1.0",
.data = body };
iot_atop_response_t resp = {0};

int rc = iot_atop_call(client, &req, &resp);
if (rc == OPRT_OK) {
if (resp.result != NULL) {
my_parse(resp.result); /* result 是 JSON 文本,用什么库解析都行 */
}
} else if (rc == OPRT_ATOP_BUSINESS_ERROR) {
/* 请求到达了云端,被云端拒绝 —— error_code 说明原因 */
log_error("rejected: %s (%s)", resp.error_code, resp.error_msg);
} else {
/* 传输层失败:DNS / TLS / HTTP / 解密 */
log_error("call failed: %d", rc);
}

iot_atop_response_free(client, &resp); /* 每条路径都要调,包括失败路径 */

返回值:区分"云端拒绝"和"没连上"

这是使用通用入口时最重要的一点。你调的接口 SDK 并不认识,所以 SDK 无法替你判断业务是否成功,云端自己的 errorCode 是唯一可靠的线索

返回值含义怎么处理
OPRT_OK云端接受了这次调用result 是结果 JSON,或 NULL(云端没返回 result,这是合法的)
OPRT_ATOP_BUSINESS_ERROR到达了云端,被云端拒绝error_code / error_msg,按你的接口文档处理
OPRT_INVALID_PARAMETER参数不对,或 data 不是 JSON 对象本地就拦下了,没发网络请求
OPRT_UNINITIALIZED设备还没有激活凭据先完成激活
其他传输层失败(DNS / TLS / HTTP / 解密)可重试

error_code[0] == '\0' 等价于"业务成功"。

限制

只支持已激活的设备。 通用入口用 devid + secret_key 签名。激活本身用的是 uuid + authkey,是另一条路径,仍然只能通过 iot_client_init_on_boarding() 走。在没有凭据的 client 上调用返回 OPRT_UNINITIALIZED

单次响应不能超过 4096 字节,连 HTTP 头一起算。 状态行、响应头和加密后的响应体共用一个固定大小的缓冲区(RESPONSE_BUFFER_SIZEhttp_client_interface.c),没有分段续读。溢出时底层 coreHTTP 返回 HTTPInsufficientMemory,SDK 把它折叠成 OPRT_COMMUNICATION_ERROR——和"socket 断了"是同一个返回值,而 coreHTTP 自己的解释性日志在本项目里被编译掉了(HTTP_DO_NOT_USE_CUSTOM_CONFIG),所以现场只看得到一次"传输层失败",重试也不会好。扣掉响应头、base64 膨胀和信封字段,解密后 JSON 的实际上限约 2.8 KB。返回定长字段的接口绰绰有余;返回列表、DP schema 这类长度随产品增长的接口很容易撞上——遇到这种接口请提 issue,把它变成具名接口的同一个改动里需要把这个常量一起抬上去。

请求体原样透传。 SDK 不会改写你的请求体,所以接口要求的字段必须自己带齐——包括大多数 ATOP 接口在请求体里要求的 t 时间戳字段。SDK 只校验到"能解析成 JSON 对象"为止,目的是把手误变成一个立刻返回的 OPRT_INVALID_PARAMETER,而不是花一次 HTTPS 往返换一句含义模糊的云端拒绝。

result 是字符串,不是 cJSON 对象。 这样 cJSON 不会进入 SDK 的公共 ABI,业务层不必绑定 SDK 的 cJSON 版本,内存归属也清晰:iot_atop_response_free() 负责释放。

设备时钟要基本准确。 签名带时间戳,设备时间偏差过大云端会拒签。resp.server_time 是云端返回的时间(秒级 Unix 时间戳),可以用来校正本地时钟。

© 2026 Tuya Inc.Tuya Physical AI · Agentic-kit