OneSay文档

文档

ESP32快速入门

预览版:功能和API可能会变化。

PDF由浏览器的打印对话框生成,不是托管的导出服务。

只使用任务的应用选择语音任务;要在房间里传输实时音频,选择实时语音(RTC)。不需要OneSay OS。SDK目前处于预览阶段,尚未发布;请把它作为ESP-IDF组件加入你的项目。即使客户端二进制文件或本地能力标志发生变化,所有权限仍以服务端为准。

ESP32 + 语音任务#

从sdks/esp32/examples/voice_button开始,这是一个只使用任务的独立ESP-IDF应用。把它复制到新的项目目录,并把OneSay ESP32组件(sdks/esp32的内容)加入为components/onesay;示例要求组件位于这个位置,缺少组件就无法单独构建。请保留idf_component.yml中组件的依赖版本锁定;首次构建时,ESP-IDF组件管理器会下载缺少的依赖。

在这个项目中使用ESP-IDF 6.0.2构建,然后烧录开发板并查看串口输出:

Text
idf.py set-target esp32s3
idf.py build
idf.py -p <PORT> flash monitor

构建不需要开发板。烧录只写入应用,不会写入设备凭据。

按照sdks/esp32/examples/voice_button/README.md和示例自带的main/voice_button.h操作。为onesay_voice_button_configure提供安全的设备凭据存储,现有已授权的项目、环境、应用以及音频源和房间绑定,网络访问,可信的UTC时间和证书,以及有上限的16 kHz单声道PCM s16le音频生产者。它的默认钩子会在连接前返回ONESAY_ERR_AUTH,因此未经修改的示例不会连接。采集、GPIO、I2S、编解码器和板级调优由你的开发板代码负责。

调用方需要串行调用onesay_voice_button_init、onesay_voice_button_connect和有上限的onesay_voice_button_step。SDK负责WSS/OSAF、序号和ACK、背压、重放和恢复以及取消。在ACK释放之前保留已缓存的PCM;取消消息发送成功并不是任务的终态结果。必须先由onesay_voice_button_close释放传输,才能释放它的存储和依赖。这条路径不会打开RTC会话。

ESP32 + 实时语音(RTC)#

sdks/esp32中的组件会提供onesay_esp32_rtc.h,前提是选中了CONFIG_ONESAY_RTC_ENABLED。RTC基于LiveKit ESP32 SDK构建;sdks/esp32/idf_component.yml锁定了经过测试的版本。请使用ESP-IDF 6.0.2,并保持组件的依赖图不变。组件中的sdkconfig.rtc.defaults是RTC配置片段,在应用默认配置之前加载;它不是开发板配置。在任务示例里启用RTC,并不会让它变成RTC应用;请从下面的RTC项目开始。

可运行的项目:rtc_smoke#

与开发板无关的项目sdks/esp32/examples/rtc_smoke (../../sdks/esp32/examples/rtc_smoke/README.md)是可运行的RTC起点。它是独立的ESP-IDF应用,只使用OneSay组件和voice_button任务流程。它不包含GPIO、I2S、编解码器、BSP、AEC、Wi-Fi或NVS代码,也不依赖OneSay OS。一次运行会先流式完成一个任务,再用合成采集连接RTC,发送一个数据包,释放所有资源,并在串口控制台输出一行不含内容的摘要。

创建项目的方法与任务示例相同:把rtc_smoke复制到新目录,把组件加入为components/onesay,再把main/voice_button.c和main/voice_button.h从voice_button示例复制到它的main/中。然后用上面的idf.py命令构建并烧录。

请提供强符号版本的onesay_rtc_smoke_configure。它的弱符号默认实现会在任何网络活动之前返回ONESAY_ERR_AUTH。在你的实现返回ONESAY_OK之前,固件要先启动网络、设置可信的UTC时间,并从安全存储中填好配置。项目的README列出了所有字段。开发板设置(Flash大小、PSRAM、控制台)放在第三个defaults文件中。

用设备凭据换取RTC Bearer Token#

