ESP32-S3-CAM:豆包语音识别文字后控制小车(终章)——语音控制+L298N模块驱动小车

1、组装好小车

1)L198N连接两个轮子,然后用胶带粘到车架上的,比较丑陋,先组装起来

2)供电:我是用充电宝给ESP32-S3供电;用电池仓给L298N供电

3)L298N接线:

左轮:图中黑色胶带对应的那边

左轮:OUT3 接左电机左侧金属片 和OUT4 接左电机右侧金属片,控制线 接IN3 和IN4 分别插入G5和G6引脚

右轮:OUT1 接右电机左侧金属片 和OUT2 接右电机右侧金属片,控制线 接IN1 和IN2 分别插入G17和G18引脚

板子GND和电池仓负极接L298N的接地(图中灰褐色线与电池仓黑色负极)

接好后的小车如下:

2、写程序控制小车

第一步:语音转文字逻辑

已经在第四篇中搞定,代码可以直接复制ESP32-S3-CAM:豆包语音识别文字后控制小车(四)------增加mic拾音后通过豆包语音识别后转文字输出

第二步:增加前进后退控制逻辑代码

ESP32-S3-CAM:豆包语音识别文字后控制小车(五)------认识L298N驱动模块

我们在第五篇的时候,已经用kimi写了一个电机的控制逻辑,再让豆包实现两个电机的控制逻辑

前进,后退的逻辑。

2.1 先实现两个电机前进,后退,左转,右转

接着第五篇的kimi对话,继续给提示词:

现在我接了两个电机,左电机:控制线 接IN3 和IN4 分别插入G5和G6引脚

右电机:控制线 接IN1 和IN2 分别插入G17和G18引脚,

每次输入命令后,前进,后退,运行3秒,左转,右转 只运行 1秒

更新下代码

然后就可以实现 :"命令: 1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止"

代码如下:

cpp 复制代码
// ESP32-S3-CAM + L298N 双电机控制(定时自动停止)
// 左电机: IN3->GPIO5, IN4->GPIO6
// 右电机: IN1->GPIO17, IN2->GPIO18

#define LEFT_IN3  5    // 左电机 IN3 -> GPIO5
#define LEFT_IN4  6    // 左电机 IN4 -> GPIO6
#define RIGHT_IN1 17   // 右电机 IN1 -> GPIO17
#define RIGHT_IN2 18   // 右电机 IN2 -> GPIO18

unsigned long stopTime = 0;  // 记录停止时间点
bool isRunning = false;      // 电机运行状态

void setup() {
  Serial.begin(115200);
  
  pinMode(LEFT_IN3, OUTPUT);
  pinMode(LEFT_IN4, OUTPUT);
  pinMode(RIGHT_IN1, OUTPUT);
  pinMode(RIGHT_IN2, OUTPUT);
  
  stopAll();
  
  Serial.println("ESP32-S3 双电机控制已启动");
  Serial.println("命令: 1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止");
}

void loop() {
  // 检查是否需要自动停止
  if (isRunning && millis() >= stopTime) {
    stopAll();
    isRunning = false;
    Serial.println("■ 自动停止");
  }
  
  // 处理串口命令
  if (Serial.available() > 0) {
    char cmd = Serial.read();
    if (cmd == '\n' || cmd == '\r') return;
    
    switch (cmd) {
      case '1': 
        forward();  
        setTimer(3000);  // 3秒后停止
        Serial.println("↑ 前进 3秒");
        break;
        
      case '0': 
        backward(); 
        setTimer(3000);  // 3秒后停止
        Serial.println("↓ 后退 3秒");
        break;
        
      case '2': 
        turnLeft(); 
        setTimer(1000);  // 1秒后停止
        Serial.println("↺ 左转 1秒");
        break;
        
      case '3': 
        turnRight();
        setTimer(1000);  // 1秒后停止
        Serial.println("↻ 右转 1秒");
        break;
        
      case 's': 
      case 'S': 
        stopAll(); 
        isRunning = false;
        Serial.println("■ 立即停止");
        break;
        
      default: 
        Serial.println("未知命令");
        Serial.println("1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止");
    }
  }
}

// 设置定时器
void setTimer(unsigned long duration) {
  stopTime = millis() + duration;
  isRunning = true;
}

// 前进: 双电机正转
void forward() {
  digitalWrite(LEFT_IN3, HIGH);  digitalWrite(LEFT_IN4, LOW);
  digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}

// 后退: 双电机反转
void backward() {
  digitalWrite(LEFT_IN3, LOW);   digitalWrite(LEFT_IN4, HIGH);
  digitalWrite(RIGHT_IN1, LOW);  digitalWrite(RIGHT_IN2, HIGH);
}

// 左转: 左退右进
void turnLeft() {
  digitalWrite(LEFT_IN3, LOW);   digitalWrite(LEFT_IN4, HIGH);
  digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}

// 右转: 左进右退
void turnRight() {
  digitalWrite(LEFT_IN3, HIGH);  digitalWrite(LEFT_IN4, LOW);
  digitalWrite(RIGHT_IN1, LOW);  digitalWrite(RIGHT_IN2, HIGH);
}

// 停止
void stopAll() {
  digitalWrite(LEFT_IN3, LOW);  digitalWrite(LEFT_IN4, LOW);
  digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, LOW);
}

测试一下:代码编译上传到板子后,按一下复位按钮,板子上那个RST那个黑色按钮

