Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,12 @@ test:memory-leak:
# tai_log_module_zero_tests, tai_integration_tests
# iot-client : iot_cipher_test, iot_ai_ctrl_test
# tuya-ble : tuya_ble_test
# biz-client : biz_ai_role_test (public ATOP boundary faked)
# rtc-client is a prebuilt closed lib (headers + libstm.a) with no offline
# test, so there is nothing to leak-check for it here. The mock-driven iot
# tests are covered by the `test` job; under valgrind their Python
# handshakes would time out.
for t in tai_unit_tests tai_log_level_zero_tests tai_log_module_zero_tests tai_integration_tests iot_cipher_test iot_ai_ctrl_test tuya_ble_test; do
for t in biz_ai_role_test tai_unit_tests tai_log_level_zero_tests tai_log_module_zero_tests tai_integration_tests iot_cipher_test iot_ai_ctrl_test tuya_ble_test; do
echo "==================== valgrind: ${t} ===================="
"$VG" "./${CMAKE_BUILD_DIR}/${t}" 2>&1 | tee "valgrind-${t}.log"
rc=${PIPESTATUS[0]}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- biz-client — typed AI role management, language/timbre queries, POSIX and ESP-IDF examples, and bilingual API documentation (#47).
- iot-client — `iot_get_binding_status()` queries `tuya.device.info.sync` with saved credentials, without an IoT client (#44).
- `iot_client_get_binding_status()` reuses an existing client's endpoint and TLS settings.
- Examples — `unbind_demo` checks cloud binding status before client initialization/reconnect and after a removal notice (#44).
Expand Down
34 changes: 34 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,25 @@ target_link_libraries(tuya_iot_client PUBLIC
Threads::Threads
)

# -----------------------------------------------------------------------------
# biz-client module (typed device-side cloud services)
# -----------------------------------------------------------------------------
set(BIZ_CLIENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/biz-client")
set(BIZ_CLIENT_SOURCES
"${BIZ_CLIENT_DIR}/src/biz_client.c"
"${BIZ_CLIENT_DIR}/src/ai_role.c"
)
add_library(tuya_biz_client STATIC ${BIZ_CLIENT_SOURCES})
target_include_directories(tuya_biz_client
PUBLIC "${BIZ_CLIENT_DIR}/include"
PRIVATE "${cjson_source_dir}"
)
target_link_libraries(tuya_biz_client PUBLIC tuya_iot_client
PRIVATE ${AGENTIC_KIT_CJSON_TARGET} agentic_kit_common)
if(UNIX)
target_link_libraries(tuya_biz_client PRIVATE m)
endif()

# -----------------------------------------------------------------------------
# rtc-client module (prebuilt binary)
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -270,6 +289,21 @@ target_link_libraries(tuya_ble PUBLIC
# Tests
# -----------------------------------------------------------------------------
if(AGENTIC_KIT_BUILD_TESTS AND AGENTIC_KIT_ENABLE_PROJECT_TESTS)
# Fake only the public ATOP boundary, so service parsing/ownership can be
# checked without network credentials or a subprocess. Register in ctest.
add_executable(biz_ai_role_test ${BIZ_CLIENT_SOURCES}
"${BIZ_CLIENT_DIR}/test/ai_role_test.c")
target_include_directories(biz_ai_role_test PRIVATE
"${BIZ_CLIENT_DIR}/include" "${IOT_CLIENT_DIR}/include"
"${PAL_DIR}" "${COMMON_DIR}" "${cjson_source_dir}")
target_compile_options(biz_ai_role_test PRIVATE -Wall -Wextra -Werror)
target_link_libraries(biz_ai_role_test PRIVATE
${AGENTIC_KIT_CJSON_TARGET} agentic_kit_common)
if(UNIX)
target_link_libraries(biz_ai_role_test PRIVATE m)
endif()
add_test(NAME biz_ai_role_test COMMAND biz_ai_role_test)

set(AI_TCP_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/rtc-tcp-client/test")
set(IOT_CLIENT_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/iot-client/test")
set(TUYA_BLE_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/tuya-ble/test")
Expand Down
4 changes: 4 additions & 0 deletions CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ and decisions.

## Contexts

- [Business Client](./modules/biz-client/CONTEXT.md) — typed device-side cloud
business services, initially AI role management and language/timbre queries.
- [IoT Client](./modules/iot-client/CONTEXT.md) — device ↔ Tuya cloud: activation, the
Data Point model, MQTT reporting/downlink. ADRs: `modules/iot-client/docs/adr/`.
- [RTC TCP Client](./modules/rtc-tcp-client/CONTEXT.md) — device ↔ Tuya AI Foundation
Expand All @@ -16,6 +18,8 @@ and decisions.

## Relationships

- **Business Client → IoT Client**: uses only `iot_atop_call()` and its public
response-release contract. It does not depend on RTC or own application tasks.
- **IoT Client → RTC TCP Client**: the IoT Client obtains an AI session token
(`iot_client_get_session_token`) that the RTC client uses to open a Session.
Applications may also keep MQTT active as an independent control path:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
| RTC TCP Client | `tuya_ai.h` | tRTC(tuya自研RTC协议) TCP 实现,开源实现,PAL 可移植 |
| RTC Client | `stm_open.h` | tRTC(tuya自研RTC协议) UDP 实现,预编译静态库形式 |
| IoT Client | `iot_client.h`、`iot_dp.h`、`iot_ota.h` | 设备激活、MQTT 连接、会话令牌获取;数据点(DP)管理(schema 校验 / 缓存 / 上下行 / 持久化);OTA 固件升级(云端协议) |
| Business Client | `tuya_biz_client.h`、`tuya_ai_role.h` | 设备侧云端业务 API:AI 角色查询、切换、编辑、删除,以及语言和音色查询;复用 IoT ATOP,PAL 可移植 |
| Tuya BLE | `tuya_ble_prov.h` | BLE 蓝牙配网(ESP-IDF) |

## 环境要求
Expand Down
1 change: 1 addition & 0 deletions README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Multimodal device-side SDK for connecting smart hardware to the Tuya AI platform
| RTC Client | `stm_open.h` | tRTC (Tuya RTC protocol), UDP implementation, pre-compiled static library |
| IoT Client | `iot_client.h`, `iot_dp.h`, `iot_ota.h` | Device activation, MQTT, session token; Data Point (DP) management (schema validation / cache / up- & downlink / persistence); OTA firmware upgrade (cloud protocol) |
| Tuya BLE | `tuya_ble_prov.h` | BLE provisioning (ESP-IDF) |
| Business Client | `tuya_biz_client.h`, `tuya_ai_role.h` | Typed cloud business APIs: AI role queries, selection, editing and deletion, plus language/timbre queries; uses public IoT ATOP and PAL |

## Prerequisites

Expand Down
128 changes: 128 additions & 0 deletions docs-site/docs/guides/ai-role-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: 设备端 AI 角色管理
sidebar_label: AI 角色管理
---

# 设备端 AI 角色管理

`biz-client` 为设备提供有类型的云端业务 API。第一组能力是 AI 角色管理:查询当前角色和角色列表、切换角色、编辑名称/语言/音色/语速、删除自定义角色。带屏设备可以绑定自己的列表和编辑页面,无屏设备也可以通过按键或串口调用。

本模块依赖 `iot-client` 的公开 `iot_atop_call()`。签名、加密和 HTTPS 由 IoT Client 处理,业务调用不需要再次提供设备密钥,也不依赖 RTC TCP Client。

## 接入前提 {#prerequisites}

- 已调用 `iot_init()` 或 `iot_init_default()`,并持有已激活的 `iot_client_t`;MQTT 连接不是角色 ATOP 请求的前提。
- 设备系统时间已校准,客户端配置了正确区域和 CA 证书或平台证书包。模块自动在请求体中加入当前 POSIX 秒数 `t`;时间明显未初始化时返回 `OPRT_UNINITIALIZED`,不发送请求。
- 产品在云端有可用角色。可访问范围、语言和音色以实际设备返回为准。
- ATOP 响应缓冲足够容纳角色列表。默认 `AGENTIC_KIT_RESPONSE_BUFFER_SIZE=4096` 同时容纳 HTTP 头和加密正文,解密后的 JSON 余量更小。模板列表不能分页时,客户应根据真实响应和设备内存增加此编译配置,例如 32768;它必须作用于编译 `iot-client` 的目标,不能只定义在应用源文件中。

## 创建业务客户端 {#client}

```c
#include "tuya_biz_client.h"
#include "tuya_ai_role.h"

tuya_biz_client_t *biz = NULL;
int rc = tuya_biz_client_create(iot, "zh", &biz);
```

`iot` 是应用已有的客户端。第二个参数 `request_lang` 决定角色名称等展示字段的语言,`NULL` 默认为 `zh`;它不同于角色回复语言。业务客户端借用 IoT Client 和其 PAL,退出时先释放列表、销毁业务客户端,再销毁 IoT Client。

POSIX 工程链接 `tuya_biz_client`;仓库自带的 ESP-IDF `agentic_kit` 组件已包含该模块源文件和公开头文件。模块不需要 PSRAM、NVS、LVGL 或特定芯片。

## 查询与切换 {#query-and-bind}

以下示例必须在应用工作任务中执行:

```c
tuya_ai_role_list_t templates = {0};
iot_atop_rejection_t rejection = {0};
int rc = tuya_ai_role_list_templates(biz, &templates, &rejection);
if (rc == OPRT_OK && templates.count > 0) {
rc = tuya_ai_role_bind(biz, templates.items[0].type,
templates.items[0].role_id, &rejection);
}
tuya_ai_role_list_free(biz, &templates);

/* 绑定成功后回读,不以 UI 提示代替云端确认。 */
tuya_ai_role_t bound = {0};
if (rc == OPRT_OK) {
rc = tuya_ai_role_get_bound(biz, &bound, &rejection);
}
```

使用列表返回的 `role_id` 和类型,不根据名称猜 ID。`template_id` 用于关联模板来源,不能代替接口要求的角色 ID。

自定义角色和音色支持显式分页:

```c
tuya_ai_role_list_t customs = {0};
rc = tuya_ai_role_list_custom(biz, 1, 5, &customs, &rejection);
/* 消费 customs.items,然后释放。 */
tuya_ai_role_list_free(biz, &customs);

tuya_ai_timbre_list_t voices = {0};
rc = tuya_ai_role_list_timbres(biz, "zh_ha", 1, 5, &voices, &rejection);
tuya_ai_timbre_list_free(biz, &voices);
```

`page_no`、`page_size` 必须为正数。`total_count=-1` 表示云端没有提供总数;空数组是正常结果。模块不会自动加载所有页。选择语言后,用云端返回的完整编码重新查询音色,例如保留 `zh_ha`,不要截断成 `zh`。

## 修改与删除 {#edit-and-delete}

```c
/* selected 是已经获取的角色快照,且在调用期间有效。 */
tuya_ai_role_edit_t edit = {
.name = "学习伙伴",
.set_speed = true,
.speed = 1.20,
};
char effective_id[TUYA_AI_ROLE_ID_LEN] = {0};
rc = tuya_ai_role_apply_edit(biz, &selected, &edit,
effective_id, &rejection);
```

| 目标 | 保存行为 |
| --- | --- |
| `TUYA_AI_ROLE_TEMPLATE=1` | 基于模板创建自定义角色,并发送 `needBind=true` 请求绑定;成功后返回新 ID |
| `TUYA_AI_ROLE_CUSTOM=0` | 更新原自定义角色,返回原 ID,不自动切换绑定 |
| `TUYA_AI_ROLE_SINGLE=2` | 可识别为查询结果;不接受本模块的多角色编辑和绑定操作 |

字符串指针为 `NULL`、`set_speed=false` 表示不修改该字段。只提交真正修改的字段;空编辑、空字符串、过长值和非正/非有限语速会被拒绝。`speed` 在云端请求中是字符串,由模块转换;云端决定支持的语速范围。

当前可编辑字段仅有 `name`、`lang_code`、`timbre_id`、`speed`,不包含系统提示词、模型或知识库设置。

调用 `tuya_ai_role_delete_custom(biz, role_id, &rejection)` 删除自定义角色前,应用应确认该 ID 来自自定义列表。不要提交模板 ID;删除当前角色前先绑定其他可用角色并回读确认。模块不提供删除后的自动回退策略。

## 调度与内存 {#scheduling-and-memory}

SDK 不自动创建线程、不重试、不缓存。每次角色操作都同步阻塞到 HTTPS 请求返回。应用在自己的工作任务中串行调用,并协调 IoT 客户端销毁;不要从 UI、MQTT 或 TAI 接收回调直接调用网络接口。

异步消息必须复制参数内容。`tuya_ai_role_edit_t` 内是借用指针,不能直接把指向 UI 临时缓冲的结构体排队。ESP-IDF 示例提供按值复制的 `role_demo_job_t`。

| 分配结果 | 释放接口 |
| --- | --- |
| 模板/自定义角色列表 | `tuya_ai_role_list_free(biz, &list)` |
| 语言列表 | `tuya_ai_language_list_free(biz, &list)` |
| 音色列表 | `tuya_ai_timbre_list_free(biz, &list)` |
| 业务客户端 | `tuya_biz_client_destroy(biz)` |

查询前将输出清零;再次使用输出变量前先释放旧列表。释放时必须使用分配它的同一个、仍有效的业务客户端,列表指针不能在释放后交给 UI。所有容量单位为字节,包含结尾 `NUL`;云端返回字符串过长时返回 `OPRT_INVALID_RESULT`,不会截断 ID 或语言编码。

角色缓存、头像 HTTP 下载/解码和 UI 由应用实现。缓存只能用于展示,联网后应回读云端;设备解绑/换用户时应清理业务缓存。

## 错误与对话生效 {#errors-and-effect}

所有接口返回 `OPRT_*`。可选 `iot_atop_rejection_t` 在调用入口清零,`OPRT_ATOP_BUSINESS_ERROR` 时携带云端 `code`、`msg`;传输错误保持原返回码。成功的云端信封也必须具有符合接口形状的结果,缺失/null/false 或格式错误返回 `OPRT_INVALID_RESULT`。

不自动重试模板创建:超时可能意味着云端已创建并绑定,应先查询当前绑定和自定义列表,避免重复创建。写入已成功但回读失败时,应分别展示“变更已接受”和“状态待刷新”。

云端绑定成功不等于已在当前 AI Session 生效。空闲时切换后,先核对绑定,再发起新一轮真实语音验证角色、语言及音色;同时验证重建 Session 的情况。是否需要新 Session 由客户云端配置和实际对话结果决定,由应用会话管理任务协调,`biz-client` 不会自行断开 RTC。

## 示例与验收 {#examples-and-acceptance}

- POSIX:`examples/posix/ai/role-management`,提供查询、绑定、编辑、删除的命令行入口,真实变更只在显式命令下执行。
- ESP-IDF:`examples/esp-idf/ai/role-management`,可接入已有设备工程的组件,包含工作队列和退出流程;不是独立配网固件。
- API、ATOP 对照及完整签名见 [Business Client 参考](../reference/biz-client.md)。

至少验收模板/自定义查询、两种角色切换、模板修改产生新 ID、自定义修改保留 ID、未修改字段保留、语言和音色匹配、自定义删除、网络失败不误报成功、退出后无悬空客户端,以及新一轮实际语音效果。
69 changes: 69 additions & 0 deletions docs-site/docs/reference/biz-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Business Client API 参考
sidebar_label: Business Client
---

# Business Client API 参考

模块目录 `modules/biz-client`,POSIX 构建目标 `tuya_biz_client`,公开头文件 `tuya_biz_client.h`、`tuya_ai_role.h`。接入步骤见 [AI 角色管理](../guides/ai-role-management.md)。

## 客户端和生命周期 {#lifecycle}

```c
int tuya_biz_client_create(iot_client_t *iot, const char *request_lang,
tuya_biz_client_t **out);
void tuya_biz_client_destroy(tuya_biz_client_t *client);
```

借用 IoT Client 及其 PAL。`request_lang=NULL` 默认 `zh`,非空值最多 31 字节。创建失败时 `*out=NULL`。先 `iot_init`,再创建;所有结果必须在销毁客户端前释放,所有调用必须在销毁 IoT Client 前完成。多实例可独立持有客户端和展示语言;调用方负责串行调度。

## 角色 API {#role-api}

下表省略共同首参 `tuya_biz_client_t *` 和末尾可选参数 `iot_atop_rejection_t *`,完整声明以公开头文件为准。

| API | 中间参数 | ATOP v1.0 |
| --- | --- | --- |
| `tuya_ai_role_get_bound` | `tuya_ai_role_t *out` | `thing.ai.agent.role.get-bind-role` |
| `tuya_ai_role_list_templates` | `tuya_ai_role_list_t *out` | `thing.ai.agent.role.role-template.list` |
| `tuya_ai_role_list_custom` | `int page_no, int page_size, tuya_ai_role_list_t *out` | `thing.ai.agent.role.custom-role.page` |
| `tuya_ai_role_bind` | `tuya_ai_role_type_t type, const char *role_id` | `thing.ai.agent.role.bind-with-role` |
| `tuya_ai_role_apply_edit` | `const tuya_ai_role_t *cur, const tuya_ai_role_edit_t *edit, char out_id[TUYA_AI_ROLE_ID_LEN]` | `thing.ai.agent.role.custom-role.create-from-template` 或 `thing.ai.agent.role.custom-role.update` |
| `tuya_ai_role_delete_custom` | `const char *role_id` | `thing.ai.agent.role.custom-role.delete` |
| `tuya_ai_role_list_languages` | `tuya_ai_language_list_t *out` | `thing.ai.agent.config.languages.list` |
| `tuya_ai_role_list_timbres` | `const char *lang, int page_no, int page_size, tuya_ai_timbre_list_t *out` | `thing.ai.timbre.market.page` |

每个请求加入 `t`;列表查询还携带展示语言 `requestLang`。音色查询通过 `lang` 筛选,`NULL` 表示不过滤。分页接口不会自动继续请求下一页。

## 数据类型 {#types}

`tuya_ai_role_t` 包含 `role_id`、`template_id`、`type`、`name`、`description`、`image_url`、`lang_code`、`lang_name`、`timbre_id`、`timbre_name` 和 `speed`。列表接口按来源设置类型,当前绑定查询读取云端类型;缺失类型按 `SINGLE` 处理。

`tuya_ai_role_edit_t` 包含可选 `name`、`lang_code`、`timbre_id` 指针,以及 `set_speed` 和 `speed`。模板分支创建并请求绑定,自定义分支原地更新。`out_id` 至少有 `TUYA_AI_ROLE_ID_LEN` 字节,失败时首字节清零。无修改时应跳过调用。

角色和音色列表包含 `items`、`count`、`total_count`,总数未知为 -1。语言列表包含 `items`、`count`。音色的 `support_langs` 是完整语种编码拼接的逗号串,仅用于展示;筛选仍由云端执行。

响应列表接受裸数组,或 `datas`、`list`、`records`、`rows`、`content`、`data`、`items` 中的数组。未知形状不猜测。角色 ID、语言编码、音色 ID 必须存在且非空;过长字段使整个结果失败,不返回半份列表。

绑定、删除和更新的确认结果保持与现有 TuyaOpen 角色实现兼容:ATOP 信封成功后,将非 null、非 false 的 JSON 结果视为不透明确认,不假定它必须是 Boolean。模板创建则必须返回非空的新 ID 字符串;是否正确绑定仍需回读确认。

## 释放与错误 {#release-and-errors}

```c
void tuya_ai_role_list_free(tuya_biz_client_t *, tuya_ai_role_list_t *);
void tuya_ai_language_list_free(tuya_biz_client_t *, tuya_ai_language_list_t *);
void tuya_ai_timbre_list_free(tuya_biz_client_t *, tuya_ai_timbre_list_t *);
```

使用原分配上下文释放,释放后列表清零。查询失败时输出清空。客户端销毁不是列表批量释放器,调用方先释放每份结果。

| 返回码 | 含义 |
| --- | --- |
| `OPRT_OK` | 接口结果有效;对话效果仍需实际验证 |
| `OPRT_INVALID_PARAMETER` | 空/非法参数、无改动、非法类型或分页参数 |
| `OPRT_UNINITIALIZED` | 设备未激活或系统时间明显未初始化 |
| `OPRT_MALLOC_FAILED` | PAL/cJSON 申请内存失败 |
| `OPRT_INVALID_RESULT` | 云端结果缺失、形状错误、必要 ID 缺失或字段过长 |
| `OPRT_ATOP_BUSINESS_ERROR` | 云端拒绝,读取可选 `rejection.code/msg` |
| 其他 `OPRT_*` | ATOP 传输错误原样返回 |

删除接口不会先验证角色来源;应用只能提交已确认的自定义 ID。模块不下载头像、不保存 NVS、不调用 RTC,不自动重试任何请求。
Loading
Loading