RTC持有者对象只接收调用方提供的Bearer Token,从不交换设备密钥。要只用公开调用从设备凭据走到RTC,请在冒烟测试配置中设置rtc_bearer_from_device_credential = true。完整链路如下:

  1. 所有者授权,在设备运行之前完成。所有者创建委托,并决定各项用户同意请求。设备在该委托下用createAppSession创建自己的应用会话。设备会收到房间、应用会话、音频源和委托的ID。ID本身不授予任何权限;服务端会在每次请求时检查实际的授权、委托和用户同意。
  2. 用设备密钥换取短期Token。rtc_bearer步骤通过你现有的安全存储回调(task.credentials.load)读取一次设备密钥。它只把密钥用作Authorization,放在一次exchangeDeviceAccessToken请求(POST /v1/device-access-tokens,请求体{})中,通过公开的onesay_esp32_control_request发送。RTC的Control配置必须与任务的配置指向同一个部署环境、设备、项目和项目环境,否则该步骤会在读取密钥之前失败。无论结果如何,都会擦除密钥。只保留access_token,它取自{"data":{"access_token","token_type":"Bearer","expires_at"}}响应结构。从不调用存储的erase(即恢复出厂设置)。
  3. 用Token连接RTC。onesay_esp32_rtc_prepare、onesay_esp32_rtc_connect_audio和数据包策略只接收这个短期设备访问Token,并使用同一个Control持有者。冒烟测试在释放后擦除自己的副本。

你的幂等键回调在每次运行时都要为exchangeDeviceAccessToken返回新的键,因为重放的键会返回原来的过期时间。对每个RTC操作,它要返回原先与用户同意绑定的键。如果要改用其他既有流程得到的Bearer Token,请提供load_rtc_bearer回调。两种来源只能二选一。切勿把长期有效的设备密钥、平台密钥、服务商密钥或LiveKit密钥写进固件源代码、日志或文档。

准备应用输入#

初始化onesay_esp32_control_context_t:调用onesay_esp32_control_init,传入你的onesay_esp32_control_config_t。提供预期的设备、项目、环境和应用绑定,证书或经过验证的证书包,有上限的请求超时,以及next_idempotency_key回调。即使单独的RTC Bearer Token属于用户,共用的Control初始化函数也要求有效的device_id;这个配置的ID不会改变Bearer Token的身份,也不会授权RTC。把environment设为SDK枚举中你有权使用的环境。网络设置、可信UTC时间和证书存储仍由应用负责。不要关闭TLS主机名或日期校验。

下面的音频流程需要调用方持有的以下输入:

输入公开接口约定
control已初始化的Control持有者;使用期间保留它依赖的证书和键回调。
bearer / bearer_length当前有效的短期用户Bearer Token,或经委托的设备或应用会话Bearer Token,例如通过设备凭据链路得到的设备访问Token。RTC prepare不会交换设备密钥,也不会创建应用会话。不要嵌入平台、服务商或LiveKit的长期密钥。
inputonesay_action_rtc_connection_create_input_t,包含现有的room_id和app_session_id,字符串须以空字符结尾。资源必须已存在并已授权。
connection_delegation所选连接操作的delegation_id;省略时为NULL。它是请求元数据,不代表权限。
audioonesay_rtc_audio_config_t:已授权的source_id、该音轨发布操作单独的delegation_id(留空即省略)、采集和渲染回调,以及now_unix_ms及其上下文。
owner由调用方保留、初始化为NULL的指针;切勿把已持有会话的槽位传给prepare。

键回调必须为每个逻辑操作返回原来的键:createRTCConnection和createRTCPublication是两个独立的操作。涉及用户同意或委托时,每个键都必须与它自己已确认的操作对应。音轨发布不会继承连接的选择器。一切以服务端检查为准;本地选择器或回调即使有效,也不能授予访问权限。

准备、连接、观测和释放#

下面是基于公开头文件的应用代码,不是另一个SDK导出,也不是完整的开发板应用。它需要上面的输入。

C
#include "onesay_esp32_rtc.h"