bash 复制代码
ESP32-S3 双电机控制已启动
命令: 1=前进(3秒) | 0=后退(3秒) | 2=左转(1秒) | 3=右转(1秒) | s=立即停止
↑ 前进 3秒
■ 自动停止
↺ 左转 1秒
■ 自动停止
↻ 右转 1秒
■ 自动停止
↓ 后退 3秒
■ 自动停止

2.2 实现按boot按钮后自动旋转

提示词:现在修改下代码,不用从终端输入,改成 按boot 按键后 自动前进3秒,左转2秒,前进3秒,右转2秒,前进3秒,然后后退3秒

这样就不用一直插着USB了,用充电宝供电后,按一下 板子上的boot按键,就可以自动跑了。

这一步,主要是为了给后续第三步,按boot后开始自动录音转文字功能做准备。

cpp 复制代码
// ESP32-S3-CAM + L298N 自动序列控制(Boot键触发)
// 左电机: IN3->GPIO5, IN4->GPIO6
// 右电机: IN1->GPIO17, IN2->GPIO18
// Boot键: GPIO0(ESP32-S3-CAM开发板上的Boot按钮)

#define LEFT_IN3  5    // 左电机 IN3 -> GPIO5
#define LEFT_IN4  6    // 左电机 IN4 -> GPIO6
#define RIGHT_IN1 17   // 右电机 IN1 -> GPIO17
#define RIGHT_IN2 18   // 右电机 IN2 -> GPIO18
#define BOOT_BUTTON 0  // Boot按键接GPIO0

// 状态定义
enum State {
  IDLE,           // 待机,等待按键
  FORWARD_1,      // 第一次前进3秒
  TURN_LEFT,      // 左转2秒
  FORWARD_2,      // 第二次前进3秒
  TURN_RIGHT,     // 右转2秒
  FORWARD_3,      // 第三次前进3秒
  BACKWARD        // 后退3秒
};

State currentState = IDLE;
unsigned long stateStartTime = 0;
bool sequenceRunning = false;

void setup() {
  Serial.begin(115200);
  
  pinMode(LEFT_IN3, OUTPUT);
  pinMode(LEFT_IN4, OUTPUT);
  pinMode(RIGHT_IN1, OUTPUT);
  pinMode(RIGHT_IN2, OUTPUT);
  pinMode(BOOT_BUTTON, INPUT_PULLUP);  // Boot键,内部上拉
  
  stopAll();
  
  Serial.println("ESP32-S3 自动序列控制已启动");
  Serial.println("按Boot键开始执行序列");
  Serial.println("序列: 前进3秒→左转2秒→前进3秒→右转2秒→前进3秒→后退3秒");
}

void loop() {
  // 检测Boot按键(低电平触发,带消抖)
  if (currentState == IDLE && digitalRead(BOOT_BUTTON) == LOW) {
    delay(50);  // 消抖
    if (digitalRead(BOOT_BUTTON) == LOW) {
      startSequence();
      // 等待按键释放
      while (digitalRead(BOOT_BUTTON) == LOW);
      delay(50);
    }
  }
  
  // 执行状态机
  if (sequenceRunning) {
    runStateMachine();
  }
}

// 启动序列
void startSequence() {
  sequenceRunning = true;
  currentState = FORWARD_1;
  stateStartTime = millis();
  forward();
  Serial.println("▶ 序列开始: 前进3秒");
}

// 状态机控制
void runStateMachine() {
  unsigned long elapsed = millis() - stateStartTime;
  
  switch (currentState) {
    case FORWARD_1:
      if (elapsed >= 3000) {  // 3秒到,转左转
        currentState = TURN_LEFT;
        stateStartTime = millis();
        turnLeft();
        Serial.println("↺ 左转2秒");
      }
      break;
      
    case TURN_LEFT:
      if (elapsed >= 2000) {  // 2秒到,转前进
        currentState = FORWARD_2;
        stateStartTime = millis();
        forward();
        Serial.println("↑ 前进3秒");
      }
      break;
      
    case FORWARD_2:
      if (elapsed >= 3000) {  // 3秒到,转右转
        currentState = TURN_RIGHT;
        stateStartTime = millis();
        turnRight();
        Serial.println("↻ 右转2秒");
      }
      break;
      
    case TURN_RIGHT:
      if (elapsed >= 2000) {  // 2秒到,转前进
        currentState = FORWARD_3;
        stateStartTime = millis();
        forward();
        Serial.println("↑ 前进3秒");
      }
      break;
      
    case FORWARD_3:
      if (elapsed >= 3000) {  // 3秒到,转后退
        currentState = BACKWARD;
        stateStartTime = millis();
        backward();
        Serial.println("↓ 后退3秒");
      }
      break;
      
    case BACKWARD:
      if (elapsed >= 3000) {  // 3秒到,序列结束
        stopAll();
        sequenceRunning = false;
        currentState = IDLE;
        Serial.println("■ 序列完成,等待再次按键");
      }
      break;
      
    default:
      break;
  }
}

// 电机控制函数
void forward() {
  digitalWrite(LEFT_IN3, HIGH);  digitalWrite(LEFT_IN4, LOW);
  digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}

void backward() {
  digitalWrite(LEFT_IN3, LOW);   digitalWrite(LEFT_IN4, HIGH);
  digitalWrite(RIGHT_IN1, LOW);  digitalWrite(RIGHT_IN2, HIGH);
}

void turnLeft() {
  digitalWrite(LEFT_IN3, LOW);   digitalWrite(LEFT_IN4, HIGH);
  digitalWrite(RIGHT_IN1, HIGH); digitalWrite(RIGHT_IN2, LOW);
}

