小智AI超好玩儿的,最近准备自搭后台让其接入免费听歌。就是想听啥哥就能听啥歌。可以随心DIY...我有后台资源,搭个mcp服务就行了。在物联网时代,设备的远程管理和固件升级能力至关重要。小智ESP32设备采用了一套完善的OTA(Over-The-Air)升级和设备激活机制,确保设备能够安全、可靠地接入云平台并获取最新功能。本文将详细介绍小智ESP32设备的接入流程、协议规范以及核心代码实现。
小智AI项目开源地址:https://github.com/78/xiaozhi-esp32
小智ESP32环境搭建,参加文档《Windows搭建 ESP IDF 5.5.1开发环境以及编译小智》

设备接入整体流程
小智ESP32设备的接入流程主要包含以下几个关键步骤:
- 设备信息采集:收集设备的硬件信息、固件版本、网络状态等
- OTA版本检查:向服务器发送版本检查请求,获取最新固件信息
- 设备激活:根据激活版本执行不同的激活流程
- 配置更新:获取并应用MQTT、WebSocket等服务器配置
- 固件升级:如有新版本,执行OTA升级流程
设备接入操作流程
1. 设备配网
设备配网是接入流程的第一步,使用以下方式:
- 设备进入AP模式,创建一个临时WiFi热点(名称通常包含设备型号或ID)
- 用户手机连接到该临时热点
- 在手机浏览器中访问设备的Web配网页面(通常是192.168.4.1)
- 输入目标WiFi的SSID和密码,点击确认
- 设备连接到指定WiFi网络,配网完成
2. 配网成功后的激活码获取
配网成功后,设备会向OTA地址发送POST请求,自动执行以下操作:
-
连接服务器:设备使用配置的WiFi网络连接到小智云服务器(api.tenclass.net/xiaozhi/ota/)
-
发送设备信息:设备向服务器发送设备ID、MAC地址、固件版本等信息
-
获取激活码 :服务器验证设备信息后,返回包含激活码的响应:
json{ "activation": { "code": "539000", "message": "xiaozhi.me\n539000", "challenge": "8dcafd09-fa80-4a4e-b97b-851918ee1b08" } // 其他配置信息... } -
显示激活码:设备通过显示屏、LED指示灯或语音等方式向用户展示激活码(如"539000")
3. 登录xiaozhi.me系统添加设备
获取激活码后,用户需要通过以下步骤完成设备添加:
-
访问xiaozhi.me :在浏览器中打开https://xiaozhi.me
-
用户登录:使用已注册的账号登录系统
- 如果没有账号,需要先完成注册
- 支持手机号、邮箱等多种登录方式
-
进入设备管理页面:
- 登录成功后,点击导航栏中的"设备管理"或类似选项
- 进入设备列表页面
-
添加新设备:
- 点击"添加设备"或"注册新设备"按钮
- 选择设备类型(根据实际设备型号选择)
-
输入激活码:
- 在弹出的添加设备表单中,找到"激活码"字段
- 输入设备显示的激活码(如"539000")
- 点击"下一步"或"确认"按钮
-
完成设备添加:
- 系统验证激活码的有效性
- 验证通过后,设备将显示在设备列表中
- 用户可以为设备设置名称、位置等信息
- 设备状态变为"在线",表示已成功接入
4. 设备激活完成
添加设备成功后,系统会自动完成以下操作:
- 后台激活:服务器向设备发送激活确认消息
- 设备响应:设备收到确认消息后,完成最终激活流程
- 状态同步:设备状态在xiaozhi.me系统中更新为"已激活"
- 功能可用:用户可以在系统中查看设备状态、控制设备功能等

OTA接口详细说明
接口地址
POST https://api.tenclass.net/xiaozhi/ota/
Content-Type: application/json
请求头参数
| 参数名 | 类型 | 描述 | 是否必需 |
|---|---|---|---|
| Activation-Version | String | 激活版本,1或2 | 必需 |
| Device-Id | String | 设备唯一标识符(MAC地址) | 必需 |
| Client-Id | String | 客户端唯一标识符(UUID v4) | 必需 |
| User-Agent | String | 客户端名称和版本 | 必需 |
| Accept-Language | String | 客户端当前语言 | 可选 |
| Serial-Number | String | 设备序列号(仅激活版本2需要) | 条件必需 |
注:当前OTA接口地址是固化在固件代码里了。默认值是虾哥的后台接口地址: https://api.tenclass.net/xiaozhi/ota/。
后台管理地址是xiaozhi.me, 如果想接入自有平台或其他平台,需要改下代码重新刷固件。
代码中的修改位置:

cpp
Ota::~Ota() {
}
std::string Ota::GetCheckVersionUrl() {
Settings settings("wifi", false);
std::string url = settings.GetString("ota_url");
if (url.empty()) {
url = CONFIG_OTA_URL;
}
return url;
}
或者修改配置文件中的值:


注:由于我买的星辰的板子,固件1.6.2以下才需要这么干。如果是新版本如1.9.2或者2.1.0最新固件,则默认就支持在配网的时候去修改ota的url地址。
新固件如2.1.0版本,配网界面有以下高级选项,可以自定义OTA,方便接入其他三方后台服务。

请求体结构
请求体为JSON格式,包含丰富的设备信息:
json
{
"version": 2,
"flash_size": 4194304,
"psram_size": 0,
"minimum_free_heap_size": 123456,
"mac_address": "00:00:00:00:00:00",
"uuid": "00000000-0000-0000-0000-000000000000",
"chip_model_name": "esp32s3",
"chip_info": {
"model": 1,
"cores": 2,
"revision": 0,
"features": 0
},
"application": {
"name": "my-app",
"version": "1.0.0",
"compile_time": "2021-01-01T00:00:00Z",
"idf_version": "4.2-dev",
"elf_sha256": ""
},
"partition_table": [
{
"label": "app",
"type": 1,
"subtype": 2,
"address": 10000,
"size": 100000
}
],
"ota": {
"label": "ota_0"
},
"board": {
"type": "esp32s3-devkitc",
"name": "aa",
"ssid": "aa",
"rssi": "0",
"channel": "1",
"ip": "192.168.1.5",
"mac": "34:85:18:ab:cd:ef"
}
}
响应信息格式
服务器响应包含设备所需的各种配置信息:
json
{
"mqtt": {
"endpoint": "mqtt.xiaozhi.me",
"client_id": "GID_test@@@34_85_18_ab_cd_ef@@@550e8400-e29b-41d4-a716-446655440000",
"username": "eyJpcCI6IjIyMC4yMDIuNzIuNTgifQ==",
"password": "advv6eT5EDtQ4DOZ2QpJiTnla6ar0SZFKRybjj/CuX0=",
"publish_topic": "device-server",
"subscribe_topic": "null"
},
"websocket": {
"url": "wss://api.tenclass.net/xiaozhi/v1/",
"token": "test-token"
},
"server_time": {
"timestamp": 1768454488884,
"timezone_offset": 480
},
"firmware": {
"version": "1.0.0",
"url": ""
},
"activation": {
"code": "539000",
"message": "xiaozhi.me\n539000",
"challenge": "8dcafd09-fa80-4a4e-b97b-851918ee1b08"
}
}
完整OTA报文例子
bash
### OTA 小智接入API
post https://api.tenclass.net/xiaozhi/ota/
Content-Type: application/json
Activation-Version:1
Device-Id:34:85:18:ab:cd:ee
Client-Id:550e8400-e29b-41d4-a716-446655440000
Accept-Language:zh-CN
#Serial-Number:34:85:18:ab:cd:ee
{
"version": 2,
"flash_size": 4194304,
"psram_size": 0,
"minimum_free_heap_size": 123456,
"mac_address": "00:00:00:00:00:00",
"uuid": "00000000-0000-0000-0000-000000000000",
"chip_model_name": "esp32s3",
"chip_info": {
"model": 1,
"cores": 2,
"revision": 0,
"features": 0
},
"application": {
"name": "my-app",
"version": "1.0.0",
"compile_time": "2021-01-01T00:00:00Z",
"idf_version": "4.2-dev",
"elf_sha256": ""
},
"partition_table": [
{
"label": "app",
"type": 1,
"subtype": 2,
"address": 10000,
"size": 100000
}
],
"ota": {
"label": "ota_0"
},
"board": {
"type": "esp32s3-devkitc",
"name": "aa",
"ssid": "aa",
"rssi": "0",
"channel": "1",
"ip": "192.168.1.5",
"mac": "34:85:18:ab:cd:ee"
}
}
##响应信息
{
"mqtt": {
"endpoint": "mqtt.xiaozhi.me",
"client_id": "GID_test@@@34_85_18_ab_cd_ef@@@550e8400-e29b-41d4-a716-446655440000",
"username": "eyJpcCI6IjIyMC4yMDIuNzIuNTgifQ==",
"password": "advv6eT5EDtQ4DOZ2QpJiTnla6ar0SZFKRybjj/CuX0=",
"publish_topic": "device-server",
"subscribe_topic": "null"
},
"websocket": {
"url": "wss://api.tenclass.net/xiaozhi/v1/",
"token": "test-token"
},
"server_time": {
"timestamp": 1768454488884,
"timezone_offset": 480
},
"firmware": {
"version": "1.0.0",
"url": ""
},
"activation": {
"code": "539000",
"message": "xiaozhi.me\n539000",
"challenge": "8dcafd09-fa80-4a4e-b97b-851918ee1b08"
}
}
上述报文是第一次请求的报文示例,可以看出后台返回了activation字段信息,包含了激活码code信息,此时设备上会语音提示你请登录xiaozhi.me后台完成设备激活,输入code码。用户登录xiaozhi.me后台完成设备激活后,如果再次请求,则会收到以下响应内容:
bash
{
"mqtt": {
"endpoint": "mqtt.xiaozhi.me",
"client_id": "GID_test@@@34_85_18_ab_cd_ee@@@550e8400-e29b-41d4-a716-446655440001",
"username": "eyJpcCI6IjIyMC4yMDIuNzIuNTgifQ==",
"password": "eVsbvo+Yu1GFbj1uoq/iVRtrvmHFeHDullacyuGmtFo=",
"publish_topic": "device-server",
"subscribe_topic": "null"
},
"websocket": {
"url": "wss://api.tenclass.net/xiaozhi/v1/",
"token": "test-token"
},
"server_time": {
"timestamp": 1768642695827,
"timezone_offset": 480
},
"firmware": {
"version": "1.0.0",
"url": ""
}
}
可以看出这时少了activation字段信息,设备再次开机不会再走激活流程了:
cpp
void Application::CheckNewVersion() {
//省略......
//如果没返回激活码则不再展示
if (!ota_->HasActivationCode() && !ota_->HasActivationChallenge()) {
// Exit the loop if done checking new version
break;
}
display->SetStatus(Lang::Strings::ACTIVATION);
// Activation code is shown to the user and waiting for the user to input
//如果返回了激活码,则再设备上展示
if (ota_->HasActivationCode()) {
ShowActivationCode(ota_->GetActivationCode(), ota_->GetActivationMessage());
}
// This will block the loop until the activation is done or timeout
//激活流程
for (int i = 0; i < 10; ++i) {
ESP_LOGI(TAG, "Activating... %d/%d", i + 1, 10);
OTA请求代码
cpp
std::unique_ptr<Http> Ota::SetupHttp() {
auto& board = Board::GetInstance();
auto network = board.GetNetwork();
auto http = network->CreateHttp(0);
auto user_agent = SystemInfo::GetUserAgent();
http->SetHeader("Activation-Version", has_serial_number_ ? "2" : "1");
http->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
http->SetHeader("Client-Id", board.GetUuid());
if (has_serial_number_) {
http->SetHeader("Serial-Number", serial_number_.c_str());
ESP_LOGI(TAG, "Setup HTTP, User-Agent: %s, Serial-Number: %s", user_agent.c_str(), serial_number_.c_str());
}
http->SetHeader("User-Agent", user_agent);
http->SetHeader("Accept-Language", Lang::CODE);
http->SetHeader("Content-Type", "application/json");
return http;
}
设备激活机制
小智ESP32设备支持两种激活版本,提供不同级别的安全性和复杂度:
激活版本1(简化流程)
当Activation-Version请求头设置为"1"时,设备采用简化的激活流程:
- 无需发送
Serial-Number头 - 无需执行额外的激活请求
- 设备自动完成激活
这种方式适用于对安全性要求不高的场景,简化了设备接入流程。
激活版本2(完整流程)
当Activation-Version请求头设置为"2"时(默认值),设备需要执行完整的激活流程:
-
发送带有序列号的版本检查请求:
cpphttp->SetHeader("Activation-Version", "2"); http->SetHeader("Serial-Number", serial_number_.c_str()); -
获取激活挑战码 :服务器在响应中返回
activation.challenge字段 -
计算HMAC值:使用设备内部密钥对挑战码进行HMAC-SHA256计算
-
发送激活请求:将计算得到的HMAC值和相关信息发送给服务器进行验证
-
完成激活:服务器验证通过后,设备完成激活流程
在代码中也可以看到,是否启用激活版本2,是根据用户的配置,是否在ESP_EFUSE_BLOCK_USR_DATA用户自定义数据区写入了序列号来区分的:
cpp
Ota::Ota() {
#ifdef ESP_EFUSE_BLOCK_USR_DATA
// Read Serial Number from efuse user_data
uint8_t serial_number[33] = {0};
if (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA, serial_number, 32 * 8) == ESP_OK) {
if (serial_number[0] == 0) {
//无序列号
has_serial_number_ = false;
} else {
serial_number_ = std::string(reinterpret_cast<char*>(serial_number), 32);
//有序列号
has_serial_number_ = true;
}
}
#endif
}
根据这个http->SetHeader("Activation-Version", has_serial_number_ ? "2" : "1");来设置是2还是1。
网上买到的开源硬件,没有给设备刷过序列号设置过秘钥,没有登记过系统。猜测这个地方是1,所以可以很容易的接入xiaozhi.me后台。(实际它并未走设备的激活协议。而只是让输入接口返回的code码,完成了绑定而已,并未做进一步的安全认证激活流程,校验hmac256的操作)。正式商用的话,为了安全接入,才会增加这层安全机制(有了该机制,不在系统登记在册的设备就彻底无法接入了)。
cpp
std::unique_ptr<Http> Ota::SetupHttp() {
auto& board = Board::GetInstance();
auto network = board.GetNetwork();
auto http = network->CreateHttp(0);
auto user_agent = SystemInfo::GetUserAgent();
http->SetHeader("Activation-Version", has_serial_number_ ? "2" : "1");
http->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
http->SetHeader("Client-Id", board.GetUuid());
if (has_serial_number_) {
http->SetHeader("Serial-Number", serial_number_.c_str());
ESP_LOGI(TAG, "Setup HTTP, User-Agent: %s, Serial-Number: %s", user_agent.c_str(), serial_number_.c_str());
}
http->SetHeader("User-Agent", user_agent);
http->SetHeader("Accept-Language", Lang::CODE);
http->SetHeader("Content-Type", "application/json");
return http;
}
HMAC计算与安全机制
在激活版本2中,设备使用HMAC-SHA256算法对挑战码进行加密,确保激活过程的安全性:
核心代码实现
cpp
std::string Ota::GetActivationPayload() {
if (!has_serial_number_) {
return "{}";
}
std::string hmac_hex;
#ifdef SOC_HMAC_SUPPORTED
uint8_t hmac_result[32]; // SHA-256输出为32字节
// 使用Key0计算HMAC
esp_err_t ret = esp_hmac_calculate(HMAC_KEY0, (uint8_t*)activation_challenge_.data(), activation_challenge_.size(), hmac_result);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "HMAC calculation failed: %s", esp_err_to_name(ret));
return "{}";
}
for (size_t i = 0; i < sizeof(hmac_result); i++) {
char buffer[3];
sprintf(buffer, "%02x", hmac_result[i]);
hmac_hex += buffer;
}
#endif
cJSON *payload = cJSON_CreateObject();
cJSON_AddStringToObject(payload, "algorithm", "hmac-sha256");
cJSON_AddStringToObject(payload, "serial_number", serial_number_.c_str());
cJSON_AddStringToObject(payload, "challenge", activation_challenge_.c_str());
cJSON_AddStringToObject(payload, "hmac", hmac_hex.c_str());
auto json_str = cJSON_PrintUnformatted(payload);
std::string json(json_str);
cJSON_free(json_str);
cJSON_Delete(payload);
ESP_LOGI(TAG, "Activation payload: %s", json.c_str());
return json;
}
激活报文举例
bash
### OTA 小智接入设备激活API
post https://api.tenclass.net/xiaozhi/ota/activate
Content-Type: application/json
Activation-Version:2
Device-Id:34:85:18:ab:cd:ef
Client-Id:550e8400-e29b-41d4-a716-446655440000
Accept-Language:zh-CN
Serial-Number:34:85:18:ab:cd:ef
{
"algorithm":"hmac-sha256",
"serial_number":"34:85:18:ab:cd:ef",
"challenge":"8dcafd09-fa80-4a4e-b97b-851918ee1b08",
"hmac":"4a11c0babb8b4c443e7ddd36315f073834e66f9cc756abebe3ce14c779007745"
}
##响应信息
{
}
HMAC_KEY0说明
在ESP32平台中,HMAC_KEY0是一个特殊的硬件密钥,存储在设备的eFuse中。它具有以下特点:
- 硬件级安全:存储在ESP32的安全eFuse区域,无法通过软件直接读取
- 唯一标识:每个设备都有唯一的HMAC_KEY0值
- 不可逆性:只能用于HMAC计算,无法反向推导出原始密钥
- 硬件加速 :通过
esp_hmac_calculateAPI使用硬件加速计算HMAC值
HMAC_KEY0在ESP-IDF框架中定义,无需用户手动定义。它是ESP32芯片的内置安全特性,确保设备身份验证的安全性。
更多关于HMAC的介绍,参见乐鑫官方文档:https://docs.espressif.com/projects/esp-idf/zh_CN/v5.5.1/esp32s3/api-reference/peripherals/hmac.html
HMAC_KEY0的后台验证机制
关于"HMAC_KEY0是设备硬件密钥,后台如何验证"的猜测?涉及到ESP32的安全设计和设备生产流程:
密钥与序列号的关联机制
在设备生产或烧录阶段,执行以下关键操作:
- 生成唯一密钥对:为每个设备生成唯一的HMAC_KEY0和对应的序列号
- 安全存储 :
- HMAC_KEY0被写入ESP32的eFuse安全区域,无法通过软件读取
- 序列号也被写入eFuse的用户数据区域(
ESP_EFUSE_USER_DATA)
- 建立映射关系:将设备的序列号与对应的HMAC_KEY0建立映射关系,并安全存储在后台服务器中
激活验证流程
当设备进行激活时,验证流程如下:
-
设备侧操作:
cpp// 从eFuse读取序列号 esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA, serial_number, 32 * 8); // 使用硬件密钥计算HMAC esp_hmac_calculate(HMAC_KEY0, (uint8_t*)activation_challenge_.data(), activation_challenge_.size(), hmac_result); -
激活请求:设备将序列号、挑战码和计算得到的HMAC值发送给服务器
-
服务器侧验证:
- 根据收到的序列号,从数据库中查找对应的HMAC_KEY0
- 使用相同的挑战码和查找到的HMAC_KEY0计算HMAC值
- 将服务器计算的HMAC值与设备发送的HMAC值进行比较
- 如果一致,则验证通过;否则,验证失败
安全设计优势
这种设计方案具有以下安全优势:
-
密钥永不泄露:HMAC_KEY0永远不会离开设备的eFuse区域,即使设备被破解,也无法获取原始密钥
-
双向验证:
- 设备验证服务器(通过挑战码的真实性)
- 服务器验证设备(通过HMAC值的正确性)
-
防止克隆:每个设备的HMAC_KEY0都是唯一的,无法通过复制序列号来克隆设备
-
抗重放攻击:挑战码是一次性的,每次激活都会生成新的挑战码
替代方案:密钥派生机制
在某些实现中,序列号和HMAC_KEY0之间可能采用密钥派生机制,而不是直接存储映射关系:
- 序列号作为基础输入,通过特定算法派生HMAC_KEY0
- 服务器和设备使用相同的派生算法
- 这种方式避免了服务器存储密钥的需求,进一步提高安全性
无论采用哪种方式,核心思想都是确保HMAC_KEY0的安全性,同时实现可靠的设备身份验证。
激活请求实现
设备在计算完HMAC值后,需要发送激活请求:
cpp
esp_err_t Ota::Activate() {
if (!has_activation_challenge_) {
ESP_LOGW(TAG, "No activation challenge found");
return ESP_FAIL;
}
std::string url = GetCheckVersionUrl();
if (url.back() != '/') {
url += "/activate";
} else {
url += "activate";
}
auto http = SetupHttp();
std::string data = GetActivationPayload();
http->SetContent(std::move(data));
if (!http->Open("POST", url)) {
ESP_LOGE(TAG, "Failed to open HTTP connection");
return ESP_FAIL;
}
auto status_code = http->GetStatusCode();
if (status_code == 202) {
return ESP_ERR_TIMEOUT;
}
if (status_code != 200) {
ESP_LOGE(TAG, "Failed to activate, code: %d, body: %s", status_code, http->ReadAll().c_str());
return ESP_FAIL;
}
ESP_LOGI(TAG, "Activation successful");
return ESP_OK;
}
OTA版本检查与升级流程
设备定期执行OTA版本检查,获取最新固件信息和服务器配置:
cpp
esp_err_t Ota::CheckVersion() {
// ...设备信息采集
auto http = SetupHttp();
std::string data = board.GetSystemInfoJson();
std::string method = data.length() > 0 ? "POST" : "GET";
http->SetContent(std::move(data));
if (!http->Open(method, url)) {
// ...错误处理
}
// ...解析响应,获取固件信息、激活码、MQTT配置等
return ESP_OK;
}
配置更新与应用
设备在收到服务器响应后,会自动更新MQTT和WebSocket配置:
cpp
cJSON *mqtt = cJSON_GetObjectItem(root, "mqtt");
if (cJSON_IsObject(mqtt)) {
Settings settings("mqtt", true);
cJSON *item = NULL;
cJSON_ArrayForEach(item, mqtt) {
if (cJSON_IsString(item)) {
if (settings.GetString(item->string) != item->valuestring) {
settings.SetString(item->string, item->valuestring);
}
} else if (cJSON_IsNumber(item)) {
if (settings.GetInt(item->string) != item->valueint) {
settings.SetInt(item->string, item->valueint);
}
}
}
has_mqtt_config_ = true;
}
总结与最佳实践
小智ESP32设备的接入流程设计考虑了安全性、可靠性和灵活性:
- 分层安全机制:通过激活版本选择,平衡安全性和复杂度
- 硬件级安全:利用ESP32的HMAC硬件加速和eFuse密钥存储,确保设备身份验证的安全性
- 完整的配置管理:支持MQTT、WebSocket等多种通信协议的配置更新
- 灵活的升级策略:支持强制升级和条件升级,确保设备始终运行最新固件
最佳实践
- 选择合适的激活版本:根据设备使用场景和安全要求选择激活版本
- 确保序列号的唯一性:序列号是设备身份的重要标识,必须保证唯一性
- 定期执行版本检查:建议设备定期(如每天)执行版本检查,及时获取更新
- 处理网络异常:在网络不稳定的环境下,实现重试机制和错误处理
- 验证固件完整性:在OTA升级过程中,使用SHA256等算法验证固件完整性
通过遵循这些最佳实践,可以确保小智ESP32设备安全、可靠地接入云平台,并获得最佳的用户体验。