首頁 › 動手做 › ESP32 手機遙控車

ESP32 手機遙控車:不用 App,打開瀏覽器就能開

難度:入門時間:約 40 分鐘沿用避障小車硬體ESP32 套件 3.x

ESP32 內建 Wi‑Fi,可以自己當熱點、自己跑網頁伺服器。這篇讓你的小車變成手機遙控車:手機連上車子的 Wi‑Fi,打開一個網址就有方向鍵和速度滑桿。

✓ 本文程式已在 2026/09/29 以 Arduino ESP32 開發板套件 3.3.12 編譯通過(開啟全部編譯警告,0 個警告),開發板選 ESP32 Dev Module。

1. 需要的東西

完全沿用 ESP32 避障小車 的車體、L298N 與接線,超音波感測器可以留著不接。沒做過避障小車的話,先照那篇的第 1 與第 3 節把馬達接好。

2. 運作原理

  1. 熱點模式(AP):WiFi.softAP() 讓 ESP32 開出名為 TinyBot-Car 的 Wi‑Fi,預設網址固定是 192.168.4.1。
  2. 網頁伺服器:WebServer 在 80 埠提供遙控頁面。頁面本身存在 ESP32 的快閃記憶體裡,不需要網路。
  3. 指令:按住方向鍵時,手機每 200 毫秒送一次 /cmd?c=F 這類請求;放開就送停止。
  4. 失聯保護:超過 0.6 秒沒收到指令就自動停車。手機斷線、切到別的 App 時,車子不會暴衝。

3. 程式

先把開頭的 AP_PASS 改成你自己的密碼,至少 8 個字元。

/*
 * TinyBot Forge — 用手機遙控的 ESP32 機器人
 * https://tinybotforge.com
 *
 * ESP32 自己開一個 Wi-Fi 熱點,手機連上後打開 http://192.168.4.1 就能遙控。
 * 不需要路由器、不需要 App。
 * 硬體與接線沿用「ESP32 避障小車」(L298N + 兩顆 TT 馬達)。
 * 環境:Arduino IDE 2.x + ESP32 開發板套件 3.x
 */
#include <WiFi.h>
#include <WebServer.h>

// ---------- Wi-Fi 熱點設定(密碼至少 8 碼,請改成你自己的) ----------
const char *AP_SSID = "TinyBot-Car";
const char *AP_PASS = "tinybot123";

// ---------- 馬達腳位(與避障小車相同) ----------
const int PIN_ENA = 25, PIN_IN1 = 26, PIN_IN2 = 27;
const int PIN_ENB = 33, PIN_IN3 = 32, PIN_IN4 = 13;
const int PWM_FREQ = 1000, PWM_BITS = 8;
const unsigned long FAILSAFE_MS = 600;  // 超過這麼久沒收到指令就自動停車

WebServer server(80);
int speedValue = 180;
unsigned long lastCommandMs = 0;

void setMotor(int pinEn, int pinA, int pinB, int speed) {
  speed = constrain(speed, -255, 255);
  digitalWrite(pinA, speed > 0 ? HIGH : LOW);
  digitalWrite(pinB, speed < 0 ? HIGH : LOW);
  ledcWrite(pinEn, abs(speed));
}

void drive(int left, int right) {
  setMotor(PIN_ENA, PIN_IN1, PIN_IN2, left);
  setMotor(PIN_ENB, PIN_IN3, PIN_IN4, right);
}