void turnRight() {
  digitalWrite(LEFT_IN3, HIGH);  digitalWrite(LEFT_IN4, LOW);
  digitalWrite(RIGHT_IN1, LOW);  digitalWrite(RIGHT_IN2, HIGH);
}

void stopAll() {
  digitalWrite(LEFT_IN3, LOW);  digitalWrite(LEFT_IN4, LOW);
  digitalWrite(RIGHT_IN1, LOW); digitalWrite(RIGHT_IN2, LOW);
}

2.3 实现语音转文字后控制电机的功能

用之前语音转文字的代码和前面电机控制的代码,让kimi综合写了一份控制代码

本来也是计划让他可以通过按boot按键后就录音转文字然后执行动作的,但始终没调通

最后放弃了,还是先确保输入audio命令后,每次都能够正常运行,最终调试通的代码如下

调试过程中发现,网络不稳定会严重导致websocket连接速度变慢,程序容易出错,尽量先确保网络稳定,后续可以优化代码逻辑。

cpp 复制代码
/*
 * ESP32-S3 豆包ASR语音识别 - 双电机语音控制小车版(定时版)
 * 支持: 1) SD卡文件识别  2) 麦克风实时识别  3) 语音控制小车
 * 
 * 硬件: GOOUUU ESP32-S3-CAM v1.3
 * SD卡: CLK=39, CMD=38, D0=40
 * 麦克风(I2S): WS=GPIO1, SCK=GPIO2, SD=GPIO42
 * 左电机(L298N): IN3=GPIO5, IN4=GPIO6
 * 右电机(L298N): IN1=GPIO17, IN2=GPIO18
 */

#include <WiFi.h>
#include <WebSocketsClient.h>
#include <SD_MMC.h>
#include <ArduinoJson.h>
#include <driver/i2s.h>

// ============== 配置区域 输入你的信息==============
const char* ssid = "";
const char* password = "";

// 豆包ASR配置
const char* appid = "";
const char* token = "";
const char* cluster = "volcengine_input_common";
const char* ws_host = "openspeech.bytedance.com";
const int ws_port = 443;
const char* ws_path = "/api/v2/asr";

// SD卡引脚
#define SD_CLK  39
#define SD_CMD  38
#define SD_D0   40

// 麦克风I2S引脚
#define I2S_WS      1
#define I2S_SCK     2
#define I2S_SD      42
#define I2S_PORT    I2S_NUM_0

// ===== 双电机控制引脚定义 =====
#define LEFT_IN3    5   // 左电机IN3
#define LEFT_IN4    6   // 左电机IN4
#define RIGHT_IN1   17  // 右电机IN1
#define RIGHT_IN2   18  // 右电机IN2

// ===== 动作持续时间配置(毫秒) =====
#define DURATION_FORWARD    3000  // 前进3秒
#define DURATION_BACKWARD   3000  // 后退3秒
#define DURATION_TURN       2000  // 转向2秒

// 音频配置
const int audio_rate = 16000;
const int audio_bits = 16;
const int audio_channel = 2;

// 分片大小
const size_t CHUNK_SIZE = 64000;

// 协议常量
#define PROTOCOL_VERSION 0x01
#define DEFAULT_HEADER_SIZE 0x01
#define CLIENT_FULL_REQUEST 0x01
#define CLIENT_AUDIO_ONLY_REQUEST 0x02
#define SERVER_FULL_RESPONSE 0x09
#define SERVER_ACK 0x0B
#define SERVER_ERROR_RESPONSE 0x0F
#define NO_SEQUENCE 0x00
#define NEG_SEQUENCE 0x02
#define JSON_SERIALIZATION 0x01
#define NO_COMPRESSION 0x00

// ============== 全局变量 ==============
WebSocketsClient webSocket;
bool ws_connected = false;
bool asr_completed = false;
bool asr_running = false;
bool result_displayed = false;
String recognition_result = "";

// SD卡文件模式
File audio_file;
size_t audio_total_size = 0;
size_t audio_sent_size = 0;
String target_filename = "";

// 麦克风模式
bool mic_mode = false;
int16_t* mic_buffer = nullptr;
const int MIC_BUFFER_SAMPLES = 1024;
unsigned long mic_start_time = 0;
const unsigned long MIC_RECORD_DURATION = 5000;

// 小车控制状态
enum CarCommand {
    CMD_NONE,
    CMD_FORWARD,
    CMD_BACKWARD,
    CMD_LEFT,
    CMD_RIGHT,
    CMD_STOP
};
CarCommand last_command = CMD_NONE;

// ===== 新增:定时控制变量 =====
bool car_is_moving = false;           // 小车是否正在执行动作
unsigned long car_action_start = 0;   // 动作开始时间
unsigned long car_action_duration = 0;// 动作持续时间

// ============== 函数声明 ==============
bool initSD();
void listSDFiles();
void showPrompt();
void processCommand(String cmd);
bool loadAndRecognizeFile(String filename);
bool startMicRecognition();
void initWebSocket();
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length);
void sendFullClientRequest(bool from_mic);
void sendNextChunk();
void sendMicAudio();
void sendLastMicChunk();
void parseResponse(uint8_t* data, size_t len);
void generateHeader(uint8_t* header, uint8_t msg_type, uint8_t flags);
bool initI2SMic();
void stopI2SMic();

