教程 · 中级 · 1.5 小时

用 ESP32 网页服务器在浏览器里遥控机器人

把 ESP32 变成一个 Wi-Fi 热点,由它自己提供控制页面,这样任何一部手机都能在浏览器里遥控你的机器人,无需安装任何 App。

引言

HC-05需要一个串口终端应用,还要跳一遍配对的舞。ESP32能做更好的事:**自己就是网络,自己就提供控制页面。**你把手机连到机器人的 Wi-Fi 上,打开浏览器,就可以开车。什么都不用装,而且任何带浏览器的设备都能用。

ESP32 片上就带 Wi-Fi 和蓝牙,双核 240 MHz,RAM 足够把一个网页放在闪存里再提供出来。整个技巧就是这个。

五阶段的竖向流程图。手机浏览器为前进这条路由发出一个 HTTP GET 请求。ESP32 作为软接入点或已加入某个网络,收到该请求。它的 HTTP 服务器把请求路由给一个处理函数。处理函数驱动电机驱动器的方向引脚和 PWM。轮子转动,机器人前进。旁边注解提醒:请求停止到达时,看门狗会停下电机。
从拇指到轮子之间的每一层。看门狗最重要:没有它,连接一断机器人就继续开。 下载 SVG

热点还是现有网络

两种模式,而选择比看上去重要得多。

热点(AP)——ESP32 自己建一个 Wi-Fi 网络,手机加入它。

  • 在任何地方都能用,不需要路由器。机器人是自给自足的。
  • 开车时手机会失去上网能力,而且有些手机会悄悄切回移动数据、把你踢下线。
  • 地址永远相同,通常是 192.168.4.1。

站点(STA)——ESP32 加入你现有的 Wi-Fi。

  • 手机保持上网。
  • 机器人的 IP 由路由器分配,每次启动都可能变。
  • 出了那个网络的范围就没用,这一条就足以否掉演示。

对于一台要随身带走的机器人,AP 模式几乎总是对的:

#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);

void setup() {
  WiFi.softAP("RobotCar", "drive1234");   // password must be 8+ characters
  Serial.println(WiFi.softAPIP());        // 192.168.4.1
  server.on("/", handleRoot);
  server.on("/cmd", handleCommand);
  server.begin();
}

void loop() {
  server.handleClient();
  applyDeadman();                          // see below — this is not optional
}

把控制页面提供出来

页面以原始字符串的形式住在闪存里。保持精简——每一个字节都要由一颗单片机来提供——并且把控件做得对触屏友好,因为用手机开车就是全部的意义所在。

const char PAGE[] PROGMEM = R"(
<!doctype html><meta name=viewport content="width=device-width,initial-scale=1">
<style>
  body{font-family:system-ui;margin:0;display:grid;place-items:center;height:100vh}
  button{width:90px;height:90px;font-size:24px;margin:4px;touch-action:manipulation}
</style>
<div>
  <div style="text-align:center"><button data-c="F">▲</button></div>
  <div><button data-c="L">◀</button><button data-c="S">■</button><button data-c="R">▶</button></div>
  <div style="text-align:center"><button data-c="B">▼</button></div>
</div>
<script>
let held = null;
const send = c => fetch('/cmd?c=' + c);
for (const b of document.querySelectorAll('button')) {
  // Repeat while held, so releasing and losing signal look the same to the robot.
  b.onpointerdown = e => { held = b.dataset.c; send(held);
                           b.setPointerCapture(e.pointerId); };
  b.onpointerup = b.onpointercancel = () => { held = null; send('S'); };
}
setInterval(() => held && send(held), 100);
</script>
)";

void handleRoot() { server.send_P(200, "text/html", PAGE); }

按钮上的 touch-action: manipulation 很关键:没有它,手机浏览器会等上约 300 ms 来判断你是不是想双击,于是机器人感觉像是坏了。

松手超时,再说一次

蓝牙教程里关于链路丢失说的一切在这里都适用,而且更严重——Wi-Fi 的覆盖范围是突然断掉,而不是优雅地断掉。

unsigned long lastCommandAt = 0;

void handleCommand() {
  char c = server.arg("c")[0];
  drive(c);
  lastCommandAt = millis();
  server.send(200, "text/plain", "ok");
}

void applyDeadman() {
  if (millis() - lastCommandAt > 400) stop();
}

浏览器每 100 ms 发送一次当前按住的指令,所以 400 ms 的超时会容忍三个丢掉的请求才停车。这个比例才是要调的东西:太紧,弱链路下机器人会一顿一顿;太松,你已经失去控制权它还在开。

什么时候该换到 WebSocket

上面那套 fetch 做法每条指令都新开一条 HTTP 连接。它简单,在浏览器的网络面板里也容易调试,每秒十条指令完全够用。

但当你需要回传遥测时,它就不够用了。用更多 HTTP 请求去轮询传感器数值会把流量翻倍,还给两个方向都加上延迟。WebSocket 保持一条连接常开,任何一方随时都能说话:

#include <WebSocketsServer.h>
WebSocketsServer ws(81);

void onWsEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t len) {
  if (type == WStype_TEXT) { drive(payload[0]); lastCommandAt = millis(); }
  if (type == WStype_DISCONNECTED) stop();   // the link closing IS a stop command
}

最后一行才是真正的那次升级。用 HTTP 时机器人只能从“没有动静”去推断连接丢了;WebSocket 会在连接关闭的那一刻告诉你,于是机器人立刻停下,而不用等超时。

先用 HTTP。等你希望机器人能回话时,再换到 WebSocket。

供电

ESP32 是一颗 3.3 V 器件,无线电发射时电流是尖锐的脉冲——峰值远超 200 mA。由此带来两个后果:

**不要从 Arduino 的 3.3 V 引脚给它供电。**那个稳压器供不上发射峰值,症状是 Wi-Fi 一忙板子就重启,看起来像代码缺陷而其实不是。

**它的 GPIO 引脚不耐 5 V。**任何从 5 V 逻辑把信号送进 ESP32 的东西都需要分压器或电平转换器,和 HC-05 的 RX 线一样。

从电池包经它自己的稳压器供电,并且把电机电源分开走。

开跑前先验证

  1. **把页面提供出来。**手机连上 RobotCar,打开 http://192.168.4.1,确认按键出现。
  2. **观察指令。**通过 USB 把每一条收到的指令打印出来。按住一个按键,确认重复指令以大约每秒 10 次到达。
  3. **掐断链路。**按住前进,然后把手机的 Wi-Fi 关掉。机器人必须在超时之内停下。

然后把它装进一台机器人里:手机遥控车项目把整件事都做了,无线电任选,底盘也可以自己开。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料