回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

小智语音控制ESP32 S3双电机舵机方向舵仰望小车|极简MCP插件教程 简单

头像 rzyzzxw 2025.08.14 32 0

8.14

 

【任务目标】

前面有两个帖子,记录了用ESP32 S3开发板控制舵机和双电机的学习过程。其实目标是将两者整合起来实现仰望小车的控制。

硬件整合之后的仰望小车,电线布满了身体。

难看无所谓了,母不嫌子丑。

 

b20edd303678e710c831ab3908ab30b.jpg

为了保证供电,电源不冲突,开发板、舵机、电机都是单独供电,线是真乱啊。

077e58af10d90e2b75788d4acf2fbc4.jpg

编程环境配置:

让小智语音控制ESP32 S3点灯|极简MCP插件教程- Makelog(造物记)

材料清单

  • K10小智AI X1
  • ESP32 S3-DevKitC-1开发板(N16R8) X1
  • L298N电机驱动模块 X1
  • 乐高电机 X2
  • 乐高舵机 X1
  • 仰望小车底盘 X1
  • 锂电池组合 X1

步骤1 电路连接

基本电路是在上一帖子的电路上增加舵机接在IO5。

下图是ESP32 S3+小车双电机+L298N+舵机接线:

在IO5引脚增加舵机,舵机也是单独供电。

image.png
image.png
d11797e0133edd40cac58169984dc14.jpg

步骤2 代码编写

在原来双电机小车代码基础上修改,增加了舵机控制。

经过一番测试修改,得到如下代码。

实现预想的全部功能:

​核心功能架构​​

​硬件驱动层​

​电机控制​​:通过L298N库驱动双直流电机(ENA/IN1/IN2控制左轮,ENB/IN3/IN4控制右轮),支持前进、后退、转向和停止。

​舵机控制​​:使用ESP32Servo库驱动舵机(SERVO_PIN),实现转向角度调节(0°-180°),转向时自动调整舵机角度(左转60°、右转120°、直行90°)。

​网络通信层​

​WiFi连接​​:通过WiFi库连接指定AP(SSID: CMCC-QuYY),为WebSocket通信提供网络基础。

​WebSocket协议​​:通过WebSocketMCP库与远程服务器(api.xiaozhi.me)建立长连接,实现低延迟双向指令传输。

​控制逻辑层​

​状态机模型​​:定义CarState枚举(STOP/FORWARD/BACKWARD/LEFT/RIGHT),管理小车运动状态。

​多模式执行​​:

​持续模式​​:默认状态,需显式发送stop指令终止动作。

​限时模式​​:通过duration参数设定动作持续时间(毫秒),超时后自动停止。

​​核心控制指令​​
​​car_control运动控制指令​​
​​功能​​:控制小车基础运动(前进、后退、转向、停止)。
​​指令格式​​(JSON):
{
"action": "forward/backward/left/right/stop",
"duration": 整数(单位:ms,0表示持续执行),
"speed": 整数(0-255,可选)
}
​​参数说明​​:
action:动作类型,必填。
forward:前进(舵机归中90°)
backward:后退(舵机归中90°)
left:左转(舵机转向60°)
right:右转(舵机转向120°)
stop:停止(清除超时标志)
duration:动作持续时间(毫秒),默认0(持续执行直至新指令)。
speed:临时速度值(0-255),缺省时使用当前全局速度值。
​​执行逻辑​​:
舵机角度随转向动作自动调整,直行/后退时归中。
若设置duration>0,启动超时计时器,到期自动停止。
​​set_speed速度调节指令​​
​​功能​​:动态调整电机PWM占空比(全局速度)。
​​指令格式​​(JSON):
{ "speed": 整数(0-255) }
​​参数说明​​:
speed:必填,范围0(停止)至255(全速)。
​​执行效果​​:
立即更新全局速度变量currentSpeed。
若小车正在运动(非STOP状态),实时调整电机转速。

上面视频中并没有展示出速度调节功能,运动中也可以调速并且马上执行。

代码
#include <WiFi.h>
#include <WebSocketMCP.h>
#include <L298N.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>

// 电机引脚定义
#define ENA 14
#define IN1 12
#define IN2 13
#define ENB 17
#define IN3 15
#define IN4 16
#define SERVO_PIN 5

// 硬件对象初始化
L298N leftMotor(ENA, IN1, IN2);
L298N rightMotor(ENB, IN3, IN4);
Servo myServo;

// 网络配置
const char* ssid = "******";
const char* password = "******";
const char* mcpEndpoint = "ws://api.xiaozhi.me/mcp/?token=******";
WebSocketMCP mcpClient;

// 控制状态机
enum CarState { STOP, FORWARD, BACKWARD, LEFT, RIGHT };
CarState currentState = STOP;
int currentSpeed = 150;  // 默认速度(0-255)

// 超时控制核心变量
unsigned long actionStartTime = 0;   // 当前动作开始时间
int actionDuration = 0;              // 用户设定持续时间(0=持续执行)
unsigned long lastCmdTime = 0;       // 最后有效指令时间戳

// 函数前置声明
void registerMcpTools();
void controlCar(CarState state, int speed = -1);
void setServoAngle(int angle);

// 连接状态回调
void onConnectionStatus(bool connected) {
  if (connected) {
    Serial.println("[MCP] 已连接到服务器");
    registerMcpTools();
  } else {
    Serial.println("[MCP] 与服务器断开连接");
  }
}