// ===== 双电机控制函数声明 =====
void initMotor();
void leftMotorForward();
void leftMotorBackward();
void leftMotorStop();
void rightMotorForward();
void rightMotorBackward();
void rightMotorStop();
void carForward();
void carBackward();
void carTurnLeft();
void carTurnRight();
void carStop();
CarCommand parseVoiceCommand(String text);
void executeCarCommand(CarCommand cmd);
void checkCarActionTimeout();  // 新增:检查动作是否超时

// ============== 协议头生成 ==============
void generateHeader(uint8_t* header, uint8_t msg_type, uint8_t flags) {
    header[0] = (PROTOCOL_VERSION << 4) | DEFAULT_HEADER_SIZE;
    header[1] = (msg_type << 4) | flags;
    header[2] = (JSON_SERIALIZATION << 4) | NO_COMPRESSION;
    header[3] = 0x00;
}

// ===== 双电机控制实现 =====
void initMotor() {
    pinMode(LEFT_IN3, OUTPUT);
    pinMode(LEFT_IN4, OUTPUT);
    pinMode(RIGHT_IN1, OUTPUT);
    pinMode(RIGHT_IN2, OUTPUT);
    
    carStop();
    Serial.println("[MOTOR] 双电机控制初始化完成 (左轮:5/6, 右轮:17/18)");
}

// 左电机控制
void leftMotorForward() {
    digitalWrite(LEFT_IN3, HIGH);
    digitalWrite(LEFT_IN4, LOW);
}

void leftMotorBackward() {
    digitalWrite(LEFT_IN3, LOW);
    digitalWrite(LEFT_IN4, HIGH);
}

void leftMotorStop() {
    digitalWrite(LEFT_IN3, LOW);
    digitalWrite(LEFT_IN4, LOW);
}

// 右电机控制
void rightMotorForward() {
    digitalWrite(RIGHT_IN1, HIGH);
    digitalWrite(RIGHT_IN2, LOW);
}

void rightMotorBackward() {
    digitalWrite(RIGHT_IN1, LOW);
    digitalWrite(RIGHT_IN2, HIGH);
}

void rightMotorStop() {
    digitalWrite(RIGHT_IN1, LOW);
    digitalWrite(RIGHT_IN2, LOW);
}

// ===== 修改:带定时的动作控制 =====
void carForward() {
    leftMotorForward();
    rightMotorForward();
    car_is_moving = true;
    car_action_start = millis();
    car_action_duration = DURATION_FORWARD;
    Serial.printf("🚗 [小车] 前进 ⬆️ (持续%d秒)\n", DURATION_FORWARD/1000);
}

void carBackward() {
    leftMotorBackward();
    rightMotorBackward();
    car_is_moving = true;
    car_action_start = millis();
    car_action_duration = DURATION_BACKWARD;
    Serial.printf("🚗 [小车] 后退 ⬇️ (持续%d秒)\n", DURATION_BACKWARD/1000);
}

void carTurnLeft() {
    leftMotorStop();
    rightMotorForward();
    car_is_moving = true;
    car_action_start = millis();
    car_action_duration = DURATION_TURN;
    Serial.printf("🚗 [小车] 左转 ↪️ (持续%d秒)\n", DURATION_TURN/1000);
}

void carTurnRight() {
    leftMotorForward();
    rightMotorStop();
    car_is_moving = true;
    car_action_start = millis();
    car_action_duration = DURATION_TURN;
    Serial.printf("🚗 [小车] 右转 ↩️ (持续%d秒)\n", DURATION_TURN/1000);
}

void carStop() {
    leftMotorStop();
    rightMotorStop();
    car_is_moving = false;
    car_action_duration = 0;
    Serial.println("🚗 [小车] 停止 ⏹️");
}

// 检查动作是否超时(在loop中调用)
void checkCarActionTimeout() {
    if (car_is_moving && (millis() - car_action_start >= car_action_duration)) {
        Serial.println("[小车] 动作时间到,自动停止");
        carStop();
    }
}

// 解析语音命令
CarCommand parseVoiceCommand(String text) {
    text.toLowerCase();
    
    if (text.indexOf("前进") >= 0 || text.indexOf("向前") >= 0 || 
        text.indexOf("往前") >= 0 || text.indexOf("走") >= 0 ||
        text.indexOf("直走") >= 0) {
        return CMD_FORWARD;
    }
    
    if (text.indexOf("后退") >= 0 || text.indexOf("向后") >= 0 || 
        text.indexOf("往后") >= 0 || text.indexOf("退") >= 0) {
        return CMD_BACKWARD;
    }
    
    if (text.indexOf("左转") >= 0 || text.indexOf("向左") >= 0 || 
        text.indexOf("往左") >= 0 || text.indexOf("左") >= 0) {
        return CMD_LEFT;
    }
    
    if (text.indexOf("右转") >= 0 || text.indexOf("向右") >= 0 || 
        text.indexOf("往右") >= 0 || text.indexOf("右") >= 0) {
        return CMD_RIGHT;
    }
    
    if (text.indexOf("停止") >= 0 || text.indexOf("停") >= 0 || 
        text.indexOf("别动") >= 0 || text.indexOf("站住") >= 0 ||
        text.indexOf("站") >= 0) {
        return CMD_STOP;
    }
    
    return CMD_NONE;
}

// 执行小车命令
void executeCarCommand(CarCommand cmd) {
    // 如果正在执行动作,先停止
    if (car_is_moving) {
        carStop();
        delay(100);  // 短暂停顿
    }
    
    switch(cmd) {
        case CMD_FORWARD:  carForward();  break;
        case CMD_BACKWARD: carBackward(); break;
        case CMD_LEFT:     carTurnLeft(); break;
        case CMD_RIGHT:    carTurnRight(); break;
        case CMD_STOP:     carStop();     break;
        default: break;
    }
}