// 遙控頁面:按住按鈕時每 200 ms 送一次指令,放開就送停止
const char PAGE[] PROGMEM = R"HTML(
<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>TinyBot 遙控器</title>
<style>
body{font-family:sans-serif;background:#1b1d1f;color:#eee;text-align:center;margin:0;padding:20px;user-select:none;-webkit-user-select:none}
.pad{display:grid;grid-template-columns:repeat(3,90px);gap:12px;justify-content:center;margin:24px auto}
button{height:90px;font-size:34px;border:0;border-radius:18px;background:#e8551c;color:#fff;touch-action:none}
button.stop{background:#555}
input{width:80%}
</style></head><body>
<h2>TinyBot 遙控器</h2>
<div class="pad">
<span></span><button data-c="F">▲</button><span></span>
<button data-c="L">◀</button><button class="stop" data-c="S">■</button><button data-c="R">▶</button>
<span></span><button data-c="B">▼</button><span></span>
</div>
<p>速度 <span id="v">180</span></p>
<input type="range" min="100" max="255" value="180" id="spd">
<script>
let timer=null;
function send(c){fetch('/cmd?c='+c+'&s='+document.getElementById('spd').value).catch(()=>{});}
function start(c){stop();send(c);if(c!=='S')timer=setInterval(()=>send(c),200);}
function stop(){if(timer){clearInterval(timer);timer=null;}}
document.querySelectorAll('button').forEach(b=>{
  b.addEventListener('pointerdown',e=>{e.preventDefault();start(b.dataset.c);});
  ['pointerup','pointerleave','pointercancel'].forEach(t=>b.addEventListener(t,()=>{if(timer){stop();send('S');}}));
});
document.getElementById('spd').oninput=e=>document.getElementById('v').textContent=e.target.value;
</script></body></html>
)HTML";

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

void handleCommand() {
  String c = server.arg("c");
  if (server.hasArg("s")) speedValue = constrain(server.arg("s").toInt(), 100, 255);
  int s = speedValue;
  int turn = s * 3 / 4;

  if (c == "F")      drive(s, s);
  else if (c == "B") drive(-s, -s);
  else if (c == "L") drive(-turn, turn);
  else if (c == "R") drive(turn, -turn);
  else               drive(0, 0);

  lastCommandMs = millis();
  server.send(200, "text/plain", "ok");
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_IN1, OUTPUT); pinMode(PIN_IN2, OUTPUT);
  pinMode(PIN_IN3, OUTPUT); pinMode(PIN_IN4, OUTPUT);
  ledcAttach(PIN_ENA, PWM_FREQ, PWM_BITS);
  ledcAttach(PIN_ENB, PWM_FREQ, PWM_BITS);
  drive(0, 0);

  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.print("熱點已開啟,手機連上後開啟 http://");
  Serial.println(WiFi.softAPIP());

  server.on("/", handleRoot);
  server.on("/cmd", handleCommand);
  server.begin();
}

void loop() {
  server.handleClient();
  // 失聯保護:手機斷線或畫面切走時自動停車
  if (lastCommandMs != 0 && millis() - lastCommandMs > FAILSAFE_MS) {
    drive(0, 0);
    lastCommandMs = 0;
  }
}
下載 .ino 檔

4. 使用方式

  1. 上傳程式,打開序列埠監控視窗(115200 baud),會看到「熱點已開啟」。
  2. 手機 Wi‑Fi 連 TinyBot-Car,輸入你設定的密碼。
  3. 手機可能跳出「此網路沒有網際網路」,選保持連線。
  4. 瀏覽器打開 http://192.168.4.1,按住方向鍵開車。

5. 常見問題

打不開 192.168.4.1

手機可能因為「沒有網路」自動切回行動數據。關掉行動數據再試,或確認網址開頭是 http:// 而不是 https。

按著方向鍵車子卻一頓一頓的

Wi‑Fi 訊號太弱或周圍干擾多。先在 3 公尺內測試;也可以把 FAILSAFE_MS 調高到 1000。

左右方向相反

對調那一側馬達的兩條線,或把程式裡 L 與 R 的 drive() 參數互換。

可以用家裡的 Wi‑Fi 嗎?

可以,把 WiFi.softAP() 改成 WiFi.begin("家裡SSID", "密碼"),再從序列埠看 ESP32 拿到的 IP。熱點模式的好處是帶到哪裡都能玩。

免費領取:ESP32 機器人零件與接線速查表

一張 A4 看懂 ESP32 安全腳位、馬達驅動與感測器接法,可以列印貼在工作桌旁。

免費下載速查表 PDF