// 舵机控制(带变化检测)
void setServoAngle(int angle) {
  static int lastAngle = -1;
  angle = constrain(angle, 0, 180);
  if (angle != lastAngle) {
    myServo.write(angle);
    Serial.printf("舵机: %d°\n", angle);
    lastAngle = angle;
  }
}

// 核心控制函数
void controlCar(CarState state, int speed) {
  if (speed >= 0) currentSpeed = speed;
  
  // 舵机转向逻辑
  switch (state) {
    case FORWARD:
    case BACKWARD:
    case STOP: setServoAngle(90); break;  // 直行方向
    case LEFT: setServoAngle(60); break; // 左转角度
    case RIGHT: setServoAngle(120); break; // 右转角度
  }

  // 电机驱动逻辑
  switch (state) {
    case FORWARD:
      leftMotor.setSpeed(currentSpeed);
      rightMotor.setSpeed(currentSpeed);
      leftMotor.forward();
      rightMotor.forward();
      break;
    case BACKWARD:
      leftMotor.setSpeed(currentSpeed);
      rightMotor.setSpeed(currentSpeed);
      leftMotor.backward();
      rightMotor.backward();
      break;
    case LEFT:
    case RIGHT:
      leftMotor.setSpeed(currentSpeed);
      rightMotor.setSpeed(currentSpeed);
      leftMotor.forward();
      rightMotor.forward();
      break;
    case STOP:
      leftMotor.stop();
      rightMotor.stop();
      actionDuration = 0; // 清除超时标志
      break;
  }
  
  // 状态更新与调试输出
  currentState = state;
  const char* stateNames[] = {"停止", "前进", "后退", "左转", "右转"};
  Serial.printf("动作: %s | 速度: %d | 模式: %s\n", 
                stateNames[state],
                currentSpeed,
                actionDuration > 0 ? "限时模式" : "持续模式");
}

// 注册MCP控制工具
void registerMcpTools() {
  // 运动控制工具
  mcpClient.registerTool(
    "car_control",
    "控制小车运动(forward/backward/left/right/stop)",
    "{\"type\":\"object\",\"properties\":{"
    "\"action\":{\"type\":\"string\",\"enum\":[\"forward\",\"backward\",\"left\",\"right\",\"stop\"]},"
    "\"duration\":{\"type\":\"integer\",\"description\":\"持续时间(ms,0表示持续执行)\"},"
    "\"speed\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255}}"
    ",\"required\":[\"action\"]}",
    [](const String& args) {
      DynamicJsonDocument doc(256);
      deserializeJson(doc, args);
      
      String action = doc["action"].as<String>();
      int duration = doc["duration"] | 0;  // 默认0=持续执行
      int speed = doc["speed"] | -1;

      // 指令转换
      CarState targetState = STOP;
      if (action == "forward") targetState = FORWARD;
      else if (action == "backward") targetState = BACKWARD;
      else if (action == "left") targetState = LEFT;
      else if (action == "right") targetState = RIGHT;

      // 超时参数记录
      if (duration > 0) {
        actionStartTime = millis();
        actionDuration = duration;
      } else {
        actionDuration = 0; // 持续执行模式
      }

      // 执行动作
      controlCar(targetState, speed);
      
      // 关键修复:收到指令时刷新安全计时器
      lastCmdTime = millis();
      
      return WebSocketMCP::ToolResponse("{\"status\":\"executing\",\"action\":\"" + action + "\"}");
    }
  );
  
  // 速度调节工具
  mcpClient.registerTool(
    "set_speed",
    "设置小车速度(0-255)",
    "{\"type\":\"object\",\"properties\":{\"speed\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255}},\"required\":[\"speed\"]}",
    [](const String& args) {
      DynamicJsonDocument doc(128);
      deserializeJson(doc, args);
      currentSpeed = doc["speed"];
      
      if (currentState != STOP) {
        controlCar(currentState);
      }
      // 刷新安全计时器
      lastCmdTime = millis();
      return WebSocketMCP::ToolResponse("{\"speed\":" + String(currentSpeed) + "}");
    }
  );
}

void setup() {
  Serial.begin(115200);
  
  // 硬件初始化序列
  myServo.setPeriodHertz(50);
  myServo.attach(SERVO_PIN, 500, 2500);
  setServoAngle(90);  // 舵机归中
  delay(500);         // 确保舵机稳定

  // 电机初始状态
  controlCar(STOP);
  
  // WiFi连接
  Serial.printf("连接WiFi: %s\n", ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi已连接! IP: %s\n", WiFi.localIP().toString());

  // MCP客户端启动
  mcpClient.begin(mcpEndpoint, onConnectionStatus);
  lastCmdTime = millis(); // 初始化安全计时器
}

void loop() {
  mcpClient.loop();
  
  // 模式1:限时动作检测
  if (actionDuration > 0 && currentState != STOP) {
    if (millis() - actionStartTime >= actionDuration) {
      controlCar(STOP);
      Serial.println("[系统] 动作时间到达");
      actionDuration = 0;
    }
  }
  
  // 安全保护:30秒无新指令停止
  if (millis() - lastCmdTime > 30000 && currentState != STOP) {
    controlCar(STOP);
    Serial.println("[安全] 30秒无指令自动停车");
  }
  
  delay(10);
}

上传代码,测试通过。

小智后台:

image.png

【小结】

1、整体效果很满意,达到全部预设功能。

2、基础代码自己改写,瓶颈问题由DeepSeek协助优化。

3、实践是检验的唯一标准,反复修改与调试,看似枯燥无味却又乐趣无穷。

评论

user-avatar