// ============== 设置 ==============
void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("\n========================================");
    Serial.println("ESP32-S3 豆包ASR语音识别(双电机定时版)");
    Serial.println("========================================");
    Serial.println("硬件配置:");
    Serial.println("  左电机: GPIO5(IN3) / GPIO6(IN4)");
    Serial.println("  右电机: GPIO17(IN1) / GPIO18(IN2)");
    Serial.println("----------------------------------------");
    Serial.println("动作时长:");
    Serial.println("  前进/后退: 3秒");
    Serial.println("  左转/右转: 2秒");
    Serial.println("----------------------------------------");
    Serial.println("命令:");
    Serial.println("  r <文件名>  - 识别SD卡中的音频文件");
    Serial.println("  audio       - 从麦克风实时识别");
    Serial.println("  list        - 列出SD卡文件");
    Serial.println("  help        - 显示帮助");
    Serial.println("----------------------------------------");
    Serial.println("语音控制指令:");
    Serial.println("  \"前进/向前/走\"  - 小车前进3秒");
    Serial.println("  \"后退/向后/退\"  - 小车后退3秒");
    Serial.println("  \"左转/向左\"     - 小车左转2秒");
    Serial.println("  \"右转/向右\"     - 小车右转2秒");
    Serial.println("  \"停止/停/别动\"  - 立即停止");
    Serial.println("========================================\n");

    // 初始化电机控制
    initMotor();

    // 初始化SD卡
    if (!initSD()) {
        Serial.println("⚠️ SD卡初始化失败,文件模式不可用");
    }

    // 连接WiFi
    Serial.print("[WiFi] 连接 ");
    Serial.print(ssid);
    Serial.print(" ...");
    WiFi.begin(ssid, password);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 30) {
        delay(500);
        Serial.print(".");
        retry++;
    }
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println(" ❌ 失败");
        while (1) delay(1000);
    }
    Serial.println(" ✅ 已连接");
    Serial.print("[WiFi] IP: ");
    Serial.println(WiFi.localIP());

    // 初始化麦克风缓冲区
    mic_buffer = (int16_t*)malloc(MIC_BUFFER_SAMPLES * sizeof(int16_t));
    if (!mic_buffer) {
        Serial.println("⚠️ 麦克风缓冲区分配失败");
    }

    showPrompt();
}

// ============== 主循环 ==============
void loop() {
    webSocket.loop();

    // ===== 新增:检查小车动作超时 =====
    checkCarActionTimeout();

    // 处理串口命令
    if (Serial.available()) {
        String cmd = Serial.readStringUntil('\n');
        cmd.trim();
        if (cmd.length() > 0) {
            processCommand(cmd);
        }
    }

    // 麦克风模式:采集5秒后自动结束
    if (mic_mode && ws_connected && !asr_completed) {
        if (millis() - mic_start_time >= MIC_RECORD_DURATION) {
            Serial.println("\n[MIC] 5秒录音时间到,停止采集");
            sendLastMicChunk();
            mic_mode = false;
        } else {
            sendMicAudio();
        }
    }

    // 识别完成处理
    if (asr_completed && !result_displayed) {
        result_displayed = true;

        Serial.println("\n========================================");
        Serial.println("🎉 ASR识别完成!");
        Serial.println("----------------------------------------");
        Serial.print("📢 识别文本: ");
        Serial.println(recognition_result);
        
        // 解析并执行语音命令
        CarCommand cmd = parseVoiceCommand(recognition_result);
        if (cmd != CMD_NONE) {
            Serial.println("----------------------------------------");
            Serial.println("🎮 检测到控制指令!");
            executeCarCommand(cmd);
            last_command = cmd;
        } else {
            Serial.println("💬 非控制指令,忽略");
        }
        
        Serial.println("========================================");

        webSocket.setReconnectInterval(1000);

        if (audio_file) audio_file.close();
        if (mic_buffer) stopI2SMic();

        asr_running = false;
        mic_mode = false;

        showPrompt();
    }
}

// ============== 显示提示符 ==============
void showPrompt() {
    Serial.println("\n[等待命令] > ");
}

// ============== 处理命令 ==============
void processCommand(String cmd) {
    cmd.toLowerCase();

    if (cmd.startsWith("r ")) {
        String filename = cmd.substring(2);
        filename.trim();
        if (!filename.startsWith("/")) filename = "/" + filename;
        result_displayed = false;
        loadAndRecognizeFile(filename);
    }
    else if (cmd == "audio") {
        result_displayed = false;
        initWebSocket();
        startMicRecognition();
    }
    else if (cmd == "list") {
        listSDFiles();
        showPrompt();
    }
    else if (cmd == "forward" || cmd == "f") {
        carForward();
        showPrompt();
    }
    else if (cmd == "backward" || cmd == "b") {
        carBackward();
        showPrompt();
    }
    else if (cmd == "left" || cmd == "l") {
        carTurnLeft();
        showPrompt();
    }
    else if (cmd == "right" || cmd == "r") {
        carTurnRight();
        showPrompt();
    }
    else if (cmd == "stop" || cmd == "s") {
        carStop();
        showPrompt();
    }
    else if (cmd == "help") {
        Serial.println("命令:");
        Serial.println("  r <文件名>  - 识别SD卡音频文件 (如: r audio1.wav)");
        Serial.println("  audio       - 从麦克风实时识别");
        Serial.println("  list        - 列出SD卡文件");
        Serial.println("  forward/f   - 前进3秒");
        Serial.println("  backward/b  - 后退3秒");
        Serial.println("  left/l      - 左转2秒");
        Serial.println("  right/r     - 右转2秒");
        Serial.println("  stop/s      - 立即停止");
        Serial.println("  help        - 显示帮助");
        showPrompt();
    }
    else {
        Serial.println("❌ 未知命令,输入 help 查看帮助");
        showPrompt();
    }
}

