【ESP32-S3】WiFi 网页遥控 + WebSocket 实时控制编码电机+ 实时数据显示 + 急停 + 速度滑块

WiFi 网页遥控 + WebSocket 实时控制 + 实时数据显示 + 急停 + 速度滑块

ESP32-S3、L298N、两个编码电机

代码

cpp 复制代码
// ==================== 必备头文件 ====================
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <esp_timer.h>
#include <ArduinoJson.h>

// ==================== 引脚定义 ====================
#define IN1     1
#define IN2     2
#define ENA     3
#define IN3     4
#define IN4     5
#define ENB     6

#define ENC_LA  7
#define ENC_LB  8
#define ENC_RA  9
#define ENC_RB 10

#define BAT_ADC 11

// ==================== WiFi 设置 ====================
const char* ssid     = "你的WiFi名称";
const char* password = "你的WiFi密码";

// ==================== 机械参数 ====================
const float PULSE_PER_ROUND = 44.0;
const float WHEEL_DIAMETER  = 65.0;
const float WHEEL_DISTANCE  = 140.0;  // 两轮间距,必须改!

const float DIST_PER_PULSE  = (PI * WHEEL_DIAMETER) / PULSE_PER_ROUND;
const float TURN_CIRCUM     = PI * WHEEL_DISTANCE;
const float PULSE_PER_DEG   = TURN_CIRCUM / 360.0 / DIST_PER_PULSE;

// ==================== 编码器 ====================
volatile long enc_left  = 0;
volatile long enc_right = 0;

// ==================== PID 参数 ====================
float Kp = 5.5;
float Ki = 0.7;
float Kd = 0.2;

// ==================== 控制模式 ====================
#define MODE_STOP       0
#define MODE_SPEED      1
#define MODE_DISTANCE   2
#define MODE_ANGLE      3

int mode = MODE_STOP;

float target_speed = 80.0;   // 基础速度
float target_dist  = 0.0;
long pulse_angle   = 0;
long pulse_sum     = 0;
float total_dist   = 0.0;

float speedL = 0, speedR = 0;
float voltage = 0.0;

float eL, last_eL, iL, outL;
float eR, last_eR, iR, outR;

// ==================== WEB ====================
WebServer server(80);
WebSocketsServer webSocket(81);

esp_timer_handle_t pid_timer;

// ==================== 编码器中断 ====================
void IRAM_ATTR isr_left() {
  enc_left += (digitalRead(ENC_LA) != digitalRead(ENC_LB)) ? 1 : -1;
}

void IRAM_ATTR isr_right() {
  enc_right += (digitalRead(ENC_RA) != digitalRead(ENC_RB)) ? 1 : -1;
}

// ==================== 电机驱动 ====================
void motorL(int pwm) {
  pwm = constrain(pwm, -255, 255);
  if (pwm >= 0) { digitalWrite(IN1, 1); digitalWrite(IN2, 0); analogWrite(ENA, pwm); }
  else         { digitalWrite(IN1, 0); digitalWrite(IN2, 1); analogWrite(ENA, -pwm); }
}

void motorR(int pwm) {
  pwm = constrain(pwm, -255, 255);
  if (pwm >= 0) { digitalWrite(IN3, 1); digitalWrite(IN4, 0); analogWrite(ENB, pwm); }
  else         { digitalWrite(IN3, 0); digitalWrite(IN4, 1); analogWrite(ENB, -pwm); }
}

void stop_all() {
  motorL(0); motorR(0);
  mode = MODE_STOP;
  target_speed = 0;
}

// ==================== 动作函数 ====================
void forward()  { mode = MODE_SPEED; target_speed = abs(target_speed); }
void backward() { mode = MODE_SPEED; target_speed = -abs(target_speed); }

void turn_left()  { mode = MODE_SPEED; motorL(-abs(target_speed)*0.7); motorR(abs(target_speed)*0.7); }
void turn_right() { mode = MODE_SPEED; motorL(abs(target_speed)*0.7); motorR(-abs(target_speed)*0.7); }

void go_dist(float mm) {
  mode = MODE_DISTANCE; total_dist = 0; target_dist = mm; target_speed = abs(target_speed);
}

void turn_angle_left(float ang) {
  mode = MODE_ANGLE; pulse_sum = 0; pulse_angle = ang * PULSE_PER_DEG;
  motorL(-abs(target_speed)*0.6); motorR(abs(target_speed)*0.6);
}

