@@ -0,0 +1,33 @@
|
||||
# Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
# SPDX-License-Identifier: Apache License 2.0
|
||||
|
||||
file(GLOB APP_SRC "./echoear_app/*.c")
|
||||
|
||||
set(COMPONENT_SRCS "conv_ai_embedded_kit.c configuration_ap.c network.c platform_client.c" )
|
||||
|
||||
if (CONFIG_PRINT_TASK_INFO)
|
||||
add_definitions(-DPRINT_TASK_INFO)
|
||||
endif()
|
||||
|
||||
set(COMPONENT_ADD_INCLUDEDIRS .)
|
||||
set(COMPONENT_EMBED_TXTFILES assets/wifi_configuration_ap.html)
|
||||
|
||||
set(COMPONENT_REQUIRES esp_http_client esp_http_server esp_wifi esp-tls mbedtls nvs_flash json espressif__gmf_ai_audio espressif__esp_board_manager espressif__esp_lcd_touch)
|
||||
|
||||
register_component()
|
||||
|
||||
set(assets_root "${CMAKE_CURRENT_LIST_DIR}/../assets")
|
||||
|
||||
if(EXISTS "${assets_root}")
|
||||
spiffs_create_partition_assets(eaf "${assets_root}" FLASH_IN_PROJECT
|
||||
MMAP_FILE_SUPPORT_FORMAT ".eaf"
|
||||
IMPORT_INC_PATH "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
add_dependencies(${COMPONENT_LIB} assets_eaf_bin)
|
||||
# The application component includes mmap_generate_eaf.h, which is emitted by
|
||||
# the assets generator into this directory. Make that relationship explicit
|
||||
# so a clean parallel build cannot compile the display source too early.
|
||||
idf_component_get_property(application_lib application COMPONENT_LIB)
|
||||
add_dependencies(${application_lib} assets_eaf_bin)
|
||||
endif()
|
||||
|
||||
set(COMPONENT_ADD_INCLUDEDIRS . ${FRAMEWORK_INC} ${SERVICE_INC})
|
||||
@@ -0,0 +1,31 @@
|
||||
menu "Example Configuration"
|
||||
|
||||
choice AI_AGENT_MODE
|
||||
prompt "Volcano RTC/WS mode"
|
||||
default VOLC_WS_MODE
|
||||
|
||||
config VOLC_WS_MODE
|
||||
bool "enable volcano ws mode"
|
||||
|
||||
endchoice
|
||||
|
||||
menu "Develop Configuration"
|
||||
|
||||
choice VOLC_AUDIO_CODEC
|
||||
prompt "supported audio codec"
|
||||
default VOLC_AUDIO_G711A
|
||||
|
||||
config VOLC_AUDIO_G711A
|
||||
bool "External G711A codec: 8k G711A data to SDK for upstream; 8k G711A data callback from SDK for downstream"
|
||||
|
||||
config VOLC_AUDIO_PCM16
|
||||
bool "No audio codec: 16k pcm data to SDK for upstream; 16k pcm data callback from SDK for downstream"
|
||||
endchoice
|
||||
|
||||
config PRINT_TASK_INFO
|
||||
bool "print task info"
|
||||
default n
|
||||
|
||||
endmenu
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>EchoEar 平台激活</title>
|
||||
<style>body{font:16px system-ui;background:#f1f5f9;margin:0;color:#172b4d}main{max-width:420px;margin:32px auto;padding:24px;background:white;border-radius:16px}label{display:block;margin:18px 0}input,button{box-sizing:border-box;width:100%;padding:12px;border:1px solid #cbd5e1;border-radius:8px;font:inherit}input{margin-top:6px}button{cursor:pointer;margin-top:12px;background:#1769d2;color:white}button:disabled{opacity:.6;cursor:wait}small{color:#526079}#status{white-space:pre-wrap;line-height:1.6}#networks a{display:block;margin:12px 0}</style></head>
|
||||
<body><main><h1>EchoEar 平台激活</h1><p>填写家中的2.4 GHz Wi-Fi 和平台分配给这台设备的凭据。</p>
|
||||
<form id="form" novalidate>
|
||||
<label>2.4 GHz Wi-Fi 名称<input id="ssid" required></label>
|
||||
<label>Wi-Fi 密码<input id="password" type="password" maxlength="64" autocomplete="off"><small>无密码网络可留空。</small></label>
|
||||
<label>平台 Device ID<input id="device_id" required minlength="36" maxlength="36" autocomplete="off"></label>
|
||||
<label>平台 Device Secret<input id="device_secret" type="password" required minlength="32" maxlength="128" autocomplete="off"></label>
|
||||
<p id="status" role="status" aria-live="polite">请保持手机连接设备热点。</p>
|
||||
<button id="submit" type="submit">连接并激活</button><button id="scan" type="button">扫描附近的 Wi-Fi</button><div id="networks"></div>
|
||||
</form></main><script>
|
||||
const form=document.getElementById('form'), button=document.getElementById('submit'), scan=document.getElementById('scan'), status=document.getElementById('status'), networks=document.getElementById('networks');
|
||||
let busy=false;
|
||||
const messages={invalid_fields:'请检查配置:Wi-Fi 名称最多32字节,Device ID 为36位,Device Secret 为32至128位。',wifi_failed:'Wi-Fi 连接失败或超时。请检查2.4 GHz网络名称和密码,再重试。',save_failed:'设备保存配置失败,请重试。',receive_timeout:'设备未完整收到配置,请保持连接热点后重试。',no_memory:'设备暂时忙,请稍后重试。'};
|
||||
function show(text,failed=false){status.textContent=text;status.style.color=failed?'#b42318':'#175cd3'}
|
||||
function setBusy(value){busy=value;button.disabled=value;scan.disabled=value;button.textContent=value?'正在连接,请稍候…':'连接并激活'}
|
||||
async function request(path,options={},timeout=7000){const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),timeout);try{const response=await fetch(path,{...options,cache:'no-store',signal:controller.signal}),data=await response.json();if(!response.ok)throw Object.assign(new Error(data.error||'request_failed'),{code:data.error});return data}finally{clearTimeout(timer)}}
|
||||
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
||||
async function watchResult(){const deadline=Date.now()+50000;while(Date.now()<deadline){let data;try{data=await request('/setup-status')}catch(_){show('暂时联系不到设备。请保持连接 VolcConvAI 热点,正在重试查询…');await delay(1500);continue}
|
||||
if(data.state==='saved'){show('配置已保存!设备将在15秒内重启并进行平台激活。热点随后消失属于正常现象,请查看设备屏幕结果。');button.textContent='配置已保存';return}
|
||||
if(data.state==='wifi_failed'||data.state==='save_failed'){setBusy(false);show(messages[data.state],true);return}
|
||||
if(data.state==='idle'){setBusy(false);show('设备尚未收到连接任务,请检查配置后重新提交。',true);return}
|
||||
show('正在连接家中 Wi-Fi,最多等待约30秒。请保持连接设备热点;暂时断开时请重新连接。');await delay(1500)}setBusy(false);show('暂时无法确认结果。若设备已重启,请查看屏幕;若热点仍在,请重新连接热点并刷新此页查询。',true)}
|
||||
form.addEventListener('submit',async event=>{event.preventDefault();if(busy)return;const id=document.getElementById('device_id');id.value=id.value.trim();if(!form.checkValidity()){show('请完整填写 Wi-Fi 名称、36位 Device ID 和32至128位 Device Secret。',true);form.reportValidity();return}
|
||||
const data={ssid:document.getElementById('ssid').value,password:document.getElementById('password').value,device_id:id.value,device_secret:document.getElementById('device_secret').value};
|
||||
if(new TextEncoder().encode(data.ssid).length>32||new TextEncoder().encode(data.password).length>64){show('Wi-Fi 名称不能超过32字节,密码不能超过64字节(中文占多个字节)。',true);return}
|
||||
setBusy(true);show('正在提交配置…');try{await request('/submit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})}catch(failure){if(failure.code&&failure.code!=='busy'){setBusy(false);show(messages[failure.code]||'设备拒绝请求,请检查配置后重试。',true);return}show('正在确认设备是否收到配置…')}await watchResult()});
|
||||
scan.addEventListener('click',async()=>{if(busy)return;scan.disabled=true;button.disabled=true;networks.textContent='正在扫描附近的2.4 GHz Wi-Fi…';try{const data=await request('/scan',{},15000);networks.textContent=data.length?'点击选择网络:':'未找到网络,也可以手动填写。';data.forEach(item=>{const link=document.createElement('a');link.href='#';link.textContent=item.ssid+' ('+item.rssi+' dBm)';link.addEventListener('click',event=>{event.preventDefault();document.getElementById('ssid').value=item.ssid});networks.appendChild(link)})}catch(_){networks.textContent='扫描失败,可以手动填写 Wi-Fi 名称后连接。'}finally{scan.disabled=busy;button.disabled=busy}});
|
||||
async function restoreStatus(){try{const data=await request('/setup-status');if(busy)return;if(data.state==='connecting'||data.state==='saved'){setBusy(true);await watchResult()}else if(messages[data.state])show(messages[data.state],true)}catch(_){if(!busy)show('请确认手机仍连接设备热点,再刷新此页。',true)}}
|
||||
restoreStatus();
|
||||
</script></body></html>
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef __COMMON_H__
|
||||
#define __COMMON_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
const char *key;
|
||||
const char *value;
|
||||
} http_req_header_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "configuration_ap.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_event.h>
|
||||
#include <esp_mac.h>
|
||||
#include <esp_netif.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_log.h>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
#include <lwip/ip_addr.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "platform_client.h"
|
||||
#include "cJSON.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define TAG "CONFIGURATION_AP"
|
||||
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
#define WEB_SERVER_URL "http://192.168.4.1"
|
||||
|
||||
extern const char index_html_start[] asm("_binary_wifi_configuration_ap_html_start");
|
||||
ap_t ap;
|
||||
typedef enum { SETUP_IDLE, SETUP_CONNECTING, SETUP_SAVED, SETUP_WIFI_FAILED, SETUP_SAVE_FAILED } setup_state_t;
|
||||
static setup_state_t setup_state;
|
||||
static portMUX_TYPE setup_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static setup_state_t get_setup_state(void) {
|
||||
portENTER_CRITICAL(&setup_mux);
|
||||
setup_state_t state = setup_state;
|
||||
portEXIT_CRITICAL(&setup_mux);
|
||||
return state;
|
||||
}
|
||||
static void set_setup_state(setup_state_t state) {
|
||||
portENTER_CRITICAL(&setup_mux);
|
||||
setup_state = state;
|
||||
portEXIT_CRITICAL(&setup_mux);
|
||||
}
|
||||
static esp_err_t send_json(httpd_req_t *r, const char *status, const char *body) {
|
||||
httpd_resp_set_status(r, status);
|
||||
httpd_resp_set_type(r, "application/json; charset=utf-8");
|
||||
httpd_resp_set_hdr(r, "Cache-Control", "no-store");
|
||||
return httpd_resp_send(r, body, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
|
||||
void start_access_point()
|
||||
{
|
||||
// Get the SSID
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
snprintf(ap.ssid, 32, "%s-%02X%02X%02X", "VolcConvAI", mac[3], mac[4], mac[5]);
|
||||
// Initialize the TCP/IP stack
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
// Create the default event loop
|
||||
esp_netif_t *netif = esp_netif_create_default_wifi_ap();
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
// Set the router IP address to 192.168.4.1
|
||||
esp_netif_ip_info_t ip_info;
|
||||
IP4_ADDR(&ip_info.ip, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.gw, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
|
||||
esp_netif_dhcps_stop(netif);
|
||||
esp_netif_set_ip_info(netif, &ip_info);
|
||||
esp_netif_dhcps_start(netif);
|
||||
|
||||
// Initialize the WiFi stack in Access Point mode
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
// Set the WiFi configuration
|
||||
wifi_config_t wifi_config = {};
|
||||
strncpy((char *)wifi_config.ap.ssid, ap.ssid, sizeof(wifi_config.ap.ssid));
|
||||
wifi_config.ap.ssid_len = strlen(ap.ssid);
|
||||
wifi_config.ap.max_connection = 4;
|
||||
wifi_config.ap.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
strncpy((char *)wifi_config.ap.password, ap.ssid, sizeof(wifi_config.ap.password) - 1);
|
||||
wifi_config.ap.password[sizeof(wifi_config.ap.password) - 1] = '\0';
|
||||
|
||||
// Start the WiFi Access Point
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "Access Point started with SSID %s", ap.ssid);
|
||||
}
|
||||
|
||||
esp_err_t handler_index_html(httpd_req_t *r)
|
||||
{
|
||||
httpd_resp_set_type(r, "text/html; charset=utf-8");
|
||||
httpd_resp_set_hdr(r, "Cache-Control", "no-store");
|
||||
httpd_resp_send(r, index_html_start, strlen(index_html_start));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t handler_scan(httpd_req_t *r)
|
||||
{
|
||||
if (get_setup_state() == SETUP_CONNECTING || get_setup_state() == SETUP_SAVED)
|
||||
return send_json(r, "409 Conflict", "{\"error\":\"busy\"}");
|
||||
if (esp_wifi_scan_start(NULL, true) != ESP_OK)
|
||||
return send_json(r, "503 Service Unavailable", "{\"error\":\"scan_failed\"}");
|
||||
uint16_t count = 0;
|
||||
esp_wifi_scan_get_ap_num(&count);
|
||||
if (count > 32) count = 32;
|
||||
wifi_ap_record_t *records = count ? calloc(count, sizeof(*records)) : NULL;
|
||||
if (count && !records) {
|
||||
esp_wifi_clear_ap_list();
|
||||
return send_json(r, "503 Service Unavailable", "{\"error\":\"no_memory\"}");
|
||||
}
|
||||
if (count && esp_wifi_scan_get_ap_records(&count, records) != ESP_OK) {
|
||||
free(records);
|
||||
esp_wifi_clear_ap_list();
|
||||
return send_json(r, "503 Service Unavailable", "{\"error\":\"scan_failed\"}");
|
||||
}
|
||||
esp_wifi_clear_ap_list();
|
||||
cJSON *list = cJSON_CreateArray();
|
||||
for (int i = 0; list && i < count; i++) {
|
||||
cJSON *item = cJSON_CreateObject();
|
||||
if (!item) break;
|
||||
cJSON_AddStringToObject(item, "ssid", (const char *)records[i].ssid);
|
||||
cJSON_AddNumberToObject(item, "rssi", records[i].rssi);
|
||||
cJSON_AddNumberToObject(item, "authmode", records[i].authmode);
|
||||
cJSON_AddItemToArray(list, item);
|
||||
}
|
||||
free(records);
|
||||
char *body = list ? cJSON_PrintUnformatted(list) : NULL;
|
||||
cJSON_Delete(list);
|
||||
esp_err_t ret = body ? send_json(r, "200 OK", body) :
|
||||
send_json(r, "503 Service Unavailable", "{\"error\":\"no_memory\"}");
|
||||
free(body);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool connect2wifi(char *ssid, char *password, EventGroupHandle_t event_group)
|
||||
{
|
||||
xEventGroupClearBits(event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT);
|
||||
if (esp_wifi_disconnect() == ESP_OK) {
|
||||
xEventGroupWaitBits(event_group, WIFI_FAIL_BIT, pdTRUE, pdFALSE, pdMS_TO_TICKS(1000));
|
||||
}
|
||||
xEventGroupClearBits(event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT);
|
||||
// auto esp_netif = esp_netif_create_default_wifi_sta();
|
||||
|
||||
wifi_config_t wifi_config;
|
||||
bzero(&wifi_config, sizeof(wifi_config));
|
||||
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid));
|
||||
strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password));
|
||||
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
||||
wifi_config.sta.failure_retry_cnt = 1;
|
||||
|
||||
if (esp_wifi_set_config(WIFI_IF_STA, &wifi_config) != ESP_OK) return false;
|
||||
esp_err_t ret = esp_wifi_connect();
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to connect to WiFi: %d", ret);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG, "Connecting to WiFi %s", ssid);
|
||||
|
||||
// Require an IP address, with a bounded wait so the form can be retried.
|
||||
EventBits_t bits = xEventGroupWaitBits(event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdTRUE, pdFALSE, pdMS_TO_TICKS(30000));
|
||||
if (bits & WIFI_CONNECTED_BIT)
|
||||
{
|
||||
ESP_LOGI(TAG, "Connected to WiFi %s", ssid);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to connect to WiFi %s", ssid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
char ssid[33];
|
||||
char password[65];
|
||||
char device_id[37];
|
||||
char device_secret[129];
|
||||
} setup_input_t;
|
||||
|
||||
static esp_err_t save_setup(const setup_input_t *input)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t ret = nvs_open("wifi", NVS_READWRITE, &nvs);
|
||||
if (ret != ESP_OK) return ret;
|
||||
// Keep recovery available if persisting either namespace fails.
|
||||
ret = nvs_set_u8(nvs, "setup_requested", 1);
|
||||
if (ret == ESP_OK) ret = nvs_commit(nvs);
|
||||
if (ret == ESP_OK) ret = platform_identity_save(input->device_id, input->device_secret);
|
||||
if (ret == ESP_OK) ret = nvs_set_str(nvs, "ssid", input->ssid);
|
||||
if (ret == ESP_OK) ret = nvs_set_str(nvs, "password", input->password);
|
||||
if (ret == ESP_OK) ret = nvs_set_u8(nvs, "setup_requested", 0);
|
||||
if (ret == ESP_OK) ret = nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void setup_worker(void *arg)
|
||||
{
|
||||
setup_input_t *input = arg;
|
||||
if (!connect2wifi(input->ssid, input->password, ap.event_group)) {
|
||||
set_setup_state(SETUP_WIFI_FAILED);
|
||||
} else {
|
||||
esp_err_t ret = save_setup(input);
|
||||
ESP_LOGI(TAG, "Setup save result: %s", esp_err_to_name(ret));
|
||||
set_setup_state(ret == ESP_OK ? SETUP_SAVED : SETUP_SAVE_FAILED);
|
||||
}
|
||||
memset(input, 0, sizeof(*input));
|
||||
free(input);
|
||||
if (get_setup_state() == SETUP_SAVED) {
|
||||
// Leave time for the browser to retrieve the result before AP shutdown.
|
||||
vTaskDelay(pdMS_TO_TICKS(15000));
|
||||
esp_restart();
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static bool read_field(cJSON *json, const char *name, char *out, size_t capacity, size_t min)
|
||||
{
|
||||
cJSON *value = cJSON_GetObjectItemCaseSensitive(json, name);
|
||||
if (!cJSON_IsString(value) || !value->valuestring) return false;
|
||||
size_t length = strlen(value->valuestring);
|
||||
if (length < min || length >= capacity) return false;
|
||||
memcpy(out, value->valuestring, length + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t handler_setup_status(httpd_req_t *r)
|
||||
{
|
||||
const char *states[] = {"idle", "connecting", "saved", "wifi_failed", "save_failed"};
|
||||
char body[64];
|
||||
snprintf(body, sizeof(body), "{\"state\":\"%s\"}", states[get_setup_state()]);
|
||||
return send_json(r, "200 OK", body);
|
||||
}
|
||||
|
||||
esp_err_t handler_form_submission(httpd_req_t *r)
|
||||
{
|
||||
if (get_setup_state() == SETUP_CONNECTING || get_setup_state() == SETUP_SAVED)
|
||||
return send_json(r, "409 Conflict", "{\"error\":\"busy\"}");
|
||||
if (r->content_len <= 0 || r->content_len > 1536)
|
||||
return send_json(r, "400 Bad Request", "{\"error\":\"invalid_fields\"}");
|
||||
char *body = calloc(1, r->content_len + 1);
|
||||
if (!body) return send_json(r, "503 Service Unavailable", "{\"error\":\"no_memory\"}");
|
||||
size_t received = 0;
|
||||
while (received < r->content_len) {
|
||||
int count = httpd_req_recv(r, body + received, r->content_len - received);
|
||||
if (count <= 0) {
|
||||
memset(body, 0, r->content_len);
|
||||
free(body);
|
||||
return send_json(r, "408 Request Timeout", "{\"error\":\"receive_timeout\"}");
|
||||
}
|
||||
received += count;
|
||||
}
|
||||
cJSON *json = cJSON_Parse(body);
|
||||
memset(body, 0, r->content_len);
|
||||
free(body);
|
||||
setup_input_t *input = calloc(1, sizeof(*input));
|
||||
bool valid = input && cJSON_IsObject(json) &&
|
||||
read_field(json, "ssid", input->ssid, sizeof(input->ssid), 1) &&
|
||||
read_field(json, "password", input->password, sizeof(input->password), 0) &&
|
||||
read_field(json, "device_id", input->device_id, sizeof(input->device_id), 36) &&
|
||||
read_field(json, "device_secret", input->device_secret, sizeof(input->device_secret), 32);
|
||||
cJSON_Delete(json);
|
||||
if (!valid) {
|
||||
if (input) { memset(input, 0, sizeof(*input)); free(input); }
|
||||
ESP_LOGW(TAG, "Setup submission rejected: invalid fields");
|
||||
return send_json(r, "400 Bad Request", "{\"error\":\"invalid_fields\"}");
|
||||
}
|
||||
set_setup_state(SETUP_CONNECTING);
|
||||
if (xTaskCreate(setup_worker, "setup_connect", 6144, input, 5, NULL) != pdPASS) {
|
||||
memset(input, 0, sizeof(*input));
|
||||
free(input);
|
||||
set_setup_state(SETUP_IDLE);
|
||||
return send_json(r, "503 Service Unavailable", "{\"error\":\"no_memory\"}");
|
||||
}
|
||||
ESP_LOGI(TAG, "Setup submission accepted; connecting in background");
|
||||
return send_json(r, "202 Accepted", "{\"state\":\"connecting\"}");
|
||||
}
|
||||
|
||||
void start_web_server()
|
||||
{
|
||||
// Start the web server
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
ESP_ERROR_CHECK(httpd_start(&(ap.server), &config));
|
||||
|
||||
// Register the index.html file
|
||||
httpd_uri_t index_html = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = handler_index_html,
|
||||
.user_ctx = NULL};
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(ap.server, &index_html));
|
||||
|
||||
// Register the /scan URI
|
||||
httpd_uri_t scan = {
|
||||
.uri = "/scan",
|
||||
.method = HTTP_GET,
|
||||
.handler = handler_scan,
|
||||
.user_ctx = NULL};
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(ap.server, &scan));
|
||||
|
||||
// Register the form submission
|
||||
httpd_uri_t form_submit = {
|
||||
.uri = "/submit",
|
||||
.method = HTTP_POST,
|
||||
.handler = handler_form_submission,
|
||||
.user_ctx = NULL};
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(ap.server, &form_submit));
|
||||
|
||||
httpd_uri_t status = { .uri = "/setup-status", .method = HTTP_GET,
|
||||
.handler = handler_setup_status };
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(ap.server, &status));
|
||||
ESP_LOGI(TAG, "Web server started");
|
||||
}
|
||||
|
||||
void handler_event_wifi_ap(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED)
|
||||
{
|
||||
wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "Station " MACSTR " joined, AID=%d", MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
else if (event_id == WIFI_EVENT_AP_STADISCONNECTED)
|
||||
{
|
||||
wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "Station " MACSTR " left, AID=%d", MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
else if (event_id == WIFI_EVENT_STA_CONNECTED)
|
||||
{
|
||||
// Association alone does not mean DHCP has completed.
|
||||
}
|
||||
else if (event_id == WIFI_EVENT_STA_DISCONNECTED)
|
||||
{
|
||||
xEventGroupSetBits(ap.event_group, WIFI_FAIL_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
void handler_event_ip_ap(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_id == IP_EVENT_STA_GOT_IP)
|
||||
{
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
|
||||
xEventGroupSetBits(ap.event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
void configure_ap()
|
||||
{
|
||||
ap.ssid[0] = '\0';
|
||||
ap.event_group = xEventGroupCreate();
|
||||
// Register event handlers
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
|
||||
ESP_EVENT_ANY_ID,
|
||||
&handler_event_wifi_ap,
|
||||
NULL,
|
||||
&(ap.instance_any_id)));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
|
||||
IP_EVENT_STA_GOT_IP,
|
||||
&handler_event_ip_ap,
|
||||
NULL,
|
||||
&(ap.instance_got_ip)));
|
||||
|
||||
start_access_point();
|
||||
start_web_server();
|
||||
ESP_LOGI(TAG, "Setup portal ready at " WEB_SERVER_URL);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <esp_http_server.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
httpd_handle_t server;
|
||||
EventGroupHandle_t event_group;
|
||||
esp_event_handler_instance_t instance_any_id;
|
||||
esp_event_handler_instance_t instance_got_ip;
|
||||
char ssid[32];
|
||||
} ap_t;
|
||||
|
||||
void configure_ap();
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <inttypes.h>
|
||||
#include "esp_board_manager.h"
|
||||
#include "dev_lcd_touch_i2c.h"
|
||||
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_heap_task_info.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_sntp.h"
|
||||
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_err.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
// #include "periph_wifi.h"
|
||||
// #include "fatfs_stream.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "network.h"
|
||||
#include "volc_manager_service.h"
|
||||
#include "volc_service_common.h"
|
||||
#include "volc_conv_service.h"
|
||||
#include "local_logic_service.h"
|
||||
#include "volc_function_call_service.h"
|
||||
|
||||
#include "volc_hal.h"
|
||||
#include "volc_hal_display.h"
|
||||
#include "volc_hal_capture.h"
|
||||
#include "volc_lvgl_source.h"
|
||||
// #include "board.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_gmf_afe.h"
|
||||
#include "mmap_generate_eaf.h"
|
||||
#include "platform_client.h"
|
||||
#include "hal/usb_serial_jtag_ll.h"
|
||||
#define STATS_TASK_PRIO 5
|
||||
|
||||
static const char *TAG = "VolcConvAI";
|
||||
static volatile bool is_interrupt = false;
|
||||
static volatile bool conversation_service_ready = false;
|
||||
extern void conv_ai_service_diagnostic_say(void);
|
||||
|
||||
/* A local USB diagnostic command lets service tests run without repeated taps.
|
||||
* It does not run automatically or accept commands over the network. */
|
||||
static void usb_diagnostic_task(void *arg)
|
||||
{
|
||||
char command[32];
|
||||
size_t length = 0;
|
||||
while (true) {
|
||||
while (usb_serial_jtag_ll_rxfifo_data_available()) {
|
||||
uint8_t ch;
|
||||
if (!usb_serial_jtag_ll_read_rxfifo(&ch, 1)) break;
|
||||
if (ch == '\n' || ch == '\r') {
|
||||
command[length] = 0;
|
||||
if (conversation_service_ready && !strcmp(command, "diag-say")) {
|
||||
ESP_LOGI(TAG, "USB diagnostic: requesting fixed spoken sentence");
|
||||
conv_ai_service_diagnostic_say();
|
||||
}
|
||||
length = 0;
|
||||
} else if (length < sizeof(command) - 1) {
|
||||
command[length++] = ch;
|
||||
} else {
|
||||
length = 0;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
}
|
||||
}
|
||||
|
||||
void session_init(){
|
||||
volc_service_manager_init();
|
||||
volc_conv_service_manager_init();
|
||||
function_call_service_init();
|
||||
local_logic_service_init();
|
||||
}
|
||||
|
||||
void Task(void* data){
|
||||
aios_init(VOLC_SERVICE_EVENT_MAX); // AIOS初始化
|
||||
session_init();
|
||||
conversation_service_ready = true;
|
||||
xTaskCreate(usb_diagnostic_task, "usb_diagnostic", 3072, NULL, 3, NULL);
|
||||
aios_run(); // AIOS启动
|
||||
return;
|
||||
}
|
||||
|
||||
static void sys_monitor_task(void *pvParameters)
|
||||
{
|
||||
static char run_info[1024] = {0};
|
||||
while (1)
|
||||
{
|
||||
// vTaskGetRunTimeStats(run_info);
|
||||
vTaskList(run_info);
|
||||
printf("Task Runtime Stats:\n%s", run_info);
|
||||
printf("--------------------------------\n");
|
||||
// ESP_LOGI(TAG, "Task Runtime Stats:\n%s", run_info);
|
||||
// ESP_LOGI(TAG, "--------------------------------\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
}
|
||||
}
|
||||
|
||||
void initialize_sntp(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing SNTP");
|
||||
|
||||
// 设置SNTP操作模式为轮询(客户端)
|
||||
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
|
||||
// 设置NTP服务器(可以设置多个备用)
|
||||
esp_sntp_setservername(0, "pool.ntp.org"); // 主服务器
|
||||
esp_sntp_setservername(1, "cn.pool.ntp.org"); // 备用服务器:中国区
|
||||
esp_sntp_setservername(2, "ntp1.aliyun.com"); // 备用服务器:阿里云
|
||||
|
||||
// 初始化SNTP服务
|
||||
esp_sntp_init();
|
||||
|
||||
// 设置时区(例如北京时间CST-8)
|
||||
setenv("TZ", "CST-8", 1);
|
||||
tzset();
|
||||
}
|
||||
|
||||
void wait_for_time_sync(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Waiting for time synchronization...");
|
||||
|
||||
// 检查时间是否已同步(1970年之后)
|
||||
time_t now = 0;
|
||||
struct tm timeinfo = {0};
|
||||
int retry_count = 5; // 最大重试次数
|
||||
|
||||
while (timeinfo.tm_year < (2024 - 1900) && retry_count > 0)
|
||||
{
|
||||
ESP_LOGI(TAG, "Time not set yet, retrying... (%d)", retry_count);
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
time(&now);
|
||||
localtime_r(&now, &timeinfo);
|
||||
retry_count--;
|
||||
}
|
||||
|
||||
if (retry_count > 0)
|
||||
{
|
||||
// print_current_time();
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to synchronize time within timeout period");
|
||||
}
|
||||
}
|
||||
|
||||
static void rec_engine_cb(void *event, void *user_data)
|
||||
{
|
||||
esp_gmf_afe_evt_t * e = (esp_gmf_afe_evt_t *)event;
|
||||
|
||||
if (ESP_GMF_AFE_EVT_WAKEUP_START == e->type) {
|
||||
if (conversation_service_ready) {
|
||||
ESP_LOGI(TAG, "Wake word detected; requesting conversation");
|
||||
volc_hal_context_t *ctx = volc_get_global_hal_context();
|
||||
if (ctx && ctx->display_handle) {
|
||||
int index = MMAP_EAF_EMOTION_BLINK_FAST_284_126_EAF;
|
||||
volc_hal_display_set_content(ctx->display_handle, VOLC_DISPLAY_OBJ_MAIN,
|
||||
VOLC_DISPLAY_IMAGE, &index);
|
||||
}
|
||||
aios_event_pub(VOLC_SERVICE_AI_CONVERSATION,NULL,NULL);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
static void wifi_setup_touch_task(void *arg)
|
||||
{
|
||||
dev_lcd_touch_i2c_handles_t *touch = arg;
|
||||
TickType_t pressed_at = 0;
|
||||
TickType_t last_conversation_request = 0;
|
||||
uint16_t pressed_x = 0;
|
||||
uint16_t pressed_y = 0;
|
||||
bool holding = false;
|
||||
bool setup_triggered = false;
|
||||
for (;;) {
|
||||
uint16_t x = 0, y = 0;
|
||||
uint8_t count = 0;
|
||||
bool pressed = esp_lcd_touch_read_data(touch->touch_handle) == ESP_OK &&
|
||||
esp_lcd_touch_get_coordinates(touch->touch_handle, &x, &y, NULL, &count, 1) &&
|
||||
count > 0;
|
||||
if (!pressed) {
|
||||
if (holding && !setup_triggered) {
|
||||
TickType_t held_for = xTaskGetTickCount() - pressed_at;
|
||||
ESP_LOGI(TAG, "Touch released at (%u,%u) after %u ms",
|
||||
pressed_x, pressed_y, (unsigned)(held_for * portTICK_PERIOD_MS));
|
||||
if (held_for >= pdMS_TO_TICKS(50) &&
|
||||
held_for < pdMS_TO_TICKS(1500) && conversation_service_ready) {
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
if (last_conversation_request != 0 &&
|
||||
now - last_conversation_request < pdMS_TO_TICKS(5000)) {
|
||||
ESP_LOGI(TAG, "Ignoring repeated conversation tap");
|
||||
holding = false;
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
continue;
|
||||
}
|
||||
last_conversation_request = now;
|
||||
ESP_LOGI(TAG, "Screen tapped; requesting conversation");
|
||||
volc_hal_context_t *ctx = volc_get_global_hal_context();
|
||||
if (ctx && ctx->display_handle) {
|
||||
int index = MMAP_EAF_EMOTION_BLINK_FAST_284_126_EAF;
|
||||
volc_hal_display_set_content(ctx->display_handle, VOLC_DISPLAY_OBJ_MAIN,
|
||||
VOLC_DISPLAY_IMAGE, &index);
|
||||
}
|
||||
aios_event_pub(VOLC_SERVICE_AI_CONVERSATION, NULL, NULL);
|
||||
}
|
||||
}
|
||||
holding = false;
|
||||
setup_triggered = false;
|
||||
} else if (!holding) {
|
||||
holding = true;
|
||||
pressed_at = xTaskGetTickCount();
|
||||
pressed_x = x;
|
||||
pressed_y = y;
|
||||
} else if (!setup_triggered && pressed_y >= 280 && pressed_y <= 335 &&
|
||||
pressed_x >= 60 && pressed_x <= 300 &&
|
||||
(TickType_t)(xTaskGetTickCount() - pressed_at) >= pdMS_TO_TICKS(3000)) {
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t ret = nvs_open("wifi", NVS_READWRITE, &nvs);
|
||||
if (ret == ESP_OK) {
|
||||
ret = nvs_set_u8(nvs, "setup_requested", 1);
|
||||
if (ret == ESP_OK) ret = nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
}
|
||||
if (ret == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Restarting into setup; device identity and Wi-Fi preserved");
|
||||
setup_triggered = true;
|
||||
esp_restart();
|
||||
}
|
||||
ESP_LOGE(TAG, "Cannot request setup: %s", esp_err_to_name(ret));
|
||||
holding = false;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_setup_button_init(void)
|
||||
{
|
||||
dev_lcd_touch_i2c_handles_t *touch = NULL;
|
||||
esp_err_t ret = esp_board_manager_get_device_handle("lcd_touch", (void **)&touch);
|
||||
if (ret != ESP_OK || !touch || !touch->touch_handle) {
|
||||
ESP_LOGE(TAG, "Wi-Fi setup touch input unavailable");
|
||||
return;
|
||||
}
|
||||
if (xTaskCreate(wifi_setup_touch_task, "wifi_setup", 4096, touch, 5, NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "Cannot start Wi-Fi setup touch task");
|
||||
}
|
||||
}
|
||||
|
||||
void hal_level_init()
|
||||
{
|
||||
|
||||
volc_hal_context_t* g_hal_context = volc_get_global_hal_context();
|
||||
volc_hal_capture_config_t config = {0};
|
||||
config.media_type = VOLC_MEDIA_TYPE_AUDIO;
|
||||
config.data_cb = NULL;
|
||||
config.user_data = g_hal_context;
|
||||
config.audio_wakeup_cb = (volc_hal_audio_wakeup_cb_t)rec_engine_cb;
|
||||
volc_hal_capture_t capture = volc_hal_capture_create(&config);
|
||||
|
||||
volc_hal_capture_start(capture,VOLC_AUDIO_MODE_WAKEUP);
|
||||
|
||||
|
||||
// Allow other core to finish initialization
|
||||
// vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
}
|
||||
|
||||
static void application_boot(void)
|
||||
{
|
||||
// xTaskCreate(&sys_monitor_task, "sys_monitor_task", 4096, NULL, 5, NULL);
|
||||
volc_hal_init();
|
||||
volc_hal_display_config_t display_config = {0};
|
||||
volc_hal_display_create(&display_config);
|
||||
|
||||
volc_hal_context_t* g_hal_context = volc_get_global_hal_context();
|
||||
if(g_hal_context == NULL){
|
||||
return;
|
||||
}
|
||||
volc_hal_display_t global_display = g_hal_context->display_handle;
|
||||
|
||||
/* Initialize the default event loop */
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_log_level_set("*", ESP_LOG_WARN);
|
||||
esp_log_level_set("*", ESP_LOG_INFO);
|
||||
|
||||
// /* Initialize NVS flash for WiFi configuration */
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
|
||||
{
|
||||
ESP_LOGW(TAG, "Erasing NVS flash to fix corruption");
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
ESP_ERROR_CHECK(platform_client_init());
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
wifi_setup_button_init();
|
||||
|
||||
uint8_t station_mac[6] = {0};
|
||||
char setup_status[96] = "等待配网";
|
||||
if (esp_read_mac(station_mac, ESP_MAC_WIFI_STA) == ESP_OK) {
|
||||
snprintf(setup_status, sizeof(setup_status),
|
||||
"WiFi: VolcConvAI-%02X%02X%02X",
|
||||
station_mac[3], station_mac[4], station_mac[5]);
|
||||
}
|
||||
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS,
|
||||
VOLC_DISPLAY_TEXT, setup_status);
|
||||
|
||||
bool connected = configure_network();
|
||||
if (connected == false)
|
||||
{
|
||||
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS,
|
||||
VOLC_DISPLAY_TEXT, setup_status);
|
||||
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_SUBTITLE,
|
||||
VOLC_DISPLAY_TEXT, "192.168.4.1 密码同热点名");
|
||||
ESP_LOGI(TAG, "Waiting for setup through the Wi-Fi portal");
|
||||
return;
|
||||
}
|
||||
initialize_sntp();
|
||||
wait_for_time_sync();
|
||||
if (!platform_identity_is_configured()) {
|
||||
ESP_LOGE(TAG, "Platform identity is missing; erase Wi-Fi NVS or use the setup portal");
|
||||
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS, VOLC_DISPLAY_TEXT,
|
||||
"请通过配网页填写平台设备凭据");
|
||||
return;
|
||||
}
|
||||
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS, VOLC_DISPLAY_TEXT,
|
||||
"网络已连接,正在激活");
|
||||
ret = platform_provision_volc_credential();
|
||||
ESP_LOGI(TAG, "Startup stack headroom after activation: %u bytes", (unsigned)uxTaskGetStackHighWaterMark(NULL));
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Platform provider provisioning failed: %s", esp_err_to_name(ret));
|
||||
const char *code = platform_last_error_code();
|
||||
const char *message = "平台激活失败,请检查配置";
|
||||
if (strcmp(code, "DEVICE_ACTIVATION_REQUIRED") == 0) message = "请先在平台绑定家庭和长者";
|
||||
else if (strcmp(code, "HARDWARE_AGENT_BINDING_REQUIRED") == 0) message = "请在平台配置硬件智能体";
|
||||
else if (strcmp(code, "HARDWARE_AGENT_PRODUCT_MISMATCH") == 0) message = "平台产品配置不一致";
|
||||
else if (strcmp(code, "HARDWARE_AGENT_HARDWARE_MISMATCH") == 0) message = "设备凭据与本机不匹配";
|
||||
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS, VOLC_DISPLAY_TEXT, message);
|
||||
return;
|
||||
}
|
||||
hal_level_init();
|
||||
ESP_LOGI(TAG, "Startup stack headroom after audio: %u bytes", (unsigned)uxTaskGetStackHighWaterMark(NULL));
|
||||
xTaskCreate(&Task, "aios_task", 8192, NULL, 5, NULL);
|
||||
int index = MMAP_EAF_HAPPY_EAF;
|
||||
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_MAIN,VOLC_DISPLAY_IMAGE,&index);
|
||||
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_STATUS,VOLC_DISPLAY_TEXT,"轻触屏幕或说嗨,乐鑫");
|
||||
}
|
||||
|
||||
/* Network signing/TLS and audio startup exceed the default 4 KiB main stack.
|
||||
* Keep this memory available only during startup and free it on every exit. */
|
||||
static void application_boot_task(void *arg)
|
||||
{
|
||||
application_boot();
|
||||
ESP_LOGI(TAG, "Startup task finished; stack headroom: %u bytes",
|
||||
(unsigned)uxTaskGetStackHighWaterMark(NULL));
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
if (xTaskCreate(application_boot_task, "platform_boot", 24 * 1024,
|
||||
NULL, 5, NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "Cannot allocate platform startup task");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: '>=5.5.0'
|
||||
# espressif/esp_board_manager: ^0.4.0
|
||||
|
||||
# # Put list of dependencies here
|
||||
# # For components maintained by Espressif:
|
||||
# component: "~1.0.0"
|
||||
# # For 3rd party components:
|
||||
# username/component: ">=1.0.0,<2.0.0"
|
||||
# username2/component2:
|
||||
# version: "~1.0.0"
|
||||
# # For transient dependencies `public` flag can be set.
|
||||
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
||||
volc_conv_ai:
|
||||
path: ../components/volc_conv_ai
|
||||
public: true
|
||||
application:
|
||||
path: ../components/application
|
||||
public: true
|
||||
espressif/button: =4.*
|
||||
espressif/esp_hosted:
|
||||
version: ~1.1
|
||||
rules:
|
||||
- if: target in [esp32p4]
|
||||
|
||||
espressif/esp_wifi_remote:
|
||||
matches:
|
||||
- if: idf_version <=5.4.0 && target in [esp32p4]
|
||||
version: ~0.4
|
||||
- if: idf_version >5.4.0 && target in [esp32p4]
|
||||
version: ~0.6
|
||||
|
||||
lvgl/lvgl: 9.2.*
|
||||
|
||||
espressif/esp_lcd_st77916: ^1.0.1
|
||||
|
||||
espressif/esp_lcd_touch_cst816s:
|
||||
version: ^1.1.0
|
||||
public: true
|
||||
|
||||
# Keep the versions used by the upstream EchoEar example. Newer releases
|
||||
# changed LVGL color-format and board-manager Kconfig symbols.
|
||||
espressif/esp_lvgl_port:
|
||||
version: "2.6.0"
|
||||
public: true
|
||||
espressif/zlib: '*'
|
||||
|
||||
espressif2022/esp_emote_gfx:
|
||||
version: "^1.2.0~1"
|
||||
public: true
|
||||
|
||||
espressif2022/esp_vocat_base_control: ^1.0.0
|
||||
|
||||
espressif/esp_mmap_assets:
|
||||
version: "*"
|
||||
public: true
|
||||
|
||||
gmf_core:
|
||||
override_path: ../components/esp-gmf/gmf_core
|
||||
gmf_audio:
|
||||
override_path: ../components/esp-gmf/elements/gmf_audio
|
||||
esp_board_manager:
|
||||
version: "0.4.0"
|
||||
public: true
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @brief This file was generated by esp_mmap_assets, don't modify it
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_mmap_assets.h"
|
||||
|
||||
#define MMAP_EAF_FILES 7
|
||||
#define MMAP_EAF_CHECKSUM 0x0B63
|
||||
|
||||
enum MMAP_EAF_LISTS {
|
||||
MMAP_EAF_HAPPY_EAF = 0, /*!< Happy.eaf */
|
||||
MMAP_EAF_BOOT_360_360_EAF = 1, /*!< boot_360_360.eaf */
|
||||
MMAP_EAF_EMOTION_ANGRY_284_126_EAF = 2, /*!< emotion_angry_284_126.eaf */
|
||||
MMAP_EAF_EMOTION_BLINK1_284_126_EAF = 3, /*!< emotion_blink1_284_126.eaf */
|
||||
MMAP_EAF_EMOTION_BLINK_FAST_284_126_EAF = 4, /*!< emotion_blink_fast_284_126.eaf */
|
||||
MMAP_EAF_EMOTION_DIZZY_284_126_EAF = 5, /*!< emotion_dizzy_284_126.eaf */
|
||||
MMAP_EAF_EMOTION_SLEEP_284_126_EAF = 6, /*!< emotion_sleep_284_126.eaf */
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "network.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_event.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_log.h>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
#include "configuration_ap.h"
|
||||
#include "platform_client.h"
|
||||
|
||||
#define TAG "NETWORK"
|
||||
|
||||
#define WIFI_EVENT_CONNECTED BIT0
|
||||
#define WIFI_EVENT_FAILED BIT1
|
||||
#define WIFI_EVENT_STARTED BIT2
|
||||
#define MAX_RECONNECT_COUNT 5
|
||||
|
||||
static network_t network;
|
||||
|
||||
int8_t get_rssi()
|
||||
{
|
||||
// Get station info
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK) return 0;
|
||||
return ap_info.rssi;
|
||||
}
|
||||
|
||||
uint8_t get_channel()
|
||||
{
|
||||
// Get station info
|
||||
wifi_ap_record_t ap_info;
|
||||
ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&ap_info));
|
||||
return ap_info.primary;
|
||||
}
|
||||
|
||||
void handler_event_wifi_st(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_id == WIFI_EVENT_STA_START)
|
||||
{
|
||||
xEventGroupSetBits(network.event_group, WIFI_EVENT_STARTED);
|
||||
}
|
||||
else if (event_id == WIFI_EVENT_STA_DISCONNECTED)
|
||||
{
|
||||
xEventGroupClearBits(network.event_group, WIFI_EVENT_CONNECTED);
|
||||
if (network.reconnect_count < MAX_RECONNECT_COUNT)
|
||||
{
|
||||
/* Retry the selected AP first, but allow another AP if it is no longer
|
||||
* reachable. Never permanently bind this device to one mesh node. */
|
||||
if (network.reconnect_count == 2) {
|
||||
wifi_config_t config;
|
||||
if (esp_wifi_get_config(WIFI_IF_STA, &config) == ESP_OK && config.sta.bssid_set) {
|
||||
config.sta.bssid_set = false;
|
||||
config.sta.channel = 0;
|
||||
esp_wifi_set_config(WIFI_IF_STA, &config);
|
||||
ESP_LOGW(TAG, "Preferred AP unavailable; falling back to SSID scan");
|
||||
}
|
||||
}
|
||||
esp_wifi_connect();
|
||||
network.reconnect_count++;
|
||||
ESP_LOGI(TAG, "Reconnecting WiFi (attempt %d)", network.reconnect_count);
|
||||
}
|
||||
else
|
||||
{
|
||||
xEventGroupSetBits(network.event_group, WIFI_EVENT_FAILED);
|
||||
ESP_LOGI(TAG, "WiFi connection failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handler_event_ip_st(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
||||
{
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
network.reconnect_count = 0;
|
||||
|
||||
esp_ip4addr_ntoa(&event->ip_info.ip, network.ip_address, sizeof(network.ip_address));
|
||||
ESP_LOGI(TAG, "Got IP: %s", network.ip_address);
|
||||
xEventGroupSetBits(network.event_group, WIFI_EVENT_CONNECTED);
|
||||
}
|
||||
|
||||
bool is_connected()
|
||||
{
|
||||
return xEventGroupGetBits(network.event_group) & WIFI_EVENT_CONNECTED;
|
||||
}
|
||||
|
||||
bool configure_network()
|
||||
{
|
||||
|
||||
// Create the event group
|
||||
network.ssid[0] = '\0';
|
||||
network.password[0] = '\0';
|
||||
network.reconnect_count = 0;
|
||||
network.ip_address[0] = '\0';
|
||||
network.event_group = xEventGroupCreate();
|
||||
|
||||
// Get ssid and password from NVS
|
||||
nvs_handle_t nvs_handle;
|
||||
uint8_t setup_requested = 0;
|
||||
esp_err_t ret = nvs_open("wifi", NVS_READONLY, &nvs_handle);
|
||||
if (ret == ESP_OK)
|
||||
{
|
||||
nvs_get_u8(nvs_handle, "setup_requested", &setup_requested);
|
||||
size_t length = sizeof(network.ssid);
|
||||
nvs_get_str(nvs_handle, "ssid", network.ssid, &length);
|
||||
length = sizeof(network.password);
|
||||
nvs_get_str(nvs_handle, "password", network.password, &length);
|
||||
nvs_close(nvs_handle);
|
||||
}
|
||||
if (setup_requested || !platform_identity_is_configured())
|
||||
{
|
||||
ESP_LOGI(TAG, "Starting setup: requested or platform identity missing");
|
||||
configure_ap();
|
||||
return false;
|
||||
}
|
||||
// Try to connect to WiFi, if failed, launch the WiFi configuration AP
|
||||
if (network.ssid[0] != '\0')
|
||||
{
|
||||
ESP_LOGI(TAG, "Connecting to WiFi %s", network.ssid);
|
||||
// Initialize the TCP/IP stack
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
esp_event_handler_instance_t instance_any_id;
|
||||
esp_event_handler_instance_t instance_got_ip;
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
|
||||
ESP_EVENT_ANY_ID,
|
||||
&handler_event_wifi_st,
|
||||
NULL,
|
||||
&instance_any_id));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
|
||||
IP_EVENT_STA_GOT_IP,
|
||||
&handler_event_ip_st,
|
||||
NULL,
|
||||
&instance_got_ip));
|
||||
|
||||
// Create the default event loop
|
||||
esp_netif_t *station_netif = esp_netif_create_default_wifi_sta();
|
||||
|
||||
// Initialize the WiFi stack in station mode
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to WiFi ssid=%s password=***", network.ssid);
|
||||
wifi_config_t wifi_config;
|
||||
bzero(&wifi_config, sizeof(wifi_config));
|
||||
strncpy((char *)wifi_config.sta.ssid, network.ssid, sizeof(wifi_config.sta.ssid));
|
||||
strncpy((char *)wifi_config.sta.password, network.password, sizeof(wifi_config.sta.password));
|
||||
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
||||
wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
|
||||
wifi_config.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
|
||||
// Start the WiFi stack
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
||||
xEventGroupWaitBits(network.event_group, WIFI_EVENT_STARTED, pdFALSE, pdFALSE, pdMS_TO_TICKS(5000));
|
||||
wifi_scan_config_t scan = {.ssid = (uint8_t *)network.ssid};
|
||||
if (esp_wifi_scan_start(&scan, true) == ESP_OK) {
|
||||
wifi_ap_record_t records[16];
|
||||
uint16_t count = 16;
|
||||
if (esp_wifi_scan_get_ap_records(&count, records) == ESP_OK && count > 0) {
|
||||
uint16_t best = 0;
|
||||
for (uint16_t i = 1; i < count; ++i) {
|
||||
if (records[i].rssi > records[best].rssi) best = i;
|
||||
}
|
||||
wifi_config.sta.bssid_set = true;
|
||||
memcpy(wifi_config.sta.bssid, records[best].bssid, sizeof(wifi_config.sta.bssid));
|
||||
wifi_config.sta.channel = records[best].primary;
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_LOGI(TAG, "Selected strongest AP among %u: rssi=%d channel=%u bssid=%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
count, records[best].rssi, records[best].primary,
|
||||
records[best].bssid[0], records[best].bssid[1], records[best].bssid[2],
|
||||
records[best].bssid[3], records[best].bssid[4], records[best].bssid[5]);
|
||||
}
|
||||
}
|
||||
ESP_ERROR_CHECK(esp_wifi_connect());
|
||||
|
||||
// Wait for the WiFi stack to start
|
||||
EventBits_t bits = xEventGroupWaitBits(network.event_group, WIFI_EVENT_CONNECTED | WIFI_EVENT_FAILED, pdFALSE, pdFALSE, pdMS_TO_TICKS(30 * 1000));
|
||||
if (!(bits & WIFI_EVENT_CONNECTED))
|
||||
{
|
||||
ESP_LOGE(TAG, "WifiStation failed");
|
||||
// Reset the WiFi stack
|
||||
|
||||
// 取消注册事件处理程序
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip));
|
||||
ESP_ERROR_CHECK(esp_wifi_stop());
|
||||
ESP_ERROR_CHECK(esp_wifi_deinit());
|
||||
esp_netif_destroy_default_wifi(station_netif);
|
||||
xEventGroupClearBits(network.event_group, WIFI_EVENT_CONNECTED | WIFI_EVENT_FAILED);
|
||||
// nvs_flash_erase();
|
||||
// return false;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Connected to %s rssi=%d channel=%d", network.ssid, get_rssi(), get_channel());
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_connected())
|
||||
{
|
||||
configure_ap();
|
||||
ESP_LOGI(TAG, "WiFi not connected, starting AP");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#pragma once
|
||||
#include <esp_http_server.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
EventGroupHandle_t event_group;
|
||||
|
||||
char ssid[32];
|
||||
char password[64];
|
||||
int reconnect_count;
|
||||
char ip_address[16];
|
||||
} network_t;
|
||||
|
||||
bool configure_network();
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "pipeline.h"
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "audio_event_iface.h"
|
||||
#include "audio_common.h"
|
||||
#include "audio_sys.h"
|
||||
#include "board.h"
|
||||
#include "algorithm_stream.h"
|
||||
#include "filter_resample.h"
|
||||
#include "i2s_stream.h"
|
||||
#include "pthread.h"
|
||||
#if (CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_ECHOEAR_V1_2_BOARD)
|
||||
#include "es7210.h"
|
||||
#elif CONFIG_M5STACK_ATOMS3R_BOARD
|
||||
#include "es8311.h"
|
||||
#endif
|
||||
#include "esp_timer.h"
|
||||
#if defined(CONFIG_VOLC_AUDIO_G711A)
|
||||
#include "g711_encoder.h"
|
||||
#include "g711_decoder.h"
|
||||
#endif
|
||||
#include "audio_idf_version.h"
|
||||
#include "raw_stream.h"
|
||||
#define CHANNEL 1
|
||||
static const char *TAG = "AUDIO_PIPELINE";
|
||||
#define I2S_SAMPLE_RATE 16000
|
||||
#define ALGO_SAMPLE_RATE 16000
|
||||
#if (CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_ECHOEAR_V1_2_BOARD)
|
||||
#define ALGORITHM_STREAM_SAMPLE_BIT 32
|
||||
#define CHANNEL_FORMAT I2S_CHANNEL_TYPE_ONLY_LEFT
|
||||
#define ALGORITHM_INPUT_FORMAT "RM"
|
||||
#define CHANNEL_NUM 1
|
||||
#elif CONFIG_M5STACK_ATOMS3R_BOARD
|
||||
#define ALGORITHM_STREAM_SAMPLE_BIT 16
|
||||
#define CHANNEL_FORMAT I2S_CHANNEL_TYPE_RIGHT_LEFT
|
||||
#define ALGORITHM_INPUT_FORMAT "MR"
|
||||
#define CHANNEL_NUM 2
|
||||
#endif
|
||||
#if (CONFIG_VOLC_AUDIO_G711A)
|
||||
#define CODEC_NAME "g711a"
|
||||
#define CODEC_SAMPLE_RATE 8000
|
||||
#else
|
||||
#define CODEC_NAME "pcm"
|
||||
#define CODEC_SAMPLE_RATE 16000
|
||||
#endif
|
||||
|
||||
static audio_element_handle_t create_resample_stream(int src_rate, int src_ch, int dest_rate, int dest_ch)
|
||||
{
|
||||
rsp_filter_cfg_t rsp_cfg = DEFAULT_RESAMPLE_FILTER_CONFIG();
|
||||
rsp_cfg.src_rate = src_rate;
|
||||
rsp_cfg.src_ch = src_ch;
|
||||
rsp_cfg.dest_rate = dest_rate;
|
||||
rsp_cfg.dest_ch = dest_ch;
|
||||
rsp_cfg.complexity = 5;
|
||||
audio_element_handle_t stream = rsp_filter_init(&rsp_cfg);
|
||||
return stream;
|
||||
}
|
||||
|
||||
static audio_element_handle_t create_record_i2s_stream(void)
|
||||
{
|
||||
#if (CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_ECHOEAR_V1_2_BOARD)
|
||||
es7210_adc_set_gain(ES7210_INPUT_MIC3, GAIN_30DB);
|
||||
#elif CONFIG_M5STACK_ATOMS3R_BOARD
|
||||
es8311_set_mic_gain(ES8311_MIC_GAIN_36DB);
|
||||
#endif
|
||||
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(CODEC_ADC_I2S_PORT, I2S_SAMPLE_RATE, ALGORITHM_STREAM_SAMPLE_BIT, AUDIO_STREAM_READER); // 参数需要仔细检查
|
||||
i2s_cfg.type = AUDIO_STREAM_READER;
|
||||
i2s_stream_set_channel_type(&i2s_cfg, CHANNEL_FORMAT);
|
||||
i2s_cfg.std_cfg.clk_cfg.sample_rate_hz = I2S_SAMPLE_RATE;
|
||||
return i2s_stream_init(&i2s_cfg);
|
||||
}
|
||||
static audio_element_handle_t create_record_encoder_stream(void)
|
||||
{
|
||||
#if(CONFIG_VOLC_AUDIO_G711A)
|
||||
g711_encoder_cfg_t g711_cfg = DEFAULT_G711_ENCODER_CONFIG();
|
||||
return g711_encoder_init(&g711_cfg);
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static audio_element_handle_t create_record_raw_stream(void)
|
||||
{
|
||||
audio_element_handle_t raw_stream = NULL;
|
||||
raw_stream_cfg_t raw_cfg = RAW_STREAM_CFG_DEFAULT();
|
||||
raw_cfg.type = AUDIO_STREAM_WRITER;
|
||||
raw_cfg.out_rb_size = 2 * 1024;
|
||||
raw_stream = raw_stream_init(&raw_cfg);
|
||||
audio_element_set_output_timeout(raw_stream, portMAX_DELAY);
|
||||
return raw_stream;
|
||||
}
|
||||
|
||||
static audio_element_handle_t create_record_algo_stream(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "[3.1] Create algorithm stream for aec");
|
||||
algorithm_stream_cfg_t algo_config = ALGORITHM_STREAM_CFG_DEFAULT();
|
||||
algo_config.sample_rate = ALGO_SAMPLE_RATE;
|
||||
algo_config.out_rb_size = 256;
|
||||
algo_config.algo_mask = ALGORITHM_STREAM_DEFAULT_MASK | ALGORITHM_STREAM_USE_AGC;
|
||||
algo_config.input_format = ALGORITHM_INPUT_FORMAT;
|
||||
audio_element_handle_t element_algo = algo_stream_init(&algo_config);
|
||||
audio_element_set_music_info(element_algo, ALGO_SAMPLE_RATE, 1, 16);
|
||||
audio_element_set_input_timeout(element_algo, portMAX_DELAY);
|
||||
return element_algo;
|
||||
}
|
||||
|
||||
recorder_pipeline_handle_t recorder_pipeline_open()
|
||||
{
|
||||
recorder_pipeline_handle_t pipeline = heap_caps_calloc(1, sizeof(recorder_pipeline_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_DEFAULT);
|
||||
esp_log_level_set(TAG, ESP_LOG_INFO);
|
||||
// create and register streams
|
||||
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
|
||||
pipeline->audio_pipeline = audio_pipeline_init(&pipeline_cfg);
|
||||
mem_assert(pipeline->audio_pipeline);
|
||||
pipeline->i2s_stream_reader = create_record_i2s_stream();
|
||||
audio_pipeline_register(pipeline->audio_pipeline, pipeline->i2s_stream_reader, "i2s");
|
||||
pipeline->algo_aec = create_record_algo_stream();
|
||||
audio_pipeline_register(pipeline->audio_pipeline, pipeline->algo_aec, "algo");
|
||||
pipeline->rsp = create_resample_stream(I2S_SAMPLE_RATE, 1, CODEC_SAMPLE_RATE, 1);
|
||||
audio_pipeline_register(pipeline->audio_pipeline, pipeline->rsp, "rsp");
|
||||
pipeline->audio_encoder = create_record_encoder_stream();
|
||||
if (pipeline->audio_encoder) {
|
||||
audio_pipeline_register(pipeline->audio_pipeline, pipeline->audio_encoder, CODEC_NAME);
|
||||
}
|
||||
pipeline->raw_reader = create_record_raw_stream();
|
||||
audio_pipeline_register(pipeline->audio_pipeline, pipeline->raw_reader, "raw");
|
||||
#if (CONFIG_VOLC_AUDIO_G711A)
|
||||
const char *link_tag[] = {"i2s", "algo", "rsp", CODEC_NAME, "raw"};
|
||||
#else
|
||||
const char *link_tag[] = {"i2s", "algo", "raw"};
|
||||
#endif
|
||||
audio_pipeline_link(pipeline->audio_pipeline, &link_tag[0], sizeof(link_tag) / sizeof(link_tag[0]));
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
void recorder_pipeline_close(recorder_pipeline_handle_t pipeline)
|
||||
{
|
||||
audio_pipeline_stop(pipeline->audio_pipeline);
|
||||
audio_pipeline_wait_for_stop(pipeline->audio_pipeline);
|
||||
audio_pipeline_terminate(pipeline->audio_pipeline);
|
||||
if (pipeline->i2s_stream_reader)
|
||||
{
|
||||
audio_pipeline_unregister(pipeline->audio_pipeline, pipeline->i2s_stream_reader);
|
||||
audio_element_deinit(pipeline->i2s_stream_reader);
|
||||
}
|
||||
if (pipeline->audio_encoder) {
|
||||
audio_pipeline_unregister(pipeline->audio_pipeline, pipeline->audio_encoder);
|
||||
audio_element_deinit(pipeline->audio_encoder);
|
||||
}
|
||||
if (pipeline->raw_reader)
|
||||
{
|
||||
audio_pipeline_unregister(pipeline->audio_pipeline, pipeline->raw_reader);
|
||||
audio_element_deinit(pipeline->raw_reader);
|
||||
}
|
||||
if (pipeline->rsp)
|
||||
{
|
||||
audio_pipeline_unregister(pipeline->audio_pipeline, pipeline->rsp);
|
||||
audio_element_deinit(pipeline->rsp);
|
||||
}
|
||||
if (pipeline->algo_aec)
|
||||
{
|
||||
audio_pipeline_unregister(pipeline->audio_pipeline, pipeline->algo_aec);
|
||||
audio_element_deinit(pipeline->algo_aec);
|
||||
}
|
||||
audio_pipeline_deinit(pipeline->audio_pipeline);
|
||||
heap_caps_free(pipeline);
|
||||
};
|
||||
|
||||
void recorder_pipeline_run(recorder_pipeline_handle_t pipeline)
|
||||
{
|
||||
audio_pipeline_run(pipeline->audio_pipeline);
|
||||
};
|
||||
|
||||
int recorder_pipeline_get_default_read_size(recorder_pipeline_handle_t pipeline)
|
||||
{
|
||||
// 60ms
|
||||
#if (CONFIG_VOLC_AUDIO_G711A)
|
||||
return (160 * 3);
|
||||
#else
|
||||
return 1920;
|
||||
#endif
|
||||
};
|
||||
|
||||
audio_element_handle_t recorder_pipeline_get_raw_reader(recorder_pipeline_handle_t pipeline)
|
||||
{
|
||||
return pipeline->raw_reader;
|
||||
};
|
||||
|
||||
audio_pipeline_handle_t recorder_pipeline_get_pipeline(recorder_pipeline_handle_t pipeline)
|
||||
{
|
||||
return pipeline->audio_pipeline;
|
||||
};
|
||||
|
||||
int recorder_pipeline_read(recorder_pipeline_handle_t pipeline, char *buffer, int buf_size)
|
||||
{
|
||||
return raw_stream_read(pipeline->raw_reader, buffer, buf_size);
|
||||
}
|
||||
|
||||
static audio_element_handle_t create_player_raw_stream(void)
|
||||
{
|
||||
raw_stream_cfg_t raw_cfg = RAW_STREAM_CFG_DEFAULT();
|
||||
raw_cfg.type = AUDIO_STREAM_READER;
|
||||
raw_cfg.out_rb_size = 8 * 1024;
|
||||
return raw_stream_init(&raw_cfg);
|
||||
}
|
||||
|
||||
static audio_element_handle_t create_player_i2s_stream(void)
|
||||
{
|
||||
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(I2S_NUM_0, I2S_SAMPLE_RATE, ALGORITHM_STREAM_SAMPLE_BIT, AUDIO_STREAM_WRITER);
|
||||
i2s_cfg.type = AUDIO_STREAM_WRITER;
|
||||
#if (CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_ECHOEAR_V1_2_BOARD)
|
||||
i2s_cfg.need_expand = (16 != 32);
|
||||
#endif
|
||||
i2s_cfg.out_rb_size = 8 * 1024;
|
||||
i2s_cfg.buffer_len = 1416; // 708
|
||||
i2s_stream_set_channel_type(&i2s_cfg, CHANNEL_FORMAT);
|
||||
audio_element_handle_t stream = i2s_stream_init(&i2s_cfg);
|
||||
i2s_stream_set_clk(stream, I2S_SAMPLE_RATE, ALGORITHM_STREAM_SAMPLE_BIT, CHANNEL_NUM);
|
||||
return stream;
|
||||
}
|
||||
|
||||
static audio_element_handle_t create_player_decoder_stream(void)
|
||||
{
|
||||
#if (CONFIG_VOLC_AUDIO_G711A)
|
||||
g711_decoder_cfg_t g711_dec_cfg = DEFAULT_G711_DECODER_CONFIG();
|
||||
g711_dec_cfg.out_rb_size = 8 * 1024;
|
||||
return g711_decoder_init(&g711_dec_cfg);
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
player_pipeline_handle_t player_pipeline_open(void)
|
||||
{
|
||||
player_pipeline_handle_t player_pipeline = heap_caps_calloc(1, sizeof(player_pipeline_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_DEFAULT);
|
||||
esp_log_level_set(TAG, ESP_LOG_INFO);
|
||||
assert(player_pipeline != 0);
|
||||
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
|
||||
player_pipeline->audio_pipeline = audio_pipeline_init(&pipeline_cfg);
|
||||
mem_assert(pipeline);
|
||||
player_pipeline->raw_writer = create_player_raw_stream();
|
||||
audio_pipeline_register(player_pipeline->audio_pipeline, player_pipeline->raw_writer, "raw");
|
||||
player_pipeline->i2s_stream_writer = create_player_i2s_stream();
|
||||
audio_pipeline_register(player_pipeline->audio_pipeline, player_pipeline->i2s_stream_writer, "i2s");
|
||||
player_pipeline->rsp = create_resample_stream(CODEC_SAMPLE_RATE, 1, I2S_SAMPLE_RATE, CHANNEL_NUM);
|
||||
audio_element_set_output_timeout(player_pipeline->rsp, portMAX_DELAY);
|
||||
audio_pipeline_register(player_pipeline->audio_pipeline, player_pipeline->rsp, "rsp");
|
||||
player_pipeline->audio_decoder = create_player_decoder_stream();
|
||||
if (player_pipeline->audio_decoder != NULL) {
|
||||
audio_pipeline_register(player_pipeline->audio_pipeline, player_pipeline->audio_decoder, CODEC_NAME);
|
||||
}
|
||||
#if (CONFIG_VOLC_AUDIO_G711A)
|
||||
const char *link_tag[] = {"raw", CODEC_NAME, "rsp", "i2s"};
|
||||
#else
|
||||
const char *link_tag[] = {"raw", "rsp", "i2s"};
|
||||
#endif
|
||||
audio_pipeline_link(player_pipeline->audio_pipeline, &link_tag[0], sizeof(link_tag) / sizeof(link_tag[0]));
|
||||
return player_pipeline;
|
||||
}
|
||||
|
||||
void player_pipeline_run(player_pipeline_handle_t player_pipeline)
|
||||
{
|
||||
audio_pipeline_run(player_pipeline->audio_pipeline);
|
||||
};
|
||||
|
||||
void player_pipeline_close(player_pipeline_handle_t player_pipeline)
|
||||
{
|
||||
audio_pipeline_stop(player_pipeline->audio_pipeline);
|
||||
audio_pipeline_wait_for_stop(player_pipeline->audio_pipeline);
|
||||
audio_pipeline_terminate(player_pipeline->audio_pipeline);
|
||||
if (player_pipeline->raw_writer)
|
||||
{
|
||||
audio_pipeline_unregister(player_pipeline->audio_pipeline, player_pipeline->raw_writer);
|
||||
audio_element_deinit(player_pipeline->raw_writer);
|
||||
}
|
||||
if (player_pipeline->rsp)
|
||||
{
|
||||
audio_pipeline_unregister(player_pipeline->audio_pipeline, player_pipeline->rsp);
|
||||
audio_element_deinit(player_pipeline->rsp);
|
||||
}
|
||||
if (player_pipeline->i2s_stream_writer)
|
||||
{
|
||||
audio_pipeline_unregister(player_pipeline->audio_pipeline, player_pipeline->i2s_stream_writer);
|
||||
audio_element_deinit(player_pipeline->i2s_stream_writer);
|
||||
}
|
||||
if (player_pipeline->audio_decoder) {
|
||||
audio_pipeline_unregister(player_pipeline->audio_pipeline, player_pipeline->audio_decoder);
|
||||
audio_element_deinit(player_pipeline->audio_decoder);
|
||||
}
|
||||
audio_pipeline_deinit(player_pipeline->audio_pipeline);
|
||||
heap_caps_free(player_pipeline);
|
||||
};
|
||||
|
||||
int player_pipeline_write(player_pipeline_handle_t player_pipeline, char *buffer, int buf_size)
|
||||
{
|
||||
raw_stream_write(player_pipeline->raw_writer, buffer, buf_size);
|
||||
return 0;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef __PIPELINE_H__
|
||||
#define __PIPELINE_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include "audio_pipeline.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
struct recorder_pipeline_t
|
||||
{
|
||||
audio_pipeline_handle_t audio_pipeline;
|
||||
audio_element_handle_t i2s_stream_reader;
|
||||
audio_element_handle_t audio_encoder;
|
||||
audio_element_handle_t raw_reader;
|
||||
audio_element_handle_t rsp;
|
||||
audio_element_handle_t algo_aec;
|
||||
};
|
||||
|
||||
typedef struct recorder_pipeline_t recorder_pipeline_t, *recorder_pipeline_handle_t;
|
||||
recorder_pipeline_handle_t recorder_pipeline_open();
|
||||
void recorder_pipeline_run(recorder_pipeline_handle_t);
|
||||
void recorder_pipeline_close(recorder_pipeline_handle_t);
|
||||
int recorder_pipeline_get_default_read_size(recorder_pipeline_handle_t);
|
||||
int recorder_pipeline_read(recorder_pipeline_handle_t, char *buffer, int buf_size);
|
||||
|
||||
struct player_pipeline_t
|
||||
{
|
||||
audio_pipeline_handle_t audio_pipeline;
|
||||
audio_element_handle_t raw_writer;
|
||||
audio_element_handle_t audio_decoder;
|
||||
audio_element_handle_t rsp;
|
||||
audio_element_handle_t i2s_stream_writer;
|
||||
};
|
||||
typedef struct player_pipeline_t player_pipeline_t, *player_pipeline_handle_t;
|
||||
player_pipeline_handle_t player_pipeline_open();
|
||||
void player_pipeline_run(player_pipeline_handle_t);
|
||||
void player_pipeline_close(player_pipeline_handle_t);
|
||||
int player_pipeline_get_default_read_size(player_pipeline_handle_t);
|
||||
int player_pipeline_write(player_pipeline_handle_t, char *buffer, int buf_size);
|
||||
void player_pipeline_write_play_buffer_flag(player_pipeline_handle_t player_pipeline);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,714 @@
|
||||
#include "platform_client.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_check.h"
|
||||
#include "mbedtls/base64.h"
|
||||
#include "mbedtls/md.h"
|
||||
#include "mbedtls/sha256.h"
|
||||
#include "nvs.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define PLATFORM_API_ORIGIN "https://121.41.46.238"
|
||||
#define PLATFORM_RESPONSE_MAX (32 * 1024)
|
||||
#define PLATFORM_FIRMWARE_VERSION "1.0.0-echoear-platform.1"
|
||||
#define PLATFORM_HARDWARE_AGENT_SDK_VERSION "2.0.0"
|
||||
|
||||
static const char *TAG = "platform_client";
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t length;
|
||||
size_t capacity;
|
||||
} response_buffer_t;
|
||||
|
||||
static char s_device_id[PLATFORM_DEVICE_ID_MAX];
|
||||
static char s_device_secret[PLATFORM_DEVICE_SECRET_MAX];
|
||||
static char s_hardware_id[PLATFORM_HARDWARE_ID_MAX];
|
||||
static char s_last_error_code[80];
|
||||
|
||||
const char *platform_last_error_code(void)
|
||||
{
|
||||
return s_last_error_code;
|
||||
}
|
||||
static platform_volc_credential_t s_volc;
|
||||
static int64_t s_config_revision = 1;
|
||||
static int64_t s_event_sequence = 0;
|
||||
static int64_t s_turn_sequence = 0;
|
||||
static platform_agent_session_t s_active_session;
|
||||
static esp_http_client_handle_t s_http_client;
|
||||
static StaticSemaphore_t s_http_mutex_storage;
|
||||
static SemaphoreHandle_t s_http_mutex;
|
||||
static unsigned s_http_connection_count;
|
||||
static uint32_t s_heartbeat_interval_ms = 15000;
|
||||
static TickType_t s_heartbeat_due;
|
||||
|
||||
typedef struct {
|
||||
uint32_t version;
|
||||
int64_t sequence;
|
||||
char conversation_id[37];
|
||||
char provider_session_ref[201];
|
||||
char session_nonce[257];
|
||||
} pending_session_t;
|
||||
|
||||
static esp_err_t persist_pending_session(const platform_agent_session_t *session, int64_t sequence)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open("platform", NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) return err;
|
||||
if (session) {
|
||||
pending_session_t pending = {.version = 1, .sequence = sequence};
|
||||
snprintf(pending.conversation_id, sizeof(pending.conversation_id), "%s", session->conversation_id);
|
||||
snprintf(pending.provider_session_ref, sizeof(pending.provider_session_ref), "%s", session->provider_session_ref);
|
||||
snprintf(pending.session_nonce, sizeof(pending.session_nonce), "%s", session->session_nonce);
|
||||
err = nvs_set_blob(nvs, "pending_conv", &pending, sizeof(pending));
|
||||
} else {
|
||||
err = nvs_erase_key(nvs, "pending_conv");
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) err = ESP_OK;
|
||||
}
|
||||
if (err == ESP_OK) err = nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t recover_pending_session(void)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open("platform", NVS_READONLY, &nvs);
|
||||
if (err != ESP_OK) return err;
|
||||
pending_session_t pending = {0};
|
||||
size_t size = sizeof(pending);
|
||||
err = nvs_get_blob(nvs, "pending_conv", &pending, &size);
|
||||
nvs_close(nvs);
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) return ESP_OK;
|
||||
if (err != ESP_OK) return err;
|
||||
if (size != sizeof(pending) || pending.version != 1) return ESP_ERR_INVALID_STATE;
|
||||
platform_agent_session_t session = {0};
|
||||
memcpy(session.conversation_id, pending.conversation_id, sizeof(session.conversation_id));
|
||||
memcpy(session.provider_session_ref, pending.provider_session_ref, sizeof(session.provider_session_ref));
|
||||
memcpy(session.session_nonce, pending.session_nonce, sizeof(session.session_nonce));
|
||||
session.conversation_id[sizeof(session.conversation_id) - 1] = '\0';
|
||||
session.provider_session_ref[sizeof(session.provider_session_ref) - 1] = '\0';
|
||||
session.session_nonce[sizeof(session.session_nonce) - 1] = '\0';
|
||||
s_event_sequence = pending.sequence;
|
||||
err = platform_report_session_event(&session, "session_failed", "DEVICE_RESTARTED");
|
||||
/* The event sequence is persisted before every send. A higher recovery
|
||||
* sequence can conflict only when this owned session is already terminal. */
|
||||
if (err == ESP_OK || strcmp(s_last_error_code, "HARDWARE_AGENT_EVENT_CONFLICT") == 0 ||
|
||||
strcmp(s_last_error_code, "CONVERSATION_NOT_FOUND") == 0) {
|
||||
ESP_LOGI(TAG, "Recovered previous device session after restart");
|
||||
return persist_pending_session(NULL, 0);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static void copy_json_string(cJSON *object, const char *name, char *target, size_t target_size)
|
||||
{
|
||||
cJSON *item = cJSON_GetObjectItemCaseSensitive(object, name);
|
||||
if (cJSON_IsString(item) && item->valuestring != NULL) {
|
||||
snprintf(target, target_size, "%s", item->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t load_string(nvs_handle_t nvs, const char *key, char *target, size_t target_size)
|
||||
{
|
||||
size_t required = target_size;
|
||||
esp_err_t err = nvs_get_str(nvs, key, target, &required);
|
||||
if (err != ESP_OK || required < 2 || required > target_size) {
|
||||
target[0] = '\0';
|
||||
return err == ESP_OK ? ESP_ERR_INVALID_SIZE : err;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t platform_identity_save(const char *device_id, const char *device_secret)
|
||||
{
|
||||
if (device_id == NULL || device_secret == NULL || strlen(device_id) != 36 ||
|
||||
strlen(device_secret) < 32 || strlen(device_secret) >= PLATFORM_DEVICE_SECRET_MAX) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open("platform", NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_set_str(nvs, "device_id", device_id);
|
||||
if (err == ESP_OK) err = nvs_set_str(nvs, "device_secret", device_secret);
|
||||
if (err == ESP_OK) err = nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
if (err == ESP_OK) {
|
||||
snprintf(s_device_id, sizeof(s_device_id), "%s", device_id);
|
||||
snprintf(s_device_secret, sizeof(s_device_secret), "%s", device_secret);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
bool platform_identity_is_configured(void)
|
||||
{
|
||||
return strlen(s_device_id) == 36 && strlen(s_device_secret) >= 32;
|
||||
}
|
||||
|
||||
const char *platform_hardware_id(void)
|
||||
{
|
||||
return s_hardware_id;
|
||||
}
|
||||
|
||||
const platform_volc_credential_t *platform_volc_credential(void)
|
||||
{
|
||||
return s_volc.device_secret[0] ? &s_volc : NULL;
|
||||
}
|
||||
|
||||
esp_err_t platform_client_init(void)
|
||||
{
|
||||
if (!s_http_mutex) s_http_mutex = xSemaphoreCreateMutexStatic(&s_http_mutex_storage);
|
||||
if (!s_http_mutex) return ESP_ERR_NO_MEM;
|
||||
uint8_t mac[6] = {0};
|
||||
ESP_RETURN_ON_ERROR(esp_read_mac(mac, ESP_MAC_WIFI_STA), TAG, "read station MAC");
|
||||
snprintf(s_hardware_id, sizeof(s_hardware_id), "echoear-%02x%02x%02x%02x%02x%02x",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open("platform", NVS_READONLY, &nvs);
|
||||
if (err == ESP_OK) {
|
||||
(void)load_string(nvs, "device_id", s_device_id, sizeof(s_device_id));
|
||||
(void)load_string(nvs, "device_secret", s_device_secret, sizeof(s_device_secret));
|
||||
(void)load_string(nvs, "instance_id", s_volc.instance_id, sizeof(s_volc.instance_id));
|
||||
(void)load_string(nvs, "product_key", s_volc.product_key, sizeof(s_volc.product_key));
|
||||
(void)load_string(nvs, "device_name", s_volc.device_name, sizeof(s_volc.device_name));
|
||||
(void)load_string(nvs, "volc_secret", s_volc.device_secret, sizeof(s_volc.device_secret));
|
||||
nvs_close(nvs);
|
||||
}
|
||||
snprintf(s_volc.hardware_id, sizeof(s_volc.hardware_id), "%s", s_hardware_id);
|
||||
ESP_LOGI(TAG, "hardware_id=%s platform_identity=%s", s_hardware_id,
|
||||
platform_identity_is_configured() ? "configured" : "missing");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void timestamp_now(char output[32])
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
struct tm utc = {0};
|
||||
gmtime_r(&now, &utc);
|
||||
strftime(output, 32, "%Y-%m-%dT%H:%M:%S.000Z", &utc);
|
||||
}
|
||||
|
||||
static void random_hex(char *output, size_t bytes)
|
||||
{
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (size_t i = 0; i < bytes; ++i) {
|
||||
uint8_t value = (uint8_t)esp_random();
|
||||
output[i * 2] = hex[value >> 4];
|
||||
output[i * 2 + 1] = hex[value & 0x0f];
|
||||
}
|
||||
output[bytes * 2] = '\0';
|
||||
}
|
||||
|
||||
static void sha256_hex(const char *body, char output[65])
|
||||
{
|
||||
unsigned char digest[32];
|
||||
mbedtls_sha256((const unsigned char *)body, strlen(body), digest, 0);
|
||||
for (size_t i = 0; i < sizeof(digest); ++i) snprintf(output + i * 2, 3, "%02x", digest[i]);
|
||||
}
|
||||
|
||||
static esp_err_t signature_header(const char *method, const char *path, const char *timestamp,
|
||||
const char *nonce, const char *digest, char output[64])
|
||||
{
|
||||
char canonical[768];
|
||||
int written = snprintf(canonical, sizeof(canonical), "%s\n%s\n%s\n%s\n%s",
|
||||
method, path, timestamp, nonce, digest);
|
||||
if (written <= 0 || written >= (int)sizeof(canonical)) return ESP_ERR_INVALID_SIZE;
|
||||
unsigned char mac[32];
|
||||
const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
if (mbedtls_md_hmac(info, (const unsigned char *)s_device_secret, strlen(s_device_secret),
|
||||
(const unsigned char *)canonical, written, mac) != 0) return ESP_FAIL;
|
||||
unsigned char encoded[48];
|
||||
size_t encoded_length = 0;
|
||||
if (mbedtls_base64_encode(encoded, sizeof(encoded), &encoded_length, mac, sizeof(mac)) != 0) return ESP_FAIL;
|
||||
while (encoded_length > 0 && encoded[encoded_length - 1] == '=') --encoded_length;
|
||||
for (size_t i = 0; i < encoded_length; ++i) {
|
||||
if (encoded[i] == '+') encoded[i] = '-';
|
||||
else if (encoded[i] == '/') encoded[i] = '_';
|
||||
}
|
||||
encoded[encoded_length] = '\0';
|
||||
snprintf(output, 64, "%s", encoded);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t http_event(esp_http_client_event_t *event)
|
||||
{
|
||||
if (event->event_id == HTTP_EVENT_ON_CONNECTED) ++s_http_connection_count;
|
||||
response_buffer_t *buffer = (response_buffer_t *)event->user_data;
|
||||
if (event->event_id != HTTP_EVENT_ON_DATA || buffer == NULL || event->data_len <= 0) return ESP_OK;
|
||||
if (buffer->length + event->data_len + 1 > buffer->capacity) return ESP_ERR_NO_MEM;
|
||||
memcpy(buffer->data + buffer->length, event->data, event->data_len);
|
||||
buffer->length += event->data_len;
|
||||
buffer->data[buffer->length] = '\0';
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t platform_request_impl(const char *method, const char *path, const char *body,
|
||||
const char *idempotency_key, char **response_out)
|
||||
{
|
||||
s_last_error_code[0] = '\0';
|
||||
if (!platform_identity_is_configured()) return ESP_ERR_INVALID_STATE;
|
||||
if (body == NULL) body = "";
|
||||
char url[384];
|
||||
snprintf(url, sizeof(url), "%s%s", PLATFORM_API_ORIGIN, path);
|
||||
response_buffer_t response = {
|
||||
.data = calloc(1, PLATFORM_RESPONSE_MAX), .capacity = PLATFORM_RESPONSE_MAX,
|
||||
};
|
||||
if (response.data == NULL) return ESP_ERR_NO_MEM;
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.event_handler = http_event,
|
||||
.user_data = &response,
|
||||
.timeout_ms = 15000,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
.buffer_size = 2048,
|
||||
.buffer_size_tx = 2048,
|
||||
.keep_alive_enable = true,
|
||||
};
|
||||
if (!s_http_client) s_http_client = esp_http_client_init(&config);
|
||||
esp_http_client_handle_t client = s_http_client;
|
||||
if (client == NULL) {
|
||||
free(response.data);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
esp_http_client_method_t verb = strcmp(method, "POST") == 0 ? HTTP_METHOD_POST : HTTP_METHOD_GET;
|
||||
esp_err_t err = esp_http_client_set_url(client, url);
|
||||
if (err == ESP_OK) err = esp_http_client_set_user_data(client, &response);
|
||||
if (err == ESP_OK) err = esp_http_client_set_method(client, verb);
|
||||
/* Reused handles must not inherit a preceding request's body or key. */
|
||||
if (err == ESP_OK) err = esp_http_client_set_post_field(client, body, strlen(body));
|
||||
if (!idempotency_key) (void)esp_http_client_delete_header(client, "Idempotency-Key");
|
||||
if (!body[0]) (void)esp_http_client_delete_header(client, "Content-Type");
|
||||
char timestamp[32], nonce[33], digest[65], signature[64];
|
||||
timestamp_now(timestamp);
|
||||
random_hex(nonce, 16);
|
||||
sha256_hex(body, digest);
|
||||
if (err == ESP_OK) err = signature_header(method, path, timestamp, nonce, digest, signature);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Id", s_device_id);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Timestamp", timestamp);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Nonce", nonce);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Content-SHA256", digest);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Signature", signature);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "Accept", "application/json");
|
||||
if (err == ESP_OK && idempotency_key != NULL) err = esp_http_client_set_header(client, "Idempotency-Key", idempotency_key);
|
||||
if (err == ESP_OK && body[0] != '\0') {
|
||||
err = esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
}
|
||||
TickType_t request_started = xTaskGetTickCount();
|
||||
unsigned connection_count = s_http_connection_count;
|
||||
if (err == ESP_OK) err = esp_http_client_perform(client);
|
||||
int status = esp_http_client_get_status_code(client);
|
||||
ESP_LOGI(TAG, "PLATFORM_HTTP method=%s status=%d elapsed_ms=%u new_connection=%u",
|
||||
method, status, (unsigned)((xTaskGetTickCount() - request_started) * portTICK_PERIOD_MS),
|
||||
s_http_connection_count - connection_count);
|
||||
(void)esp_http_client_set_user_data(client, NULL);
|
||||
(void)esp_http_client_set_post_field(client, NULL, 0);
|
||||
if (err != ESP_OK) {
|
||||
/* An uncertain POST is not replayed automatically. Reconnect on the
|
||||
* next request while retaining the existing event/idempotency rules. */
|
||||
esp_http_client_cleanup(client);
|
||||
s_http_client = NULL;
|
||||
}
|
||||
if (err == ESP_OK && (status < 200 || status >= 300)) {
|
||||
// Log only a machine-readable error code, never credentials or response bodies.
|
||||
cJSON *root = cJSON_Parse(response.data);
|
||||
cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error");
|
||||
cJSON *code = cJSON_GetObjectItemCaseSensitive(error, "code");
|
||||
if (cJSON_IsString(code) && code->valuestring) {
|
||||
size_t length = strlen(code->valuestring);
|
||||
if (length > 0 && length < sizeof(s_last_error_code) &&
|
||||
strspn(code->valuestring, "ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789") == length) {
|
||||
snprintf(s_last_error_code, sizeof(s_last_error_code), "%s", code->valuestring);
|
||||
}
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
ESP_LOGE(TAG, "%s %s failed: HTTP %d", method, path, status);
|
||||
ESP_LOGE(TAG, "Platform error code: %s", s_last_error_code[0] ? s_last_error_code : "UNKNOWN");
|
||||
err = ESP_FAIL;
|
||||
}
|
||||
if (err == ESP_OK && response_out != NULL) {
|
||||
*response_out = response.data;
|
||||
response.data = NULL;
|
||||
}
|
||||
free(response.data);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t platform_request(const char *method, const char *path, const char *body,
|
||||
const char *idempotency_key, char **response_out)
|
||||
{
|
||||
if (!s_http_mutex) return ESP_ERR_INVALID_STATE;
|
||||
xSemaphoreTake(s_http_mutex, portMAX_DELAY);
|
||||
esp_err_t err = platform_request_impl(method, path, body, idempotency_key, response_out);
|
||||
xSemaphoreGive(s_http_mutex);
|
||||
return err;
|
||||
}
|
||||
|
||||
static cJSON *response_data(char *response, cJSON **root_out)
|
||||
{
|
||||
cJSON *root = cJSON_Parse(response);
|
||||
if (root == NULL) return NULL;
|
||||
cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
|
||||
if (!cJSON_IsObject(data)) {
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
*root_out = root;
|
||||
return data;
|
||||
}
|
||||
|
||||
static esp_err_t save_volc_credential(void)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open("platform", NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) return err;
|
||||
if (err == ESP_OK) err = nvs_set_str(nvs, "instance_id", s_volc.instance_id);
|
||||
if (err == ESP_OK) err = nvs_set_str(nvs, "product_key", s_volc.product_key);
|
||||
if (err == ESP_OK) err = nvs_set_str(nvs, "device_name", s_volc.device_name);
|
||||
if (err == ESP_OK) err = nvs_set_str(nvs, "volc_secret", s_volc.device_secret);
|
||||
if (err == ESP_OK) err = nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t platform_provision_volc_credential(void)
|
||||
{
|
||||
char body[160];
|
||||
snprintf(body, sizeof(body), "{\"hardware_id\":\"%s\"}", s_hardware_id);
|
||||
char key[96];
|
||||
snprintf(key, sizeof(key), "echoear-provision-%s", s_hardware_id);
|
||||
char *response = NULL;
|
||||
esp_err_t err = platform_request("POST", "/device/v3/hardware-agent-credentials:provision", body, key, &response);
|
||||
if (err != ESP_OK) return err;
|
||||
cJSON *root = NULL;
|
||||
cJSON *data = response_data(response, &root);
|
||||
if (data == NULL) { free(response); return ESP_FAIL; }
|
||||
memset(&s_volc, 0, sizeof(s_volc));
|
||||
copy_json_string(data, "instance_id", s_volc.instance_id, sizeof(s_volc.instance_id));
|
||||
copy_json_string(data, "product_key", s_volc.product_key, sizeof(s_volc.product_key));
|
||||
copy_json_string(data, "device_name", s_volc.device_name, sizeof(s_volc.device_name));
|
||||
copy_json_string(data, "device_secret", s_volc.device_secret, sizeof(s_volc.device_secret));
|
||||
snprintf(s_volc.hardware_id, sizeof(s_volc.hardware_id), "%s", s_hardware_id);
|
||||
cJSON_Delete(root);
|
||||
free(response);
|
||||
if (!s_volc.instance_id[0] || !s_volc.product_key[0] || !s_volc.device_name[0] || strlen(s_volc.device_secret) < 16) return ESP_FAIL;
|
||||
ESP_LOGI(TAG, "provider credential received for device_name=%s", s_volc.device_name);
|
||||
return save_volc_credential();
|
||||
}
|
||||
|
||||
static esp_err_t refresh_config_revision(void)
|
||||
{
|
||||
char *response = NULL;
|
||||
esp_err_t err = platform_request("GET", "/device/v2/config", "", NULL, &response);
|
||||
if (err != ESP_OK) return err;
|
||||
cJSON *root = NULL;
|
||||
cJSON *data = response_data(response, &root);
|
||||
cJSON *revision = data ? cJSON_GetObjectItemCaseSensitive(data, "revision") : NULL;
|
||||
if (cJSON_IsNumber(revision) && revision->valuedouble >= 1) s_config_revision = (int64_t)revision->valuedouble;
|
||||
cJSON_Delete(root);
|
||||
free(response);
|
||||
return data == NULL ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t report_bootstrap(void)
|
||||
{
|
||||
char timestamp[32], path[320];
|
||||
timestamp_now(timestamp);
|
||||
snprintf(path, sizeof(path),
|
||||
"/device/v2/bootstrap?firmware_version=%s&device_time=%s&board_rev=echoear&rtc_sdk_version=%s&codec=pcm16&sample_rate=16000&network=wifi",
|
||||
PLATFORM_FIRMWARE_VERSION, timestamp, PLATFORM_HARDWARE_AGENT_SDK_VERSION);
|
||||
char *response = NULL;
|
||||
esp_err_t err = platform_request("GET", path, "", NULL, &response);
|
||||
if (err != ESP_OK) return err;
|
||||
cJSON *root = NULL;
|
||||
cJSON *data = response_data(response, &root);
|
||||
cJSON *device = cJSON_GetObjectItemCaseSensitive(data, "device");
|
||||
cJSON *compatible = cJSON_GetObjectItemCaseSensitive(device, "firmware_compatible");
|
||||
cJSON *minimum = cJSON_GetObjectItemCaseSensitive(device, "minimum_firmware_version");
|
||||
ESP_LOGI(TAG, "Firmware report: current=%s minimum=%s compatible=%d",
|
||||
PLATFORM_FIRMWARE_VERSION, cJSON_IsString(minimum) ? minimum->valuestring : "none",
|
||||
cJSON_IsTrue(compatible));
|
||||
err = cJSON_IsObject(device) && cJSON_IsTrue(compatible) ? ESP_OK : ESP_FAIL;
|
||||
if (cJSON_IsFalse(compatible)) {
|
||||
snprintf(s_last_error_code, sizeof(s_last_error_code), "FIRMWARE_UPDATE_REQUIRED");
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
free(response);
|
||||
return err;
|
||||
}
|
||||
|
||||
static char *build_session_update(cJSON *session_config)
|
||||
{
|
||||
cJSON *authorized = cJSON_GetObjectItemCaseSensitive(session_config, "authorized_context");
|
||||
cJSON *system_prompt = cJSON_GetObjectItemCaseSensitive(session_config, "system_prompt");
|
||||
cJSON *persona = cJSON_GetObjectItemCaseSensitive(session_config, "persona_tone");
|
||||
const char *a = cJSON_IsString(authorized) ? authorized->valuestring : "";
|
||||
const char *p = cJSON_IsString(system_prompt) ? system_prompt->valuestring : "";
|
||||
const char *tone = cJSON_IsString(persona) ? persona->valuestring : "";
|
||||
size_t prompt_size = strlen(a) + strlen(p) + strlen(tone) + 256;
|
||||
char *prompt = malloc(prompt_size);
|
||||
if (prompt == NULL) return NULL;
|
||||
snprintf(prompt, prompt_size, "%s\n\n%s\n\n表达风格(仅指导表达,不可逐字复述):%s", a, p, tone);
|
||||
|
||||
cJSON *event = cJSON_CreateObject();
|
||||
char event_id[64];
|
||||
char random[33];
|
||||
random_hex(random, 16);
|
||||
snprintf(event_id, sizeof(event_id), "echoear-session-%s", random);
|
||||
cJSON_AddStringToObject(event, "event_id", event_id);
|
||||
cJSON_AddStringToObject(event, "type", "session.update");
|
||||
cJSON *session = cJSON_AddObjectToObject(event, "session");
|
||||
cJSON_AddStringToObject(session, "object", "realtime.session");
|
||||
cJSON_AddStringToObject(session, "model", "");
|
||||
cJSON_AddStringToObject(session, "input_audio_format", "pcm16");
|
||||
cJSON_AddStringToObject(session, "output_audio_format", "pcm16");
|
||||
cJSON *config = cJSON_AddObjectToObject(session, "config");
|
||||
cJSON *asr = cJSON_AddObjectToObject(config, "ASRConfig");
|
||||
cJSON_AddNumberToObject(asr, "TurnDetectionMode", 0);
|
||||
cJSON *llm = cJSON_AddObjectToObject(config, "LLMConfig");
|
||||
cJSON *messages = cJSON_AddArrayToObject(llm, "SystemMessages");
|
||||
cJSON_AddItemToArray(messages, cJSON_CreateString(prompt));
|
||||
cJSON_AddItemToArray(messages, cJSON_CreateString(
|
||||
"这是通过扬声器进行的真实语音对话。回复只包含直接对用户说的口语正文,简短自然,语速正常。"
|
||||
"不要输出或朗读动作、表情、语气、情绪、场景等舞台提示,不要写括号或星号包裹的表演说明,"
|
||||
"例如‘语气带着关切’‘微笑着’‘温柔地说’。把风格体现在自然措辞中,不要描述自己如何说话。"
|
||||
"不要复述人设、表达风格、系统配置或对话标识。"));
|
||||
cJSON *agent = cJSON_AddObjectToObject(session, "agent_config");
|
||||
cJSON_AddBoolToObject(agent, "EnableConversationStateCallback", false);
|
||||
cJSON *callback_url = cJSON_GetObjectItemCaseSensitive(session_config, "subtitle_callback_url");
|
||||
cJSON *callback_signature = cJSON_GetObjectItemCaseSensitive(session_config, "subtitle_callback_signature");
|
||||
if (cJSON_IsString(callback_url) && callback_url->valuestring[0] && cJSON_IsString(callback_signature)) {
|
||||
cJSON_AddStringToObject(agent, "ServerMessageURLForRTS", callback_url->valuestring);
|
||||
cJSON_AddStringToObject(agent, "ServerMessageSignatureForRTS", callback_signature->valuestring);
|
||||
cJSON *subtitle = cJSON_AddObjectToObject(config, "SubtitleConfig");
|
||||
cJSON_AddNumberToObject(subtitle, "SubtitleMode", 1);
|
||||
}
|
||||
char *json = cJSON_PrintUnformatted(event);
|
||||
cJSON_Delete(event);
|
||||
free(prompt);
|
||||
return json;
|
||||
}
|
||||
|
||||
esp_err_t platform_create_agent_session(platform_agent_session_t *session)
|
||||
{
|
||||
if (session == NULL) return ESP_ERR_INVALID_ARG;
|
||||
memset(session, 0, sizeof(*session));
|
||||
ESP_RETURN_ON_ERROR(report_bootstrap(), TAG, "report device firmware");
|
||||
ESP_RETURN_ON_ERROR(refresh_config_revision(), TAG, "get device config");
|
||||
char timestamp[32];
|
||||
timestamp_now(timestamp);
|
||||
char body[1024];
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"device_time\":\"%s\",\"monotonic_ms\":0,\"capabilities\":{"
|
||||
"\"hardware_agent\":true,\"sdk_version\":\"%s\",\"transports\":[\"low_load\"],"
|
||||
"\"input_audio_format\":\"pcm_s16le_16000_mono\",\"output_audio_format\":\"pcm_s16le_16000_mono\","
|
||||
"\"server_event_callback\":false,\"native_subtitle_callback\":true,\"transcript_upload\":true,\"dynamic_session_config\":true,"
|
||||
"\"credential_rotation\":true,\"near_pcm_recording\":false,\"far_pcm_recording\":false},"
|
||||
"\"network\":{\"type\":\"wifi\"},\"requested_goal\":\"open_conversation\",\"config_revision\":%lld}",
|
||||
timestamp, PLATFORM_HARDWARE_AGENT_SDK_VERSION, (long long)s_config_revision);
|
||||
char random[33], key[64];
|
||||
random_hex(random, 16);
|
||||
snprintf(key, sizeof(key), "echoear-session-%s", random);
|
||||
char *response = NULL;
|
||||
esp_err_t err = platform_request("POST", "/device/v3/agent-sessions", body, key, &response);
|
||||
if (err != ESP_OK) return err;
|
||||
cJSON *root = NULL;
|
||||
cJSON *data = response_data(response, &root);
|
||||
cJSON *runtime = data ? cJSON_GetObjectItemCaseSensitive(data, "runtime") : NULL;
|
||||
cJSON *session_config = data ? cJSON_GetObjectItemCaseSensitive(data, "session_config") : NULL;
|
||||
cJSON *limits = data ? cJSON_GetObjectItemCaseSensitive(data, "session") : NULL;
|
||||
cJSON *interval = limits ? cJSON_GetObjectItemCaseSensitive(limits, "heartbeat_interval_sec") : NULL;
|
||||
s_heartbeat_interval_ms = cJSON_IsNumber(interval) && interval->valueint >= 1 && interval->valueint <= 60
|
||||
? (uint32_t)interval->valueint * 1000 : 15000;
|
||||
if (!cJSON_IsObject(runtime) || !cJSON_IsObject(session_config)) {
|
||||
cJSON_Delete(root); free(response); return ESP_FAIL;
|
||||
}
|
||||
copy_json_string(data, "conversation_id", session->conversation_id, sizeof(session->conversation_id));
|
||||
copy_json_string(runtime, "bot_id", session->bot_id, sizeof(session->bot_id));
|
||||
copy_json_string(runtime, "provider_session_ref", session->provider_session_ref, sizeof(session->provider_session_ref));
|
||||
copy_json_string(runtime, "session_nonce", session->session_nonce, sizeof(session->session_nonce));
|
||||
copy_json_string(runtime, "config_digest", session->config_digest, sizeof(session->config_digest));
|
||||
session->session_update_json = build_session_update(session_config);
|
||||
cJSON *subtitle_source = cJSON_GetObjectItemCaseSensitive(session_config, "subtitle_source");
|
||||
session->native_subtitle_primary = cJSON_IsString(subtitle_source) && strcmp(subtitle_source->valuestring, "provider") == 0;
|
||||
cJSON_Delete(root);
|
||||
free(response);
|
||||
if (!session->conversation_id[0] || !session->bot_id[0] || !session->provider_session_ref[0] ||
|
||||
!session->session_nonce[0] || !session->config_digest[0] || session->session_update_json == NULL) {
|
||||
platform_agent_session_free(session);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ESP_LOGI(TAG, "platform session ready conversation_id=%s", session->conversation_id);
|
||||
return persist_pending_session(session, s_event_sequence);
|
||||
}
|
||||
|
||||
static esp_err_t report_event(const platform_agent_session_t *session, const char *event_type,
|
||||
const char *provider_connection_ref, const char *error_code)
|
||||
{
|
||||
if (session == NULL || event_type == NULL) return ESP_ERR_INVALID_ARG;
|
||||
char timestamp[32], random[33], event_id[64], path[256];
|
||||
timestamp_now(timestamp);
|
||||
random_hex(random, 16);
|
||||
snprintf(event_id, sizeof(event_id), "echoear-event-%s", random);
|
||||
snprintf(path, sizeof(path), "/device/v3/agent-sessions/%s/events", session->conversation_id);
|
||||
cJSON *body = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(body, "event_id", event_id);
|
||||
cJSON_AddNumberToObject(body, "sequence_no", ++s_event_sequence);
|
||||
esp_err_t saved = persist_pending_session(session, s_event_sequence);
|
||||
if (saved != ESP_OK) { cJSON_Delete(body); return saved; }
|
||||
cJSON_AddStringToObject(body, "event_type", event_type);
|
||||
cJSON_AddStringToObject(body, "occurred_at", timestamp);
|
||||
cJSON_AddStringToObject(body, "provider_session_ref", session->provider_session_ref);
|
||||
cJSON_AddStringToObject(body, "session_nonce", session->session_nonce);
|
||||
if (strcmp(event_type, "session_started") == 0) {
|
||||
cJSON_AddStringToObject(body, "provider_connection_ref", provider_connection_ref);
|
||||
cJSON_AddStringToObject(body, "bot_id", session->bot_id);
|
||||
cJSON_AddStringToObject(body, "transport", "low_load");
|
||||
cJSON_AddStringToObject(body, "config_digest", session->config_digest);
|
||||
cJSON_AddBoolToObject(body, "dynamic_config_applied", true);
|
||||
}
|
||||
if (error_code != NULL && error_code[0]) cJSON_AddStringToObject(body, "error_code", error_code);
|
||||
char *json = cJSON_PrintUnformatted(body);
|
||||
cJSON_Delete(body);
|
||||
if (json == NULL) return ESP_ERR_NO_MEM;
|
||||
char *response = NULL;
|
||||
esp_err_t err = platform_request("POST", path, json, NULL, &response);
|
||||
free(json);
|
||||
if (err == ESP_OK && strcmp(event_type, "session_updated") == 0) {
|
||||
cJSON *root = NULL;
|
||||
cJSON *data = response_data(response, &root);
|
||||
cJSON *action = data ? cJSON_GetObjectItemCaseSensitive(data, "action") : NULL;
|
||||
bool stop = cJSON_IsString(action) && strcmp(action->valuestring, "stop_session") == 0;
|
||||
ESP_LOGI(TAG, "PLATFORM_HEARTBEAT stop=%d", stop);
|
||||
if (!data) err = ESP_FAIL;
|
||||
else if (stop) err = ESP_ERR_INVALID_STATE;
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
free(response);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t platform_report_session_started(const platform_agent_session_t *session,
|
||||
const char *provider_connection_ref)
|
||||
{
|
||||
return report_event(session, "session_started", provider_connection_ref, NULL);
|
||||
}
|
||||
|
||||
esp_err_t platform_report_session_event(const platform_agent_session_t *session,
|
||||
const char *event_type, const char *error_code)
|
||||
{
|
||||
return report_event(session, event_type, NULL, error_code);
|
||||
}
|
||||
|
||||
void platform_agent_session_free(platform_agent_session_t *session)
|
||||
{
|
||||
if (session == NULL) return;
|
||||
free(session->session_update_json);
|
||||
memset(session, 0, sizeof(*session));
|
||||
}
|
||||
|
||||
const char *platform_runtime_instance_id(void) { return s_volc.instance_id; }
|
||||
const char *platform_runtime_product_key(void) { return s_volc.product_key; }
|
||||
const char *platform_runtime_device_name(void) { return s_volc.device_name; }
|
||||
const char *platform_runtime_device_secret(void) { return s_volc.device_secret; }
|
||||
|
||||
esp_err_t platform_conversation_begin(void)
|
||||
{
|
||||
platform_agent_session_free(&s_active_session);
|
||||
ESP_RETURN_ON_ERROR(recover_pending_session(), TAG, "recover interrupted session");
|
||||
s_event_sequence = 0;
|
||||
s_turn_sequence = 0;
|
||||
return platform_create_agent_session(&s_active_session);
|
||||
}
|
||||
|
||||
const char *platform_conversation_bot_id(void) { return s_active_session.bot_id; }
|
||||
const char *platform_conversation_update_json(void) { return s_active_session.session_update_json; }
|
||||
|
||||
esp_err_t platform_conversation_started(const char *provider_connection_ref)
|
||||
{
|
||||
if (provider_connection_ref == NULL || provider_connection_ref[0] == '\0') return ESP_ERR_INVALID_ARG;
|
||||
esp_err_t err = platform_report_session_started(&s_active_session, provider_connection_ref);
|
||||
s_heartbeat_due = xTaskGetTickCount() + pdMS_TO_TICKS(s_heartbeat_interval_ms);
|
||||
return err;
|
||||
}
|
||||
|
||||
int platform_conversation_poll(void)
|
||||
{
|
||||
if (!s_active_session.conversation_id[0]) return 1;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
if ((int32_t)(now - s_heartbeat_due) < 0) return 0;
|
||||
esp_err_t err = platform_report_session_event(&s_active_session, "session_updated", NULL);
|
||||
s_heartbeat_due = xTaskGetTickCount() + pdMS_TO_TICKS(err == ESP_OK ? s_heartbeat_interval_ms : 5000);
|
||||
if (err == ESP_ERR_INVALID_STATE) return 1;
|
||||
if (err != ESP_OK) ESP_LOGW(TAG, "PLATFORM_HEARTBEAT failed=%s", esp_err_to_name(err));
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t platform_conversation_turn(const char *speaker, const char *source_id, const char *text)
|
||||
{
|
||||
if (s_active_session.native_subtitle_primary) return ESP_OK;
|
||||
if (speaker == NULL || text == NULL || text[0] == '\0' || s_active_session.conversation_id[0] == '\0') return ESP_ERR_INVALID_ARG;
|
||||
char timestamp[32], path[256], key[96], random[33];
|
||||
timestamp_now(timestamp);
|
||||
random_hex(random, 16);
|
||||
snprintf(path, sizeof(path), "/device/v3/agent-sessions/%s/turns:batch", s_active_session.conversation_id);
|
||||
snprintf(key, sizeof(key), "echoear-turn-%s", random);
|
||||
cJSON *payload = cJSON_CreateObject();
|
||||
cJSON *turns = cJSON_AddArrayToObject(payload, "turns");
|
||||
cJSON *turn = cJSON_CreateObject();
|
||||
cJSON_AddItemToArray(turns, turn);
|
||||
char turn_key[256];
|
||||
snprintf(turn_key, sizeof(turn_key), "echoear-%s-%s", speaker, source_id && source_id[0] ? source_id : random);
|
||||
cJSON_AddStringToObject(turn, "turn_key", turn_key);
|
||||
cJSON_AddNumberToObject(turn, "sequence_no", ++s_turn_sequence);
|
||||
cJSON_AddStringToObject(turn, "speaker", speaker);
|
||||
cJSON_AddStringToObject(turn, "start_at", timestamp);
|
||||
cJSON_AddStringToObject(turn, "end_at", timestamp);
|
||||
cJSON_AddStringToObject(turn, "final_text", text);
|
||||
cJSON_AddBoolToObject(turn, "interrupted", false);
|
||||
char *json = cJSON_PrintUnformatted(payload);
|
||||
cJSON_Delete(payload);
|
||||
if (json == NULL) return ESP_ERR_NO_MEM;
|
||||
char *response = NULL;
|
||||
esp_err_t err = platform_request("POST", path, json, key, &response);
|
||||
free(json);
|
||||
if (err == ESP_OK) {
|
||||
cJSON *root = NULL;
|
||||
cJSON *data = response_data(response, &root);
|
||||
cJSON *accepted = data ? cJSON_GetObjectItemCaseSensitive(data, "accepted_count") : NULL;
|
||||
cJSON *duplicate = data ? cJSON_GetObjectItemCaseSensitive(data, "duplicate_count") : NULL;
|
||||
if (!cJSON_IsNumber(accepted) || !cJSON_IsNumber(duplicate) || accepted->valueint + duplicate->valueint != 1) {
|
||||
err = ESP_FAIL;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "PLATFORM_TURN speaker=%s sequence=%lld accepted=%d duplicate=%d",
|
||||
speaker, (long long)s_turn_sequence, accepted->valueint, duplicate->valueint);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
if (err != ESP_OK) ESP_LOGE(TAG, "PLATFORM_TURN upload failed sequence=%lld", (long long)s_turn_sequence);
|
||||
free(response);
|
||||
return err;
|
||||
}
|
||||
|
||||
void platform_conversation_finish(bool failed, const char *error_code)
|
||||
{
|
||||
if (s_active_session.conversation_id[0]) {
|
||||
esp_err_t err = platform_report_session_event(&s_active_session,
|
||||
failed ? "session_failed" : "session_ended",
|
||||
failed ? error_code : NULL);
|
||||
if (err == ESP_OK) (void)persist_pending_session(NULL, 0);
|
||||
}
|
||||
platform_agent_session_free(&s_active_session);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PLATFORM_DEVICE_ID_MAX 37
|
||||
#define PLATFORM_DEVICE_SECRET_MAX 129
|
||||
#define PLATFORM_HARDWARE_ID_MAX 65
|
||||
|
||||
typedef struct {
|
||||
char instance_id[201];
|
||||
char product_key[201];
|
||||
char device_name[129];
|
||||
char device_secret[257];
|
||||
char hardware_id[PLATFORM_HARDWARE_ID_MAX];
|
||||
} platform_volc_credential_t;
|
||||
|
||||
typedef struct {
|
||||
char conversation_id[37];
|
||||
char bot_id[201];
|
||||
char provider_session_ref[201];
|
||||
char session_nonce[257];
|
||||
char config_digest[65];
|
||||
char *session_update_json;
|
||||
bool native_subtitle_primary;
|
||||
} platform_agent_session_t;
|
||||
|
||||
esp_err_t platform_identity_save(const char *device_id, const char *device_secret);
|
||||
bool platform_identity_is_configured(void);
|
||||
const char *platform_last_error_code(void);
|
||||
esp_err_t platform_client_init(void);
|
||||
const char *platform_hardware_id(void);
|
||||
const platform_volc_credential_t *platform_volc_credential(void);
|
||||
esp_err_t platform_provision_volc_credential(void);
|
||||
esp_err_t platform_create_agent_session(platform_agent_session_t *session);
|
||||
esp_err_t platform_report_session_started(const platform_agent_session_t *session,
|
||||
const char *provider_connection_ref);
|
||||
esp_err_t platform_report_session_event(const platform_agent_session_t *session,
|
||||
const char *event_type,
|
||||
const char *error_code);
|
||||
void platform_agent_session_free(platform_agent_session_t *session);
|
||||
|
||||
/* Link-safe hooks used by the vendor application component. */
|
||||
const char *platform_runtime_instance_id(void);
|
||||
const char *platform_runtime_product_key(void);
|
||||
const char *platform_runtime_device_name(void);
|
||||
const char *platform_runtime_device_secret(void);
|
||||
esp_err_t platform_conversation_begin(void);
|
||||
const char *platform_conversation_bot_id(void);
|
||||
const char *platform_conversation_update_json(void);
|
||||
esp_err_t platform_conversation_started(const char *provider_connection_ref);
|
||||
int platform_conversation_poll(void);
|
||||
esp_err_t platform_conversation_turn(const char *speaker, const char *source_id, const char *text);
|
||||
void platform_conversation_finish(bool failed, const char *error_code);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user