// ============== SD卡初始化 ==============
bool initSD() {
    Serial.println("[SD] 初始化SD卡...");
    SD_MMC.setPins(SD_CLK, SD_CMD, SD_D0);
    if (!SD_MMC.begin("/sdcard", true)) {
        Serial.println("[SD] ❌ 失败");
        return false;
    }
    uint8_t type = SD_MMC.cardType();
    Serial.print("[SD] 类型: ");
    if (type == CARD_SD) Serial.println("SD");
    else if (type == CARD_SDHC) Serial.println("SDHC");
    else if (type == CARD_MMC) Serial.println("MMC");
    else Serial.println("未知");
    Serial.printf("[SD] 容量: %llu MB\n", SD_MMC.cardSize() / 1048576);
    return true;
}

// ============== 列出SD卡文件 ==============
void listSDFiles() {
    File root = SD_MMC.open("/");
    if (!root || !root.isDirectory()) {
        Serial.println("[SD] ❌ 无法打开目录");
        return;
    }
    Serial.println("\n[SD] 文件列表:");
    int count = 0;
    File f = root.openNextFile();
    while (f) {
        if (!f.isDirectory()) {
            Serial.printf("  [FILE] %-20s %6d KB\n", f.name(), f.size() / 1024);
            count++;
        }
        f = root.openNextFile();
    }
    Serial.printf("[SD] 共 %d 个文件\n", count);
}

// ============== 加载并识别文件 ==============
bool loadAndRecognizeFile(String filename) {
    if (asr_running) {
        Serial.println("❌ 当前有识别任务在运行,请等待完成");
        return false;
    }

    Serial.printf("\n[文件] 准备识别: %s\n", filename.c_str());

    if (!SD_MMC.exists(filename)) {
        Serial.println("❌ 文件不存在");
        showPrompt();
        return false;
    }

    audio_file = SD_MMC.open(filename, FILE_READ);
    if (!audio_file) {
        Serial.println("❌ 无法打开文件");
        showPrompt();
        return false;
    }

    audio_total_size = audio_file.size();
    audio_sent_size = 0;
    target_filename = filename;
    mic_mode = false;

    Serial.printf("[文件] 大小: %d bytes (%.1f KB)\n", audio_total_size, audio_total_size / 1024.0);

    asr_completed = false;
    asr_running = true;
    recognition_result = "";

    Serial.println("[ASR] 连接服务器...");
    initWebSocket();

    return true;
}

// ============== 初始化麦克风 ==============
bool initI2SMic() {
    Serial.println("[MIC] 初始化I2S麦克风...");

    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
        .sample_rate = audio_rate,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 4,
        .dma_buf_len = 1024,
        .use_apll = false,
        .tx_desc_auto_clear = false,
        .fixed_mclk = 0
    };

    i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_SCK,
        .ws_io_num = I2S_WS,
        .data_out_num = I2S_PIN_NO_CHANGE,
        .data_in_num = I2S_SD
    };

    esp_err_t err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
    if (err != ESP_OK) {
        Serial.printf("[MIC] ❌ I2S驱动安装失败: %d\n", err);
        return false;
    }

    err = i2s_set_pin(I2S_PORT, &pin_config);
    if (err != ESP_OK) {
        Serial.printf("[MIC] ❌ I2S引脚设置失败: %d\n", err);
        i2s_driver_uninstall(I2S_PORT);
        return false;
    }

    Serial.println("[MIC] ✅ I2S麦克风初始化成功");
    return true;
}

// ============== 停止麦克风 ==============
void stopI2SMic() {
    i2s_driver_uninstall(I2S_PORT);
    Serial.println("[MIC] I2S已停止");
    mic_mode = false;
}

// ============== 启动麦克风识别 ==============
bool startMicRecognition() {
    if (asr_running) {
        Serial.println("❌ 当前有识别任务在运行,请等待完成");
        return false;
    }

    Serial.println("\n[MIC] 启动麦克风识别...");

    if (!initI2SMic()) {
        showPrompt();
        return false;
    }

    mic_mode = true;
    asr_completed = false;
    asr_running = true;
    recognition_result = "";
    mic_start_time = millis();

    Serial.println("[MIC] 请说话(5秒自动停止)...");

    return true;
}

// ============== WebSocket初始化 ==============
void initWebSocket() {
    Serial.println("[ASR] 连接服务器...");
    webSocket.beginSSL(ws_host, ws_port, ws_path);
    String auth = "Authorization: Bearer; ";
    auth += token;
    webSocket.setExtraHeaders(auth.c_str());
    webSocket.onEvent(webSocketEvent);
    webSocket.setReconnectInterval(5000);
}

// ============== WebSocket事件处理 ==============
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
    switch(type) {
        case WStype_DISCONNECTED:
            Serial.println("[WS] ❌ 断开");
            ws_connected = false;
            break;
        case WStype_CONNECTED:
            Serial.println("[WS] ✅ 已连接");
            ws_connected = true;
            sendFullClientRequest(mic_mode);
            break;
        case WStype_BIN:
            parseResponse(payload, length);
            break;
        case WStype_ERROR:
            Serial.println("[WS] ❌ 错误");
            break;
        default:
            break;
    }
}