void turn_angle_right(float ang) {
  mode = MODE_ANGLE; pulse_sum = 0; pulse_angle = ang * PULSE_PER_DEG;
  motorL(abs(target_speed)*0.6); motorR(-abs(target_speed)*0.6);
}

// ==================== 电池电压 ====================
float readBattery() {
  int val = analogRead(BAT_ADC);
  return val * (3.3 / 4095.0) * 2.0;
}

// ==================== PID 定时 50ms ====================
void IRAM_ATTR onTick(void* arg) {
  speedL = enc_left;
  speedR = enc_right;
  enc_left = 0; enc_right = 0;

  total_dist += (abs(speedL) + abs(speedR)) * DIST_PER_PULSE / 2.0;
  if (mode == MODE_ANGLE) pulse_sum += abs(speedL) + abs(speedR);

  voltage = readBattery();

  if (mode == MODE_SPEED || mode == MODE_DISTANCE) {
    eL = target_speed - speedL;
    iL += eL; iL = constrain(iL, -200, 200);
    outL = Kp*eL + Ki*iL + Kd*(eL-last_eL); last_eL = eL;

    eR = target_speed - speedR;
    iR += eR; iR = constrain(iR, -200, 200);
    outR = Kp*eR + Ki*iR + Kd*(eR-last_eR); last_eR = eR;

    motorL(outL); motorR(outR);
  }

  if (mode == MODE_DISTANCE && total_dist >= target_dist) stop_all();
  if (mode == MODE_ANGLE && pulse_sum >= pulse_angle) stop_all();
}

void init_timer() {
  esp_timer_create_args_t args = {.callback = onTick};
  esp_timer_create(&args, &pid_timer);
  esp_timer_start_periodic(pid_timer, 50000);
}

// ==================== WEB 界面 ====================
String getHTML() {
  return R"HTML(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 小车遥控</title>
<style>
  body{text-align:center;font-size:22px;margin-top:20px;font-family:Arial}
  .btn{width:90px;height:90px;font-size:28px;margin:6px}
  .big{width:200px;height:70px;font-size:20px;margin:5px}
  .stop{background:red;color:white;font-weight:bold}
  .info{margin:20px auto;width:90%;max-width:380px;text-align:left;padding:10px;border:1px solid #ccc}
  .slider-box{margin:20px auto;width:90%;max-width:380px}
  input[type="range"]{width:100%;height:30px}
</style>
</head>
<body>
<h1>ESP32 智能小车</h1>

<div class="info">
  左轮速度:<span id="spL">0</span> pulse<br>
  右轮速度:<span id="spR">0</span> pulse<br>
  总距离:<span id="dist">0.0</span> cm<br>
  电池电压:<span id="bat">0.00</span> V<br>
  当前速度:<span id="speed_val">80</span>
</div>

<div class="slider-box">
  <label>速度调节:</label>
  <input type="range" id="speedSlider" min="40" max="120" value="80" oninput="setSpeed(this.value)">
</div>

<button class="btn" onmousedown="go('forward')" onmouseup="go('stop')">↑</button><br>
<button class="btn" onmousedown="go('left')" onmouseup="go('stop')">←</button>
<button class="btn" onmousedown="go('right')" onmouseup="go('stop')">→</button><br>
<button class="btn" onmousedown="go('back')" onmouseup="go('stop')">↓</button><br><br>

<button class="btn stop" onclick="go('stop')">🛑 急停</button><br><br>

<button class="big" onclick="go('90L')">左转90°</button>
<button class="big" onclick="go('90R')">右转90°</button><br>
<button class="big" onclick="go('180')">掉头180°</button><br>
<button class="big" onclick="go('50cm')">走50cm</button>
<button class="big" onclick="go('1m')">走1米</button>

<script>
let ws = new WebSocket("ws://" + location.hostname + ":81");
ws.onmessage = (e) => {
  let d = JSON.parse(e.data);
  document.getElementById("spL").innerText = d.speedL;
  document.getElementById("spR").innerText = d.speedR;
  document.getElementById("dist").innerText = (d.dist/10).toFixed(1);
  document.getElementById("bat").innerText = d.bat.toFixed(2);
};

function go(cmd) { ws.send(cmd); }

function setSpeed(val) {
  document.getElementById("speed_val").innerText = val;
  ws.send("SPEED:" + val);
}
</script>
</body>
</html>
  )HTML";
}

