ESP32 手機遙控車:不用 App,打開瀏覽器就能開
ESP32 內建 Wi‑Fi,可以自己當熱點、自己跑網頁伺服器。這篇讓你的小車變成手機遙控車:手機連上車子的 Wi‑Fi,打開一個網址就有方向鍵和速度滑桿。
✓ 本文程式已在 2026/09/29 以 Arduino ESP32 開發板套件 3.3.12 編譯通過(開啟全部編譯警告,0 個警告),開發板選 ESP32 Dev Module。
1. 需要的東西
完全沿用 ESP32 避障小車 的車體、L298N 與接線,超音波感測器可以留著不接。沒做過避障小車的話,先照那篇的第 1 與第 3 節把馬達接好。
2. 運作原理
- 熱點模式(AP):
WiFi.softAP()讓 ESP32 開出名為 TinyBot-Car 的 Wi‑Fi,預設網址固定是192.168.4.1。 - 網頁伺服器:
WebServer在 80 埠提供遙控頁面。頁面本身存在 ESP32 的快閃記憶體裡,不需要網路。 - 指令:按住方向鍵時,手機每 200 毫秒送一次
/cmd?c=F這類請求;放開就送停止。 - 失聯保護:超過 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;
}
}
4. 使用方式
- 上傳程式,打開序列埠監控視窗(115200 baud),會看到「熱點已開啟」。
- 手機 Wi‑Fi 連 TinyBot-Car,輸入你設定的密碼。
- 手機可能跳出「此網路沒有網際網路」,選保持連線。
- 瀏覽器打開
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。熱點模式的好處是帶到哪裡都能玩。