// ============== 发送配置 ==============
void sendFullClientRequest(bool from_mic) {
    Serial.println("[ASR] 📤 发送配置...");

    JsonDocument doc;
    doc["app"]["appid"] = appid;
    doc["app"]["token"] = token;
    doc["app"]["cluster"] = cluster;
    doc["user"]["uid"] = "esp32_asr";
    doc["request"]["reqid"] = "esp32-" + String(millis());
    doc["request"]["nbest"] = 1;
    doc["request"]["workflow"] = "audio_in,resample,partition,vad,fe,decode,itn,nlu_punctuate";
    doc["request"]["show_utterances"] = false;
    doc["request"]["result_type"] = "full";
    doc["request"]["sequence"] = 1;
    doc["audio"]["format"] = from_mic ? "raw" : "wav";
    doc["audio"]["rate"] = audio_rate;
    doc["audio"]["language"] = "zh-CN";
    doc["audio"]["bits"] = audio_bits;
    doc["audio"]["channel"] = from_mic ? 1 : audio_channel;
    doc["audio"]["codec"] = "raw";

    String json;
    serializeJson(doc, json);

    size_t msg_len = 4 + 4 + json.length();
    uint8_t* msg = (uint8_t*)malloc(msg_len);
    generateHeader(msg, CLIENT_FULL_REQUEST, NO_SEQUENCE);
    msg[4] = (json.length() >> 24) & 0xFF;
    msg[5] = (json.length() >> 16) & 0xFF;
    msg[6] = (json.length() >> 8) & 0xFF;
    msg[7] = json.length() & 0xFF;
    memcpy(msg + 8, json.c_str(), json.length());

    webSocket.sendBIN(msg, msg_len);
    free(msg);

    Serial.println("[ASR] ✅ 配置已发送");

    if (!from_mic && audio_file) {
        delay(100);
        sendNextChunk();
    }
}

// ============== 发送文件下一片 ==============
void sendNextChunk() {
    if (!audio_file || !asr_running) return;

    size_t remaining = audio_total_size - audio_sent_size;
    if (remaining == 0) {
        Serial.println("[ASR] 文件读取完成,等待最终结果...");
        return;
    }

    size_t chunk = (remaining > CHUNK_SIZE) ? CHUNK_SIZE : remaining;
    bool is_last = (remaining <= CHUNK_SIZE);

    uint8_t* buffer = (uint8_t*)malloc(chunk);
    if (!buffer) {
        Serial.println("[ERR] ❌ 内存不足");
        return;
    }

    size_t read = audio_file.read(buffer, chunk);
    if (read != chunk) {
        Serial.println("[ERR] ❌ 读取失败");
        free(buffer);
        return;
    }

    size_t msg_len = 4 + 4 + chunk;
    uint8_t* msg = (uint8_t*)malloc(msg_len);
    uint8_t flags = is_last ? NEG_SEQUENCE : NO_SEQUENCE;
    generateHeader(msg, CLIENT_AUDIO_ONLY_REQUEST, flags);
    msg[4] = (chunk >> 24) & 0xFF;
    msg[5] = (chunk >> 16) & 0xFF;
    msg[6] = (chunk >> 8) & 0xFF;
    msg[7] = chunk & 0xFF;
    memcpy(msg + 8, buffer, chunk);

    webSocket.sendBIN(msg, msg_len);

    free(buffer);
    free(msg);

    audio_sent_size += chunk;

    Serial.printf("[ASR] 📤 已发送 %d/%d KB (%d%%)%s\n", 
        audio_sent_size / 1024, audio_total_size / 1024,
        audio_sent_size * 100 / audio_total_size,
        is_last ? " [最后一片]" : "");
}

// ============== 发送最后一片麦克风音频 ==============
void sendLastMicChunk() {
    if (!mic_buffer) return;

    Serial.println("[MIC] 发送结束标记...");

    size_t msg_len = 4 + 4;
    uint8_t* msg = (uint8_t*)malloc(msg_len);
    if (!msg) return;

    generateHeader(msg, CLIENT_AUDIO_ONLY_REQUEST, NEG_SEQUENCE);
    msg[4] = 0;
    msg[5] = 0;
    msg[6] = 0;
    msg[7] = 0;

    webSocket.sendBIN(msg, msg_len);
    free(msg);

    Serial.println("[MIC] 结束标记已发送,等待识别结果...");
}

// ============== 发送麦克风音频 ==============
void sendMicAudio() {
    if (!mic_buffer || !asr_running) return;

    size_t bytes_read = 0;
    esp_err_t err = i2s_read(I2S_PORT, mic_buffer, MIC_BUFFER_SAMPLES * sizeof(int16_t), &bytes_read, portMAX_DELAY);

    if (err != ESP_OK || bytes_read == 0) return;

    size_t msg_len = 4 + 4 + bytes_read;
    uint8_t* msg = (uint8_t*)malloc(msg_len);
    if (!msg) return;

    generateHeader(msg, CLIENT_AUDIO_ONLY_REQUEST, NO_SEQUENCE);
    msg[4] = (bytes_read >> 24) & 0xFF;
    msg[5] = (bytes_read >> 16) & 0xFF;
    msg[6] = (bytes_read >> 8) & 0xFF;
    msg[7] = bytes_read & 0xFF;
    memcpy(msg + 8, mic_buffer, bytes_read);

    webSocket.sendBIN(msg, msg_len);
    free(msg);

    static int mic_packets = 0;
    mic_packets++;
    if (mic_packets % 50 == 0) {
        Serial.print(".");
    }
}