onesay_err_t example_begin_rtc_audio(
    onesay_esp32_control_context_t *control,
    const uint8_t *bearer, size_t bearer_length,
    const onesay_action_rtc_connection_create_input_t *input,
    const char *connection_delegation,
    const onesay_rtc_audio_config_t *audio,
    onesay_esp32_rtc_t **owner) {
    if (owner == NULL || *owner != NULL) return ONESAY_ERR_INVALID_ARG;
    onesay_err_t result = onesay_esp32_rtc_prepare(
        control, bearer, bearer_length, input, connection_delegation, owner);
    if (result != ONESAY_OK) return result;
    /* Keep *owner even on failure: the caller still owns cleanup. */
    return onesay_esp32_rtc_connect_audio(
        *owner, control, bearer, bearer_length, audio);
}

在应用线程上串行调用这些公开函数,不要在任何媒体、网络或时钟回调中调用。函数返回后,指针和回调上下文要继续保持有效。prepare失败时,最初为空的指针仍为NULL,但HTTP结果不确定时,仍需与服务端核对状态。prepare成功后,即使connect失败,也会留下一个需要close的已持有指针。connect返回ONESAY_OK表示连接请求已受理;不代表已连接、已发布、对方已听到或已实际撤销。

后续调用(owner为保留的会话指针)含义和必需的处理
onesay_esp32_rtc_read_binding(owner, &connection),其中onesay_rtc_binding_t connection = {0}复制原始的OneSay ID、轮次、截止时间和实际状态。区分PREPARED、CONNECTING、CONNECTED、RECONNECTING、FAILED和CLOSED;任何一个状态都不能代替服务端的判定。
onesay_esp32_rtc_read_audio_binding(owner, &publication),其中onesay_rtc_audio_binding_t publication = {0}在可用时复制不含内容的音轨发布ID,连接失败后也可以复制。检查返回值:没有可用的音轨时返回ONESAY_ERR_INVALID_ARG并清空输出,不会伪造ID。
onesay_esp32_rtc_close(&owner),在owner != NULL时调用取消开发板I/O,停止RTC会话,并释放媒体持有者。只有返回ONESAY_OK才会清空指针。失败时保留指针和所有回调上下文,以便之后显式再次清理;不要释放或重新初始化它们。

在成功close之前,读取并保留之后与服务端核对状态所需的全部ID。仅观察到CLOSED不能证明已清理:close失败时可能留下已持有的指针。本地close不会执行endRTCPublication或revokeRTCConnection;服务端清理要调用这些公开操作,并使用它们各自当前的授权以及幂等键和用户同意绑定。

采集必须严格按要求提供16 kHz单声道s16le采样,每次读取最多640个。渲染必须接受实际解码出的s16le格式:8/12/16/24/48 kHz,单声道或立体声,每次回调最多120毫秒(48 kHz下为5,760帧)。所有回调在返回前借用各自的缓冲区,时间预算为40毫秒。幂等的cancel回调可能并发运行,必须唤醒I/O,但不能释放自己的上下文。回调不得调用SDK生命周期函数。这段应用代码不提供GPIO、编解码器、重采样和板级调优;SDK的媒体管线负责编码和格式转换。

授权被拒时,通过onesay_esp32_control_last_error(control)查看错误码、请求ID和是否可重试;不要转储Token、连接材料或传输内容。首次使用的材料一旦过期,延长业务截止时间或修改客户端时钟都无法挽回。音轨发布或连接失败、结果不确定时,不得通过再次调用connect或生成替代键来重试。保留原来的ID和键,释放本地持有者,并通过应用的显式授权流程与服务端核对状态。

