基于 ESP32-S3 + VB6824 的四博 A1 AI 智能拍学机方案:事件驱动架构、拍照识别与语音交互实现

基于 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 式写法更稳定,也更方便后续扩展新功能。

相关推荐
ting94520003 小时前
动手学深度学习(PyTorch版)深度详解(6):现代卷积神经网络-从经典模型到图像分类实战
人工智能·分类·cnn
@不误正业4 小时前
第12章-端侧AI操作系统概述
人工智能
Maynor9964 小时前
Codex 中国站正式上线!
人工智能·gpt·macos·github
qq_411262424 小时前
四博 CozyLife AI 中控方案:基于 ESP32-C5 双频 Wi-Fi + 4G 打造智能家居语音控制入口
人工智能·智能家居
Change is good4 小时前
桌面型软件(如UE)AI测试工具
人工智能
jkyy20144 小时前
AI赋能智慧座舱:健康有益重构移动健康空间,定义出行健康新范式
大数据·人工智能·物联网·健康医疗
superstarsupers4 小时前
宫庭海出席2026横琴-澳门国际数字艺术博览会 畅谈AI虚拟偶像产业新生态
人工智能·百度
2501_945837434 小时前
OpenClaw:重新定义 AI 执行边界的开源智能体框架
人工智能
沪漂阿龙在努力4 小时前
OpenAI Agents SDK 完全指南:从“只会动嘴”到“真正干活”的AI
人工智能