// ============== 解析响应 ==============
void parseResponse(uint8_t* data, size_t len) {
    if (len < 8) return;

    uint8_t msg_type = data[1] >> 4;
    uint8_t serialization = data[2] >> 4;
    uint8_t header_size = data[0] & 0x0F;

    size_t payload_offset = header_size * 4;
    if (len < payload_offset + 4) return;

    int32_t payload_size = ((int32_t)data[payload_offset] << 24) |
                           ((int32_t)data[payload_offset + 1] << 16) |
                           ((int32_t)data[payload_offset + 2] << 8) |
                           ((int32_t)data[payload_offset + 3]);

    if (payload_size <= 0 || len < payload_offset + 4 + payload_size) return;

    uint8_t* payload = data + payload_offset + 4;

    if (serialization == JSON_SERIALIZATION) {
        JsonDocument doc;
        DeserializationError err = deserializeJson(doc, payload, payload_size);

        if (err) return;

        int code = doc["code"] | -1;
        int sequence = doc["sequence"] | 0;

        if (code == 1000) {
            JsonArray arr = doc["result"].as<JsonArray>();
            if (arr.size() > 0) {
                const char* txt = arr[0]["text"];
                if (txt && strlen(txt) > 0) {
                    recognition_result = String(txt);
                    Serial.printf("\n[识别] %s\n", txt);
                }
            }

            if (sequence < 0) {
                asr_completed = true;
                Serial.println("[ASR] 收到最终响应标记(sequence<0)");
                return;
            }

            if (!mic_mode && audio_file) {
                sendNextChunk();
            }

        } else {
            const char* msg = doc["message"] | "Unknown";
            Serial.printf("[ERR] %d: %s\n", code, msg);
            asr_running = false;
            asr_completed = true;
        }
    }
}

2.4 测试结果

终端输入:audio

然后对着mic说话 "小车前进"

日志输出如下:

bash 复制代码
[等待命令] > 
[ASR] 连接服务器...

[MIC] 启动麦克风识别...
[MIC] 初始化I2S麦克风...
[MIC] ✅ I2S麦克风初始化成功
[MIC] 请说话(5秒自动停止)...
[WS] ✅ 已连接
[ASR] 📤 发送配置...
[ASR] ✅ 配置已发送

[识别] 前
.
[识别] 前

[识别] 前

[识别] 前

[识别] 前

[识别] 前

[识别] 前

[识别] 前进

[识别] 前进

[识别] 前进

[识别] 前进

[识别] 前进

[识别] 前进小

[识别] 前进小

[识别] 前进小

[识别] 前进小

[识别] 前进小车

[识别] 前进小车

[识别] 前进小车

[识别] 前进小车

[识别] 前进小车

[识别] 前进小车

[识别] 前进小车前

[识别] 前进小车前

[识别] 前进小车前

[识别] 前进小车前进

[识别] 前进小车前进

[识别] 前进小车前进

[识别] 前进小车前进

[识别] 前进小车前进

[识别] 前进小车前进

[识别] 前进小车前进

[MIC] 5秒录音时间到,停止采集
[MIC] 发送结束标记...
[MIC] 结束标记已发送,等待识别结果...

[识别] 前进小车前进

[识别] 前进小车前进

[识别] 前进小车前进

[识别] 前进小车前进
[ASR] 收到最终响应标记(sequence<0)

========================================
🎉 ASR识别完成!
----------------------------------------
📢 识别文本: 前进小车前进
----------------------------------------
🎮 检测到控制指令!
🚗 [小车] 前进 ⬆️ (持续3秒)
========================================
[MIC] I2S已停止

[等待命令] > 
[小车] 动作时间到,自动停止
🚗 [小车] 停止 ⏹️

小车前进

相关推荐
测试架构师2 小时前
基于OpenCode与Wooyun Legacy的AI自动化安全测试实战
运维·人工智能·自动化
Agent产品评测局2 小时前
港口行业自动化平台选型,物流与通关全流程提效:2026智慧港口技术全景与实测指引
运维·人工智能·ai·chatgpt·自动化
Coovally AI模型快速验证2 小时前
无人机拍叶片→AI找缺陷:CEA-DETR改进RT-DETR做风电叶片表面缺陷检测,mAP50达89.4%
人工智能·3d·视觉检测·无人机·异常检测·工业质检
chatexcel2 小时前
ChatExcel AI工具测试报告:从 AI Excel、数据分析到 AI PPT 的完整闭环
人工智能·数据分析·excel
gaosushexiangji2 小时前
sCMOS相机在细胞生物学成像中的关键技术与系统应用
人工智能
chaofan9802 小时前
2026年企业级AI基建:AWS Bedrock高并发架构深度实践与成本治理实操录
人工智能·架构·aws
moonsims2 小时前
基于AiBrainBox-UGV的Smart RoBot系统架构&多Smart Robot协同架构:数据流 + 多机协同架构图
人工智能·数码相机·无人机
测试那点事儿2 小时前
Cursor AI技能提示词设计建议:构建全覆盖测试用例生成体系(测试用例设计场景安全性能篇)
人工智能·安全·测试用例·ai辅助测试
果汁华2 小时前
CLI 工具深度解析系列
人工智能