基于 ESP32-S3 + VB6824 的四博 A1 AI 智能拍学机方案:事件驱动架构、拍照识别与语音交互实现
1. 项目概述
四博 A1 AI 智能拍学机可以理解为一套面向儿童学习场景的多模态 AI 终端。它通过 ESP32-S3 主控 + 摄像头 + 屏幕 + 麦克风 + 喇叭 + VB6824 语音芯片 + 云端 AI 大模型,实现拍题识别、绘本陪读、英语跟读、AI 问答、趣味游戏和 OTA 升级等功能。
从四博模组选型资料来看,ESP32-S3 系列被定位在 音视频 / AI 市场,适合承担摄像头、LCD、Wi-Fi、BLE 和 AIoT 业务调度。 四博 AI 硬件资料中也提到,ESP32-C2/C3/S3 加 VB6824 的语音方案已经应用于 S3 双目、S3 拍学机、地球仪、电子吧唧等场景,VB6824 可处理音频编解码、AEC、语音唤醒和改唤醒词,让主控专注通信和 UI。
本文不采用简单的 app_main() 顺序调用方式,而是使用:
事件总线 + FreeRTOS Queue + 状态机 + 模块化任务
这样更适合真实产品量产。
2. 系统架构设计
┌─────────────────────────────────────┐
│ 应用业务层 │
│ 拍题识别 / 绘本陪读 / 英语跟读 / 游戏 │
├─────────────────────────────────────┤
│ 事件调度层 │
│ app_event_queue / app_state_machine │
├─────────────────────────────────────┤
│ 设备服务层 │
│ Camera / LCD / Audio / Wi-Fi / OTA │
├─────────────────────────────────────┤
│ AI 通信层 │
│ WebSocket / JSON / Image Binary │
├─────────────────────────────────────┤
│ 硬件驱动层 │
│ ESP32-S3 / VB6824 / LCD / Camera │
└─────────────────────────────────────┘
核心思路:
语音命令、按键点击、AI 返回、OTA 通知全部转成事件;
业务层只处理事件,不直接耦合底层驱动;
摄像头、WebSocket、UI、语音串口分别独立成任务。
3. 工程目录建议
a1_ai_study_machine/
├── main/
│ ├── app_main.c
│ ├── app_event.h
│ ├── app_state.c
│ ├── app_state.h
│ ├── board_config.h
│ ├── camera_service.c
│ ├── camera_service.h
│ ├── voice_vb6824.c
│ ├── voice_vb6824.h
│ ├── ai_ws_client.c
│ ├── ai_ws_client.h
│ ├── ai_protocol.c
│ ├── ai_protocol.h
│ ├── ui_lvgl.c
│ ├── ui_lvgl.h
│ ├── ota_service.c
│ └── ota_service.h
├── components/
│ ├── lcd_driver/
│ ├── audio_player/
│ ├── storage/
│ └── json_helper/
├── partitions.csv
├── sdkconfig.defaults
└── CMakeLists.txt
4. 硬件配置建议
主控:ESP32-S3R8 / ESP32-S3 N16R8
Flash:16MB
PSRAM:8MB
语音:VB6824
摄像头:OV2640 / GC0308
屏幕:SPI LCD / RGB LCD
音频:MIC + Speaker + 功放
通信:Wi-Fi + BLE,4G 可选
存储:SPIFFS / LittleFS / TF Card 可选
电源:锂电池 + Type-C
5. board_config.h
所有硬件引脚统一放到 board_config.h,不要散落在业务代码里。
#pragma once
#define A1_DEVICE_NAME "SIBO_A1_AI_CAMERA"
#define A1_DEVICE_ID "A1_S3_001122334455"
/* Camera Pins */
#define CAM_PIN_PWDN -1
#define CAM_PIN_RESET -1
#define CAM_PIN_XCLK 15
#define CAM_PIN_SIOD 4
#define CAM_PIN_SIOC 5
#define CAM_PIN_D7 16
#define CAM_PIN_D6 17
#define CAM_PIN_D5 18
#define CAM_PIN_D4 12
#define CAM_PIN_D3 10
#define CAM_PIN_D2 8
#define CAM_PIN_D1 9
#define CAM_PIN_D0 11
#define CAM_PIN_VSYNC 6
#define CAM_PIN_HREF 7
#define CAM_PIN_PCLK 13
/* VB6824 UART */
#define VB6824_UART_NUM UART_NUM_1
#define VB6824_UART_TX 17
#define VB6824_UART_RX 18
#define VB6824_UART_BAUD 115200
/* AI Server */
#define AI_WS_URL "wss://ai.example.com/a1/ws"
/* OTA */
#define OTA_MANIFEST_URL "https://cdn.example.com/a1/manifest.json"
6. 事件模型设计
先定义统一事件类型。
#pragma once
#include <stdint.h>
#include <stddef.h>
typedef enum {
APP_EVT_NONE = 0,
APP_EVT_WIFI_CONNECTED,
APP_EVT_WIFI_DISCONNECTED,
APP_EVT_AI_CONNECTED,
APP_EVT_AI_DISCONNECTED,
APP_EVT_AI_TEXT_RESULT,
APP_EVT_AI_TTS_URL,
APP_EVT_AI_OTA_NOTIFY,
APP_EVT_VOICE_WAKEUP,
APP_EVT_VOICE_TAKE_PHOTO,
APP_EVT_VOICE_HOMEWORK,
APP_EVT_VOICE_ENGLISH,
APP_EVT_VOICE_STORY,
APP_EVT_VOICE_GAME,
APP_EVT_VOICE_BACK_HOME,
APP_EVT_KEY_CAMERA,
APP_EVT_KEY_BACK,
APP_EVT_KEY_CONFIRM,
APP_EVT_CAMERA_CAPTURE_OK,
APP_EVT_CAMERA_CAPTURE_FAIL,
APP_EVT_OTA_START,
APP_EVT_OTA_SUCCESS,
APP_EVT_OTA_FAIL,
} app_event_type_t;
typedef struct {
app_event_type_t type;
void *data;
size_t data_len;
} app_event_t;
全局事件队列:
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "app_event.h"
QueueHandle_t g_app_event_queue = NULL;
void app_event_init(void)
{
g_app_event_queue = xQueueCreate(16, sizeof(app_event_t));
}
void app_event_send(app_event_type_t type, void *data, size_t len)
{
if (!g_app_event_queue) {
return;
}
app_event_t evt = {
.type = type,
.data = data,
.data_len = len,
};
xQueueSend(g_app_event_queue, &evt, pdMS_TO_TICKS(20));
}
7. 状态机设计
拍学机不是简单菜单机,它至少有以下状态:
typedef enum {
A1_STATE_BOOT = 0,
A1_STATE_HOME,
A1_STATE_LISTENING,
A1_STATE_CAPTURING,
A1_STATE_UPLOADING,
A1_STATE_AI_THINKING,
A1_STATE_SPEAKING,
A1_STATE_GAME,
A1_STATE_OTA,
A1_STATE_ERROR,
} a1_state_t;
static a1_state_t s_state = A1_STATE_BOOT;
a1_state_t app_state_get(void)
{
return s_state;
}
void app_state_set(a1_state_t state)
{
s_state = state;
}
事件分发任务:
#include "app_event.h"
#include "app_state.h"
#include "camera_service.h"
#include "ai_ws_client.h"
#include "ui_lvgl.h"
#include "ota_service.h"
extern QueueHandle_t g_app_event_queue;
static void app_handle_event(app_event_t *evt)
{
switch (evt->type) {
case APP_EVT_WIFI_CONNECTED:
ui_show_status("Wi-Fi 已连接");
ai_ws_client_start();
break;
case APP_EVT_AI_CONNECTED:
ui_show_status("AI 服务在线");
app_state_set(A1_STATE_HOME);
ui_show_home();
break;
case APP_EVT_VOICE_WAKEUP:
app_state_set(A1_STATE_LISTENING);
ui_show_status("我在,请说");
break;
case APP_EVT_VOICE_TAKE_PHOTO:
case APP_EVT_VOICE_HOMEWORK:
case APP_EVT_KEY_CAMERA:
app_state_set(A1_STATE_CAPTURING);
ui_show_status("正在拍照...");
camera_service_capture_async();
break;
case APP_EVT_CAMERA_CAPTURE_OK:
app_state_set(A1_STATE_UPLOADING);
ui_show_status("正在上传 AI 识别...");
ai_ws_send_homework_image(evt->data, evt->data_len);
break;
case APP_EVT_AI_TEXT_RESULT:
app_state_set(A1_STATE_SPEAKING);
ui_show_ai_result((const char *)evt->data);
break;
case APP_EVT_VOICE_ENGLISH:
app_state_set(A1_STATE_AI_THINKING);
ui_show_status("进入英语跟读");
ai_ws_send_english_request("I have an apple.");
break;
case APP_EVT_VOICE_GAME:
app_state_set(A1_STATE_GAME);
ui_show_word_game();
break;
case APP_EVT_AI_OTA_NOTIFY:
app_state_set(A1_STATE_OTA);
ota_service_start((const char *)evt->data);
break;
case APP_EVT_VOICE_BACK_HOME:
case APP_EVT_KEY_BACK:
app_state_set(A1_STATE_HOME);
ui_show_home();
break;
default:
break;
}
}
void app_dispatcher_task(void *arg)
{
app_event_t evt;
while (1) {
if (xQueueReceive(g_app_event_queue, &evt, portMAX_DELAY) == pdTRUE) {
app_handle_event(&evt);
}
}
}
8. app_main.c
主入口只负责初始化,不写复杂业务。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "app_event.h"
#include "camera_service.h"
#include "voice_vb6824.h"
#include "ui_lvgl.h"
#include "wifi_service.h"
#include "ota_service.h"
static const char *TAG = "A1_MAIN";
void app_main(void)
{
ESP_LOGI(TAG, "四博 A1 AI 智能拍学机启动");
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
app_event_init();
ui_lvgl_init();
ui_show_boot("四博 A1 AI 拍学机");
camera_service_init();
voice_vb6824_init();
wifi_service_init();
ota_service_init();
xTaskCreate(app_dispatcher_task, "app_dispatcher", 8192, NULL, 8, NULL);
xTaskCreate(voice_vb6824_task, "voice_vb6824", 4096, NULL, 6, NULL);
wifi_service_start();
ui_show_status("系统初始化完成,正在联网...");
}
9. 摄像头服务:异步拍照
摄像头不要在 UI 回调里直接拍照,建议通过任务处理。
#include "esp_camera.h"
#include "esp_log.h"
#include "board_config.h"
#include "app_event.h"
static const char *TAG = "CAM_SERVICE";
static QueueHandle_t s_camera_cmd_queue;
typedef enum {
CAMERA_CMD_CAPTURE = 1,
} camera_cmd_t;
esp_err_t camera_service_init(void)
{
camera_config_t config = {
.pin_pwdn = CAM_PIN_PWDN,
.pin_reset = CAM_PIN_RESET,
.pin_xclk = CAM_PIN_XCLK,
.pin_sccb_sda = CAM_PIN_SIOD,
.pin_sccb_scl = CAM_PIN_SIOC,
.pin_d7 = CAM_PIN_D7,
.pin_d6 = CAM_PIN_D6,
.pin_d5 = CAM_PIN_D5,
.pin_d4 = CAM_PIN_D4,
.pin_d3 = CAM_PIN_D3,
.pin_d2 = CAM_PIN_D2,
.pin_d1 = CAM_PIN_D1,
.pin_d0 = CAM_PIN_D0,
.pin_vsync = CAM_PIN_VSYNC,
.pin_href = CAM_PIN_HREF,
.pin_pclk = CAM_PIN_PCLK,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_SVGA,
.jpeg_quality = 12,
.fb_count = 2,
.grab_mode = CAMERA_GRAB_LATEST
};
esp_err_t ret = esp_camera_init(&config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "摄像头初始化失败: 0x%x", ret);
return ret;
}
s_camera_cmd_queue = xQueueCreate(4, sizeof(camera_cmd_t));
xTaskCreate(camera_service_task, "camera_service", 8192, NULL, 7, NULL);
ESP_LOGI(TAG, "摄像头服务初始化完成");
return ESP_OK;
}
void camera_service_capture_async(void)
{
camera_cmd_t cmd = CAMERA_CMD_CAPTURE;
xQueueSend(s_camera_cmd_queue, &cmd, pdMS_TO_TICKS(10));
}
void camera_service_task(void *arg)
{
camera_cmd_t cmd;
while (1) {
if (xQueueReceive(s_camera_cmd_queue, &cmd, portMAX_DELAY) == pdTRUE) {
if (cmd == CAMERA_CMD_CAPTURE) {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
app_event_send(APP_EVT_CAMERA_CAPTURE_FAIL, NULL, 0);
continue;
}
uint8_t *copy = heap_caps_malloc(fb->len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!copy) {
esp_camera_fb_return(fb);
app_event_send(APP_EVT_CAMERA_CAPTURE_FAIL, NULL, 0);
continue;
}
memcpy(copy, fb->buf, fb->len);
size_t len = fb->len;
esp_camera_fb_return(fb);
app_event_send(APP_EVT_CAMERA_CAPTURE_OK, copy, len);
}
}
}
}
10. AI WebSocket:JSON + 二进制图片分片
为了避免一次性发送大图失败,建议采用 先发 JSON 元信息,再分片发送 JPEG 数据。
#include "esp_websocket_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include "board_config.h"
#include "app_event.h"
static const char *TAG = "AI_WS";
static esp_websocket_client_handle_t s_ws;
#define AI_IMAGE_CHUNK_SIZE 2048
static void ai_ws_event_handler(
void *handler_args,
esp_event_base_t base,
int32_t event_id,
void *event_data
)
{
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
app_event_send(APP_EVT_AI_CONNECTED, NULL, 0);
break;
case WEBSOCKET_EVENT_DISCONNECTED:
app_event_send(APP_EVT_AI_DISCONNECTED, NULL, 0);
break;
case WEBSOCKET_EVENT_DATA:
if (data->op_code == 0x1) {
char *msg = calloc(1, data->data_len + 1);
if (msg) {
memcpy(msg, data->data_ptr, data->data_len);
app_event_send(APP_EVT_AI_TEXT_RESULT, msg, data->data_len);
}
}
break;
default:
break;
}
}
void ai_ws_client_start(void)
{
esp_websocket_client_config_t config = {
.uri = AI_WS_URL,
.reconnect_timeout_ms = 3000,
.network_timeout_ms = 10000,
};
s_ws = esp_websocket_client_init(&config);
esp_websocket_register_events(
s_ws,
WEBSOCKET_EVENT_ANY,
ai_ws_event_handler,
NULL
);
esp_websocket_client_start(s_ws);
}
static bool ai_ws_ready(void)
{
return s_ws && esp_websocket_client_is_connected(s_ws);
}
static void ai_ws_send_json(cJSON *root)
{
if (!ai_ws_ready()) {
ESP_LOGW(TAG, "AI WebSocket 未连接");
return;
}
char *json = cJSON_PrintUnformatted(root);
if (!json) {
return;
}
esp_websocket_client_send_text(s_ws, json, strlen(json), portMAX_DELAY);
cJSON_free(json);
}
void ai_ws_send_homework_image(uint8_t *image, size_t image_len)
{
if (!image || image_len == 0) {
return;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "image_start");
cJSON_AddStringToObject(root, "device_id", A1_DEVICE_ID);
cJSON_AddStringToObject(root, "scene", "homework");
cJSON_AddStringToObject(root, "format", "jpeg");
cJSON_AddNumberToObject(root, "length", image_len);
cJSON_AddStringToObject(root, "prompt",
"请识别图片中的题目,给出答案,并用适合小学生理解的方式分步讲解。");
ai_ws_send_json(root);
cJSON_Delete(root);
size_t offset = 0;
while (offset < image_len) {
size_t chunk = image_len - offset;
if (chunk > AI_IMAGE_CHUNK_SIZE) {
chunk = AI_IMAGE_CHUNK_SIZE;
}
esp_websocket_client_send_bin(
s_ws,
(const char *)(image + offset),
chunk,
portMAX_DELAY
);
offset += chunk;
vTaskDelay(pdMS_TO_TICKS(5));
}
cJSON *end = cJSON_CreateObject();
cJSON_AddStringToObject(end, "type", "image_end");
cJSON_AddStringToObject(end, "scene", "homework");
ai_ws_send_json(end);
cJSON_Delete(end);
free(image);
}
11. AI 协议返回示例
云端返回可以统一为以下格式:
{
"type": "ai_result",
"scene": "homework",
"title": "拍题讲解",
"text": "这道题可以先观察已知条件,再列式计算...",
"tts_url": "https://cdn.example.com/tts/a1_001.mp3"
}
解析代码:
#include "cJSON.h"
#include "ui_lvgl.h"
#include "audio_player.h"
#include "app_event.h"
void ai_protocol_parse_text(const char *json)
{
cJSON *root = cJSON_Parse(json);
if (!root) {
return;
}
cJSON *type = cJSON_GetObjectItem(root, "type");
if (cJSON_IsString(type) &&
strcmp(type->valuestring, "ai_result") == 0) {
cJSON *title = cJSON_GetObjectItem(root, "title");
cJSON *text = cJSON_GetObjectItem(root, "text");
cJSON *tts = cJSON_GetObjectItem(root, "tts_url");
if (cJSON_IsString(title) && cJSON_IsString(text)) {
ui_show_ai_card(title->valuestring, text->valuestring);
}
if (cJSON_IsString(tts)) {
audio_player_play_url(tts->valuestring);
}
}
if (cJSON_IsString(type) &&
strcmp(type->valuestring, "ota_notify") == 0) {
cJSON *url = cJSON_GetObjectItem(root, "firmware_url");
if (cJSON_IsString(url)) {
char *ota_url = strdup(url->valuestring);
app_event_send(APP_EVT_AI_OTA_NOTIFY, ota_url, strlen(ota_url));
}
}
cJSON_Delete(root);
}
12. VB6824 语音命令接入
VB6824 和 ESP32-S3 之间采用 UART 通信。VB6824 识别到离线命令后,只需要发送一个简单命令码给 ESP32-S3。
#include "driver/uart.h"
#include "esp_log.h"
#include "board_config.h"
#include "app_event.h"
static const char *TAG = "VB6824";
typedef enum {
VB_CMD_WAKEUP = 0x01,
VB_CMD_TAKE_PHOTO = 0x02,
VB_CMD_HOMEWORK = 0x03,
VB_CMD_ENGLISH = 0x04,
VB_CMD_STORY = 0x05,
VB_CMD_GAME = 0x06,
VB_CMD_BACK_HOME = 0x07,
} vb_cmd_t;
void voice_vb6824_init(void)
{
uart_config_t uart_config = {
.baud_rate = VB6824_UART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
uart_driver_install(VB6824_UART_NUM, 1024, 0, 0, NULL, 0);
uart_param_config(VB6824_UART_NUM, &uart_config);
uart_set_pin(
VB6824_UART_NUM,
VB6824_UART_TX,
VB6824_UART_RX,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE
);
ESP_LOGI(TAG, "VB6824 UART 初始化完成");
}
static uint8_t vb_checksum(uint8_t *buf, int len)
{
uint8_t sum = 0;
for (int i = 0; i < len; i++) {
sum += buf[i];
}
return sum;
}
static void voice_dispatch_cmd(uint8_t cmd)
{
switch (cmd) {
case VB_CMD_WAKEUP:
app_event_send(APP_EVT_VOICE_WAKEUP, NULL, 0);
break;
case VB_CMD_TAKE_PHOTO:
app_event_send(APP_EVT_VOICE_TAKE_PHOTO, NULL, 0);
break;
case VB_CMD_HOMEWORK:
app_event_send(APP_EVT_VOICE_HOMEWORK, NULL, 0);
break;
case VB_CMD_ENGLISH:
app_event_send(APP_EVT_VOICE_ENGLISH, NULL, 0);
break;
case VB_CMD_STORY:
app_event_send(APP_EVT_VOICE_STORY, NULL, 0);
break;
case VB_CMD_GAME:
app_event_send(APP_EVT_VOICE_GAME, NULL, 0);
break;
case VB_CMD_BACK_HOME:
app_event_send(APP_EVT_VOICE_BACK_HOME, NULL, 0);
break;
default:
ESP_LOGW(TAG, "未知语音命令: 0x%02X", cmd);
break;
}
}
void voice_vb6824_task(void *arg)
{
uint8_t buf[64];
while (1) {
int len = uart_read_bytes(
VB6824_UART_NUM,
buf,
sizeof(buf),
pdMS_TO_TICKS(100)
);
if (len < 5) {
continue;
}
/*
* 示例协议:
* 0xAA 0x55 LEN CMD CHECKSUM
*/
if (buf[0] == 0xAA && buf[1] == 0x55) {
uint8_t frame_len = buf[2];
uint8_t cmd = buf[3];
uint8_t checksum = buf[4];
uint8_t calc = vb_checksum(buf, 4);
if (calc == checksum) {
voice_dispatch_cmd(cmd);
} else {
ESP_LOGW(TAG, "VB6824 校验失败");
}
}
}
}
13. LVGL UI:卡片式首页
#include "lvgl.h"
#include "app_event.h"
static lv_obj_t *status_label;
static void on_homework_clicked(lv_event_t *e)
{
app_event_send(APP_EVT_KEY_CAMERA, NULL, 0);
}
static void on_english_clicked(lv_event_t *e)
{
app_event_send(APP_EVT_VOICE_ENGLISH, NULL, 0);
}
static void on_game_clicked(lv_event_t *e)
{
app_event_send(APP_EVT_VOICE_GAME, NULL, 0);
}
static lv_obj_t *create_menu_card(
lv_obj_t *parent,
const char *text,
int x,
int y,
lv_event_cb_t cb
)
{
lv_obj_t *btn = lv_btn_create(parent);
lv_obj_set_size(btn, 128, 52);
lv_obj_align(btn, LV_ALIGN_TOP_LEFT, x, y);
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, text);
lv_obj_center(label);
return btn;
}
void ui_show_home(void)
{
lv_obj_clean(lv_scr_act());
lv_obj_t *title = lv_label_create(lv_scr_act());
lv_label_set_text(title, "四博 A1 AI 拍学机");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 12);
create_menu_card(lv_scr_act(), "拍题识别", 18, 58, on_homework_clicked);
create_menu_card(lv_scr_act(), "英语跟读", 154, 58, on_english_clicked);
create_menu_card(lv_scr_act(), "绘本陪读", 18, 118, on_homework_clicked);
create_menu_card(lv_scr_act(), "AI 问答", 154, 118, on_homework_clicked);
create_menu_card(lv_scr_act(), "单词游戏", 18, 178, on_game_clicked);
create_menu_card(lv_scr_act(), "系统设置", 154, 178, on_game_clicked);
status_label = lv_label_create(lv_scr_act());
lv_label_set_text(status_label, "等待连接 AI 服务...");
lv_obj_align(status_label, LV_ALIGN_BOTTOM_MID, 0, -8);
}
void ui_show_status(const char *text)
{
if (status_label) {
lv_label_set_text(status_label, text);
}
}
void ui_show_ai_card(const char *title, const char *content)
{
lv_obj_clean(lv_scr_act());
lv_obj_t *title_label = lv_label_create(lv_scr_act());
lv_label_set_text(title_label, title);
lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 12);
lv_obj_t *card = lv_obj_create(lv_scr_act());
lv_obj_set_size(card, 285, 185);
lv_obj_align(card, LV_ALIGN_CENTER, 0, 10);
lv_obj_t *text = lv_label_create(card);
lv_label_set_long_mode(text, LV_LABEL_LONG_WRAP);
lv_obj_set_width(text, 260);
lv_label_set_text(text, content);
lv_obj_align(text, LV_ALIGN_TOP_LEFT, 8, 8);
}
14. 英语跟读请求
void ai_ws_send_english_request(const char *sentence)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "english_repeat");
cJSON_AddStringToObject(root, "device_id", A1_DEVICE_ID);
cJSON_AddStringToObject(root, "sentence", sentence);
cJSON_AddStringToObject(root, "grade", "primary");
cJSON_AddBoolToObject(root, "score_enable", true);
ai_ws_send_json(root);
cJSON_Delete(root);
}
云端返回示例:
{
"type": "english_score",
"score": 88,
"comment": "整体发音不错,apple 的 p 音可以更清晰。",
"next_sentence": "I have an apple."
}
15. 单词游戏本地逻辑
typedef struct {
const char *word;
const char *options[4];
uint8_t answer;
} word_quiz_t;
static word_quiz_t s_quiz[] = {
{
.word = "apple",
.options = {"苹果", "香蕉", "学校", "铅笔"},
.answer = 0
},
{
.word = "book",
.options = {"椅子", "书", "小狗", "蓝色"},
.answer = 1
},
{
.word = "school",
.options = {"学校", "桌子", "汽车", "牛奶"},
.answer = 0
}
};
static int s_quiz_index = 0;
void game_word_start(void)
{
s_quiz_index = 0;
ui_show_quiz(
s_quiz[s_quiz_index].word,
s_quiz[s_quiz_index].options,
4
);
}
void game_word_answer(uint8_t index)
{
if (index == s_quiz[s_quiz_index].answer) {
ui_show_status("回答正确!");
audio_player_play_local("/spiffs/right.mp3");
} else {
ui_show_status("再想一想哦");
audio_player_play_local("/spiffs/wrong.mp3");
}
s_quiz_index++;
if (s_quiz_index >= sizeof(s_quiz) / sizeof(s_quiz[0])) {
s_quiz_index = 0;
}
ui_show_quiz(
s_quiz[s_quiz_index].word,
s_quiz[s_quiz_index].options,
4
);
}
16. OTA Manifest 方式升级
建议不要只下发一个固件 URL,而是使用 Manifest。
{
"version": "1.0.8",
"force": false,
"firmware": "https://cdn.example.com/a1/a1_v1.0.8.bin",
"resource": "https://cdn.example.com/a1/res_v1.0.8.zip",
"changelog": [
"优化拍题识别速度",
"新增英语跟读评分",
"修复部分网络重连问题"
]
}
OTA 代码:
#include "esp_https_ota.h"
#include "esp_log.h"
#include "esp_system.h"
static const char *TAG = "OTA";
void ota_service_start(const char *firmware_url)
{
ui_show_status("正在升级固件...");
esp_http_client_config_t http_config = {
.url = firmware_url,
.timeout_ms = 15000,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &http_config,
};
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ui_show_status("升级成功,正在重启");
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
} else {
ui_show_status("升级失败");
ESP_LOGE(TAG, "OTA failed: %s", esp_err_to_name(ret));
}
}
17. sdkconfig.defaults
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=8192
CONFIG_LWIP_TCP_WND_DEFAULT=8192
CONFIG_LWIP_TCP_RECVMBOX_SIZE=16
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP_WIFI_TX_BUFFER_TYPE_DYNAMIC=y
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
18. 总结
这套架构的重点不是简单地把摄像头、屏幕和喇叭接到 ESP32-S3 上,而是把整机设计成一个清晰的事件驱动系统:
VB6824 识别语音命令
→ 发送事件
→ 状态机切换到拍照状态
→ 摄像头任务异步采集图片
→ WebSocket 分片上传图片
→ 云端 AI 返回讲解内容
→ LVGL 显示结果
→ TTS 播放讲解
采用这种方式,A1 AI 智能拍学机可以同时兼顾:
拍题识别
绘本陪读
英语跟读
AI 问答
趣味游戏
离线语音控制
小程序配网
OTA 升级
知识库扩展
对于量产项目而言,事件驱动架构比简单 demo 式写法更稳定,也更方便后续扩展新功能。