完整的接口说明见SDK音频持有者文档 (../../sdks/esp32/README.md#rtc-audio)。

rtc_smoke项目使用合成采集和只计数的渲染接收端。它不提供麦克风、扬声器、AEC或音质调优;这些由你的开发板负责。只使用任务的voice_button示例不使用RTC;任务或JavaScript支持某项功能,并不代表ESP32 RTC也支持。

开发板上的凭据#

为每台设备单独写入设备凭据,并存放在受保护的存储中,例如加密的NVS。切勿把项目API密钥或设备凭据编译进固件,也切勿把它们输出到串口日志。首次连接之前,先提供证书和可信的UTC时间。

查看原始Markdown
# ESP32快速入门

只使用任务的应用选择[语音任务](#esp32-task-voice);要在房间里传输实时音频,选择[实时语音(RTC)](#esp32-realtime-rtc)。不需要OneSay OS。SDK目前处于预览阶段,尚未发布;请把它作为ESP-IDF组件加入你的项目。即使客户端二进制文件或本地能力标志发生变化,所有权限仍以服务端为准。

## ESP32 + 语音任务

从`sdks/esp32/examples/voice_button`开始,这是一个只使用任务的独立ESP-IDF应用。把它复制到新的项目目录,并把OneSay ESP32组件(`sdks/esp32`的内容)加入为`components/onesay`;示例要求组件位于这个位置,缺少组件就无法单独构建。请保留`idf_component.yml`中组件的依赖版本锁定;首次构建时,ESP-IDF组件管理器会下载缺少的依赖。

在这个项目中使用ESP-IDF 6.0.2构建,然后烧录开发板并查看串口输出:

```text
idf.py set-target esp32s3
idf.py build
idf.py -p <PORT> flash monitor
```

构建不需要开发板。烧录只写入应用,不会写入设备凭据。

按照`sdks/esp32/examples/voice_button/README.md`和示例自带的`main/voice_button.h`操作。为`onesay_voice_button_configure`提供安全的设备凭据存储,现有已授权的项目、环境、应用以及音频源和房间绑定,网络访问,可信的UTC时间和证书,以及有上限的16 kHz单声道PCM s16le音频生产者。它的默认钩子会在连接前返回`ONESAY_ERR_AUTH`,因此未经修改的示例不会连接。采集、GPIO、I2S、编解码器和板级调优由你的开发板代码负责。

调用方需要串行调用`onesay_voice_button_init`、`onesay_voice_button_connect`和有上限的`onesay_voice_button_step`。SDK负责WSS/OSAF、序号和ACK、背压、重放和恢复以及取消。在ACK释放之前保留已缓存的PCM;取消消息发送成功并不是任务的终态结果。必须先由`onesay_voice_button_close`释放传输,才能释放它的存储和依赖。这条路径不会打开RTC会话。

## ESP32 + 实时语音(RTC)

`sdks/esp32`中的组件会提供`onesay_esp32_rtc.h`,前提是选中了`CONFIG_ONESAY_RTC_ENABLED`。RTC基于LiveKit ESP32 SDK构建;`sdks/esp32/idf_component.yml`锁定了经过测试的版本。请使用ESP-IDF 6.0.2,并保持组件的依赖图不变。组件中的`sdkconfig.rtc.defaults`是RTC配置片段,在应用默认配置之前加载;它不是开发板配置。在任务示例里启用RTC,并不会让它变成RTC应用;请从下面的RTC项目开始。

### 可运行的项目:`rtc_smoke`

与开发板无关的项目[`sdks/esp32/examples/rtc_smoke`](../../sdks/esp32/examples/rtc_smoke/README.md)是可运行的RTC起点。它是独立的ESP-IDF应用,只使用OneSay组件和`voice_button`任务流程。它不包含GPIO、I2S、编解码器、BSP、AEC、Wi-Fi或NVS代码,也不依赖OneSay OS。一次运行会先流式完成一个任务,再用合成采集连接RTC,发送一个数据包,释放所有资源,并在串口控制台输出一行不含内容的摘要。

创建项目的方法与任务示例相同:把`rtc_smoke`复制到新目录,把组件加入为`components/onesay`,再把`main/voice_button.c`和`main/voice_button.h`从`voice_button`示例复制到它的`main/`中。然后用上面的`idf.py`命令构建并烧录。

请提供强符号版本的`onesay_rtc_smoke_configure`。它的弱符号默认实现会在任何网络活动之前返回`ONESAY_ERR_AUTH`。在你的实现返回`ONESAY_OK`之前,固件要先启动网络、设置可信的UTC时间,并从安全存储中填好配置。项目的README列出了所有字段。开发板设置(Flash大小、PSRAM、控制台)放在第三个defaults文件中。

### 用设备凭据换取RTC Bearer Token

RTC持有者对象只接收调用方提供的Bearer Token,从不交换设备密钥。要只用公开调用从设备凭据走到RTC,请在冒烟测试配置中设置`rtc_bearer_from_device_credential = true`。完整链路如下:

1. **所有者授权,在设备运行之前完成。**所有者创建委托,并决定各项用户同意请求。设备在该委托下用`createAppSession`创建自己的应用会话。设备会收到房间、应用会话、音频源和委托的ID。ID本身不授予任何权限;服务端会在每次请求时检查实际的授权、委托和用户同意。
2. **用设备密钥换取短期Token。**`rtc_bearer`步骤通过你现有的安全存储回调(`task.credentials.load`)读取一次设备密钥。它只把密钥用作`Authorization`,放在一次`exchangeDeviceAccessToken`请求(`POST /v1/device-access-tokens`,请求体`{}`)中,通过公开的`onesay_esp32_control_request`发送。RTC的Control配置必须与任务的配置指向同一个部署环境、设备、项目和项目环境,否则该步骤会在读取密钥之前失败。无论结果如何,都会擦除密钥。只保留`access_token`,它取自`{"data":{"access_token","token_type":"Bearer","expires_at"}}`响应结构。从不调用存储的`erase`(即恢复出厂设置)。
3. **用Token连接RTC。**`onesay_esp32_rtc_prepare`、`onesay_esp32_rtc_connect_audio`和数据包策略只接收这个短期设备访问Token,并使用同一个Control持有者。冒烟测试在释放后擦除自己的副本。

你的幂等键回调在每次运行时都要为`exchangeDeviceAccessToken`返回新的键,因为重放的键会返回原来的过期时间。对每个RTC操作,它要返回原先与用户同意绑定的键。如果要改用其他既有流程得到的Bearer Token,请提供`load_rtc_bearer`回调。两种来源只能二选一。切勿把长期有效的设备密钥、平台密钥、服务商密钥或LiveKit密钥写进固件源代码、日志或文档。

### 准备应用输入

初始化`onesay_esp32_control_context_t`:调用`onesay_esp32_control_init`,传入你的`onesay_esp32_control_config_t`。提供预期的设备、项目、环境和应用绑定,证书或经过验证的证书包,有上限的请求超时,以及`next_idempotency_key`回调。即使单独的RTC Bearer Token属于用户,共用的Control初始化函数也要求有效的`device_id`;这个配置的ID不会改变Bearer Token的身份,也不会授权RTC。把`environment`设为SDK枚举中你有权使用的环境。网络设置、可信UTC时间和证书存储仍由应用负责。不要关闭TLS主机名或日期校验。

下面的音频流程需要调用方持有的以下输入:

| 输入 | 公开接口约定 |
| --- | --- |
| `control` | 已初始化的Control持有者;使用期间保留它依赖的证书和键回调。 |
| `bearer` / `bearer_length` | 当前有效的短期用户Bearer Token,或经委托的设备或应用会话Bearer Token,例如通过[设备凭据链路](#device-credential-to-rtc-bearer)得到的设备访问Token。RTC prepare不会交换设备密钥,也不会创建应用会话。不要嵌入平台、服务商或LiveKit的长期密钥。 |
| `input` | `onesay_action_rtc_connection_create_input_t`,包含现有的`room_id`和`app_session_id`,字符串须以空字符结尾。资源必须已存在并已授权。 |
| `connection_delegation` | 所选连接操作的`delegation_id`;省略时为`NULL`。它是请求元数据,不代表权限。 |
| `audio` | `onesay_rtc_audio_config_t`:已授权的`source_id`、该音轨发布操作单独的`delegation_id`(留空即省略)、采集和渲染回调,以及`now_unix_ms`及其上下文。 |
| `owner` | 由调用方保留、初始化为`NULL`的指针;切勿把已持有会话的槽位传给prepare。 |

键回调必须为每个逻辑操作返回原来的键:`createRTCConnection`和`createRTCPublication`是两个独立的操作。涉及用户同意或委托时,每个键都必须与它自己已确认的操作对应。音轨发布不会继承连接的选择器。一切以服务端检查为准;本地选择器或回调即使有效,也不能授予访问权限。

### 准备、连接、观测和释放

下面是基于公开头文件的应用代码,不是另一个SDK导出,也不是完整的开发板应用。它需要上面的输入。

```c
#include "onesay_esp32_rtc.h"

onesay_err_t example_begin_rtc_audio(
    onesay_esp32_control_context_t *control,
    const uint8_t *bearer, size_t bearer_length,
    const onesay_action_rtc_connection_create_input_t *input,
    const char *connection_delegation,
    const onesay_rtc_audio_config_t *audio,
    onesay_esp32_rtc_t **owner) {
    if (owner == NULL || *owner != NULL) return ONESAY_ERR_INVALID_ARG;
    onesay_err_t result = onesay_esp32_rtc_prepare(
        control, bearer, bearer_length, input, connection_delegation, owner);
    if (result != ONESAY_OK) return result;
    /* Keep *owner even on failure: the caller still owns cleanup. */
    return onesay_esp32_rtc_connect_audio(
        *owner, control, bearer, bearer_length, audio);
}
```

在应用线程上串行调用这些公开函数,不要在任何媒体、网络或时钟回调中调用。函数返回后,指针和回调上下文要继续保持有效。prepare失败时,最初为空的指针仍为`NULL`,但HTTP结果不确定时,仍需与服务端核对状态。prepare成功后,即使connect失败,也会留下一个需要close的已持有指针。connect返回`ONESAY_OK`表示连接请求已受理;不代表已连接、已发布、对方已听到或已实际撤销。

| 后续调用(`owner`为保留的会话指针) | 含义和必需的处理 |
| --- | --- |
| `onesay_esp32_rtc_read_binding(owner, &connection)`,其中`onesay_rtc_binding_t connection = {0}` | 复制原始的OneSay ID、轮次、截止时间和实际状态。区分PREPARED、CONNECTING、CONNECTED、RECONNECTING、FAILED和CLOSED;任何一个状态都不能代替服务端的判定。 |
| `onesay_esp32_rtc_read_audio_binding(owner, &publication)`,其中`onesay_rtc_audio_binding_t publication = {0}` | 在可用时复制不含内容的音轨发布ID,连接失败后也可以复制。检查返回值:没有可用的音轨时返回`ONESAY_ERR_INVALID_ARG`并清空输出,不会伪造ID。 |
| `onesay_esp32_rtc_close(&owner)`,在`owner != NULL`时调用 | 取消开发板I/O,停止RTC会话,并释放媒体持有者。只有返回`ONESAY_OK`才会清空指针。失败时保留指针和所有回调上下文,以便之后显式再次清理;不要释放或重新初始化它们。 |

在成功close之前,读取并保留之后与服务端核对状态所需的全部ID。仅观察到`CLOSED`不能证明已清理:close失败时可能留下已持有的指针。本地close不会执行`endRTCPublication`或`revokeRTCConnection`;服务端清理要调用这些公开操作,并使用它们各自当前的授权以及幂等键和用户同意绑定。

采集必须严格按要求提供16 kHz单声道s16le采样,每次读取最多640个。渲染必须接受实际解码出的s16le格式:8/12/16/24/48 kHz,单声道或立体声,每次回调最多120毫秒(48 kHz下为5,760帧)。所有回调在返回前借用各自的缓冲区,时间预算为40毫秒。幂等的`cancel`回调可能并发运行,必须唤醒I/O,但不能释放自己的上下文。回调不得调用SDK生命周期函数。这段应用代码不提供GPIO、编解码器、重采样和板级调优;SDK的媒体管线负责编码和格式转换。

授权被拒时,通过`onesay_esp32_control_last_error(control)`查看错误码、请求ID和是否可重试;不要转储Token、连接材料或传输内容。首次使用的材料一旦过期,延长业务截止时间或修改客户端时钟都无法挽回。音轨发布或连接失败、结果不确定时,不得通过再次调用connect或生成替代键来重试。保留原来的ID和键,释放本地持有者,并通过应用的显式授权流程与服务端核对状态。

完整的接口说明见[SDK音频持有者文档](../../sdks/esp32/README.md#rtc-audio)。

`rtc_smoke`项目使用合成采集和只计数的渲染接收端。它不提供麦克风、扬声器、AEC或音质调优;这些由你的开发板负责。只使用任务的`voice_button`示例不使用RTC;任务或JavaScript支持某项功能,并不代表ESP32 RTC也支持。

## 开发板上的凭据

为每台设备单独写入设备凭据,并存放在受保护的存储中,例如加密的NVS。切勿把项目API密钥或设备凭据编译进固件,也切勿把它们输出到串口日志。首次连接之前,先提供证书和可信的UTC时间。