WiFi 网页遥控 + WebSocket 实时控制 + 实时数据显示 + 急停 + 速度滑块
- 代码
- [只需要改这 3 处](#只需要改这 3 处)
- 功能一览
- 需要安装的库(Arduino库管理器搜索)
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库管理器搜索)
- WebSocketsServer by Markus Sattler
- ArduinoJson by Benoit Blanchon
上传后打开串口监视器看 IP,浏览器输入 IP 即可遥控。