// ==================== WebSocket 指令处理 ====================
void handleWebSocket(uint8_t num, WStype_t type, uint8_t *payload, size_t len) {
  if (type != WStype_TEXT) return;
  String cmd = String((char*)payload);

  if (cmd == "forward") forward();
  else if (cmd == "back") backward();
  else if (cmd == "left") turn_left();
  else if (cmd == "right") turn_right();
  else if (cmd == "stop") stop_all();
  else if (cmd == "90L") turn_angle_left(90);
  else if (cmd == "90R") turn_angle_right(90);
  else if (cmd == "180") turn_angle_left(180);
  else if (cmd == "50cm") go_dist(500);
  else if (cmd == "1m") go_dist(1000);
  else if (cmd.startsWith("SPEED:")) {
    float s = cmd.substring(6).toFloat();
    target_speed = constrain(s, 40, 120);
  }
}

// ==================== 实时推送数据 ====================
void sendData() {
  static uint32_t prev = 0;
  if (millis() - prev < 100) return;
  prev = millis();

  StaticJsonDocument<200> doc;
  doc["speedL"] = speedL;
  doc["speedR"] = speedR;
  doc["dist"] = total_dist;
  doc["bat"] = voltage;

  String json;
  serializeJson(doc, json);
  webSocket.broadcastTXT(json);
}

// ==================== SETUP ====================
void setup() {
  Serial.begin(115200);

  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(ENA, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);
  pinMode(ENB, OUTPUT);

  pinMode(ENC_LA, INPUT_PULLUP);
  pinMode(ENC_LB, INPUT_PULLUP);
  pinMode(ENC_RA, INPUT_PULLUP);
  pinMode(ENC_RB, INPUT_PULLUP);
  pinMode(BAT_ADC, INPUT);

  attachInterrupt(digitalPinToInterrupt(ENC_LA), isr_left, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_RA), isr_right, CHANGE);

  init_timer();
  stop_all();

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.println("WiFi Connected");
  Serial.println("IP: " + WiFi.localIP().toString());

  server.on("/", [](){ server.send(200, "text/html", getHTML()); });
  server.begin();
  webSocket.begin();
  webSocket.onEvent(handleWebSocket);
}

void loop() {
  server.handleClient();
  webSocket.loop();
  sendData();
}

只需要改这 3 处

cpp 复制代码
const char* ssid     = "你的WiFi名称";
const char* password = "你的WiFi密码";

const float WHEEL_DISTANCE = 140.0;  // 两轮中心距离 mm

功能一览

  • Web 界面带 速度滑块(40~120 实时可调)
  • 实时显示:左轮速度、右轮速度、距离、电池电压
  • 按住方向键走,松开关停
  • 🛑 红色急停按钮
  • 一键 90°/180° 转向
  • 一键走 50cm / 1 米
  • 全程 PID 速度闭环,匀速稳定

需要安装的库(Arduino库管理器搜索)

  1. WebSocketsServer by Markus Sattler
  2. ArduinoJson by Benoit Blanchon

上传后打开串口监视器看 IP,浏览器输入 IP 即可遥控。

相关推荐
wzhidev2 小时前
05、Python流程控制与函数定义:从调试现场到工程实践
linux·网络·python
@haihi2 小时前
ESP32 MQTT示例解析
开发语言·网络·mqtt·github·esp32
艾莉丝努力练剑2 小时前
【Linux:文件】文件基础IO进阶
linux·运维·服务器·c语言·网络·c++·centos
程序猿编码2 小时前
基于ncurses的TCP连接可视化与重置工具:原理与实现(C/C++代码实现)
linux·c语言·网络·c++·tcp/ip
℡ 萧2 小时前
OSPF开销值、协议优先级及计时器的修改-新版(17)
网络·网络协议·网络安全·智能路由器·信息与通信
xyyaihxl3 小时前
C#数据库操作系列---SqlSugar完结篇
网络·数据库·c#
国冶机电安装3 小时前
电气安全保护装置:从设计选型到安装验收的全流程解析
服务器·网络·安全
阿捏利3 小时前
详解网络协议(二)OSI七层参考模型
网络·网络协议
松☆3 小时前
C++ 控制台通讯录管理系统 —— 从零实现到完整解析(附可运行代码)
开发语言·网络·c++