หุ่นยนต์ ESP32 บังคับผ่าน WiFi จากมือถือ ไม่ต้องลงแอป

ให้ ESP32 ปล่อย WiFi และเปิดหน้าเว็บปุ่มบังคับ กดค้างเพื่อขับ ปล่อยแล้วหยุด ส่งคำสั่งผ่าน WebSocket ให้ตอบสนองไว และหยุดเองเมื่อสัญญาณหลุด พร้อมโค้ดครบชุด

อัปเดต 2026-11-02

ESP32 มี WiFi ในตัว จึงทำรถบังคับจากมือถือได้โดยไม่ต้องติดตั้งแอปใดๆ: ให้ ESP32 ปล่อย WiFi ของตัวเอง (Access Point) มือถือต่อเข้าไปแล้วเปิดเบราว์เซอร์ที่ 192.168.4.1 จะเจอหน้าปุ่มบังคับ กดค้างเพื่อขับ ปล่อยแล้วหยุด คำสั่งวิ่งผ่าน WebSocket จึงตอบสนองไว และหุ่นหยุดเองเมื่อไม่ได้รับคำสั่งเกิน 0.3 วินาที

ลองขับหุ่น 2 ล้อในตัวจำลองก่อน →

หุ่นตัวนี้ทำงานอย่างไร

  1. ESP32 เป็น Access Point ปล่อย WiFi ชื่อ AI-Rover ไม่ต้องพึ่งเราเตอร์ เล่นที่ไหนก็ได้
  2. ESP32 เป็นเว็บเซิร์ฟเวอร์ ส่งหน้า HTML ที่มีปุ่มลูกศรให้มือถือ (พอร์ต 80)
  3. WebSocket ส่งคำสั่ง หน้าเว็บเปิดการเชื่อมต่อค้างไว้ (พอร์ต 81) แล้วส่งความเร็วล้อซ้ายขวา เช่น 1,1 ทุก 0.1 วินาทีขณะกดปุ่ม
  4. ตัวตัดความปลอดภัย ถ้า ESP32 ไม่ได้รับคำสั่งเกิน 300 ms (มือถือหลุดสัญญาณ ล็อกจอ หรือปิดหน้าเว็บ) ล้อจะหยุดทันที
ทำไมใช้ WebSocket ไม่ใช้ลิงก์ธรรมดา วิธีที่เห็นบ่อยคือให้ปุ่มแต่ละปุ่มเปิดลิงก์ เช่น /forward ทุกการกดต้องเปิดการเชื่อมต่อ HTTP ใหม่ ใช้เวลาหลายสิบถึงหลายร้อยมิลลิวินาที หุ่นจึงตอบช้าและกระตุก WebSocket เปิดค้างไว้ครั้งเดียว ส่งข้อความสั้นๆ ได้ทันที

ชิ้นส่วน

ชิ้นส่วนสำหรับ ESP32 DevKit V1
ชิ้นส่วนจำนวนราคาประมาณร้าน
ESP32 DevKit V1 (30 ขา)1฿130–฿250Shopee
TB6612FNG motor driver1฿50–฿120Shopee
ชุดแชสซี 2WD
มีมอเตอร์ TT 2 ตัว ล้อ 2 ล้อ และล้อ caster
1฿150–฿300Shopee
ถ่าน Li-ion 186502฿60–฿150Shopee
รางถ่าน 18650 แบบ 2 ก้อน (2S)1฿20–฿60Shopee
Buck converter 7.4 V → 5 V 3 A1฿30–฿90Shopee
สวิตช์เปิด-ปิด1฿5–฿20Shopee
สายจัมเปอร์ ผู้-ผู้ / ผู้-เมีย ชุด 40 เส้น1฿30–฿60Shopee
แนะนำเพิ่ม
ที่ชาร์จถ่าน 186501฿100–฿250Shopee
รวมโดยประมาณ฿535–฿1,200

ราคาประมาณการ ณ 2026-10-31 ยังไม่ได้ตรวจกับร้าน · บางลิงก์เป็นลิงก์ affiliate (อ่านเพิ่ม)

วงจร

แผนผังวงจรหุ่น 2 ล้อในตัวจำลอง ESP32 ต่อ TB6612FNG แบต 18650 สองก้อน และ buck converter 5 V
การต่อขาบน ESP32 DevKit V1 (จัดขาอัตโนมัติจากตัวจำลอง)
TB6612FNG
Motors
VM → แบต 7.4 V
VCC → 3V3
GND → GND
PWMA → GPIO25
AIN1 → GPIO27
AIN2 → GPIO14
PWMB → GPIO26
BIN1 → GPIO13
BIN2 → GPIO4
STBY → 3V3 (ต่อค้างไว้)

วงจรเดียวกับ บทความ TB6612FNG ไม่ต้องเพิ่มอะไร WiFi ดึงกระแสเป็นจังหวะสั้นๆ จึงยิ่งต้องแยกไฟมอเตอร์กับไฟบอร์ดให้ดี ถ้าบอร์ดรีเซ็ตตอนเริ่มขับ อ่าน ESP32 รีเซ็ตเองตอนมอเตอร์หมุน

โค้ด (Arduino IDE)

ต้องติดตั้งไลบรารี WebSockets ของ Markus Sattler จาก Library Manager (ค้นคำว่า WebSockets) ส่วน WiFi.h และ WebServer.h มากับ ESP32 core อยู่แล้ว

#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>   // ไลบรารี "WebSockets" ของ Markus Sattler

const char *SSID = "AI-Rover";
const char *PASS = "rover1234";            // อย่างน้อย 8 ตัวอักษร

const int PWMA = 25, AIN1 = 27, AIN2 = 14; // ล้อซ้าย
const int PWMB = 26, BIN1 = 13, BIN2 = 4;  // ล้อขวา
const int MAX_SPEED = 200;                 // ~78 % ของ 255 ถนอมมอเตอร์ TT
const unsigned long TIMEOUT_MS = 300;      // ไม่ได้รับคำสั่งนานเท่านี้ = หยุด

WebServer http(80);
WebSocketsServer ws(81);
unsigned long lastCommand = 0;

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">
<style>
  body { margin: 0; height: 100vh; display: grid; place-items: center; background: #111; color: #eee;
         font-family: sans-serif; touch-action: none; user-select: none; -webkit-user-select: none; }
  .pad { display: grid; grid-template-columns: repeat(3, 90px); gap: 12px; }
  button { height: 90px; border: 0; border-radius: 18px; background: #2a3550; color: #fff; font-size: 34px; }
  button:active { background: #4a6cf7; }
  p { text-align: center; }
</style></head><body>
<div>
  <div class="pad">
    <span></span><button data-m="1,1">▲</button><span></span>
    <button data-m="-0.6,0.6">◀</button><span></span><button data-m="0.6,-0.6">▶</button>
    <span></span><button data-m="-0.8,-0.8">▼</button><span></span>
  </div>
  <p id="status">กำลังเชื่อมต่อ...</p>
</div>
<script>
  const info = document.getElementById('status');
  const ws = new WebSocket('ws://' + location.hostname + ':81/');
  ws.onopen = () => info.textContent = 'พร้อมขับ: กดปุ่มค้างไว้';
  ws.onclose = () => info.textContent = 'หลุดการเชื่อมต่อ: รีเฟรชหน้านี้';
  let held = null;                      // คำสั่งของปุ่มที่กดค้างอยู่
  const send = m => ws.readyState === 1 && ws.send(m);
  setInterval(() => send(held || '0,0'), 100);   // ส่งซ้ำตลอด ESP32 จะรู้ว่ายังเชื่อมต่ออยู่
  document.querySelectorAll('button').forEach(b => {
    b.onpointerdown = e => { b.setPointerCapture(e.pointerId); held = b.dataset.m; send(held); };
    b.onpointerup = b.onpointercancel = () => { held = null; send('0,0'); };
  });
  document.addEventListener('visibilitychange', () => { held = null; send('0,0'); });
</script>
</body></html>)html";

// speed: -1.0 (ถอยเต็มที่) ถึง 1.0 (เดินหน้าเต็มที่)
void motor(int pwm, int in1, int in2, float speed) {
  int s = constrain((int)(speed * MAX_SPEED), -MAX_SPEED, MAX_SPEED);
  digitalWrite(in1, s > 0);
  digitalWrite(in2, s < 0);
  analogWrite(pwm, abs(s));
}

void drive(float left, float right) {
  motor(PWMA, AIN1, AIN2, left);
  motor(PWMB, BIN1, BIN2, right);
}

// ข้อความจากหน้าเว็บ: "ซ้าย,ขวา" เช่น "1,1" หรือ "-0.6,0.6"
void onWebSocket(uint8_t client, WStype_t type, uint8_t *data, size_t length) {
  if (type == WStype_DISCONNECTED) {
    drive(0, 0);
    return;
  }
  if (type != WStype_TEXT) return;
  float left, right;
  if (sscanf((const char *)data, "%f,%f", &left, &right) == 2) {
    drive(left, right);
    lastCommand = millis();
  }
}

void setup() {
  Serial.begin(115200);
  int pins[] = { PWMA, AIN1, AIN2, PWMB, BIN1, BIN2 };
  for (int p : pins) pinMode(p, OUTPUT);
  drive(0, 0);

  WiFi.softAP(SSID, PASS);
  Serial.print("ต่อ WiFi ");
  Serial.print(SSID);
  Serial.print(" แล้วเปิด http://");
  Serial.println(WiFi.softAPIP());   // ปกติคือ 192.168.4.1

  http.on("/", [] { http.send(200, "text/html", PAGE); });
  http.begin();
  ws.begin();
  ws.onEvent(onWebSocket);
}

void loop() {
  http.handleClient();
  ws.loop();
  if (millis() - lastCommand > TIMEOUT_MS) drive(0, 0);   // สัญญาณหลุด: หยุด
}

ใช้งาน

  1. อัปโหลดโค้ด เปิด Serial Monitor ที่ 115200 จะเห็นชื่อ WiFi และที่อยู่ของหน้าเว็บ
  2. ยกหุ่นให้ล้อลอยก่อนทดสอบครั้งแรก
  3. มือถือต่อ WiFi AI-Rover รหัส rover1234 ถ้ามือถือเตือนว่า WiFi นี้ไม่มีอินเทอร์เน็ต ให้เลือกใช้ต่อ
  4. เปิดเบราว์เซอร์ไปที่ http://192.168.4.1 รอจนขึ้น พร้อมขับ แล้วกดปุ่มค้าง
  5. ถ้ากดเดินหน้าแล้วล้อข้างหนึ่งถอย ให้สลับค่า AIN1 / AIN2 (หรือ BIN1 / BIN2) ในโค้ด

เรื่องความปลอดภัยที่ไม่ควรตัดออก

รถบังคับที่ไม่มีตัวตัดจะวิ่งต่อไปเรื่อยๆ เมื่อสัญญาณหลุดในจังหวะที่กำลังกดเดินหน้า จนชนหรือตกโต๊ะ โค้ดนี้หยุดล้อใน 3 กรณี:

ถ้าหุ่นหยุดๆ เดินๆ ตอนอยู่ไกล แปลว่าสัญญาณอ่อน ให้เพิ่ม TIMEOUT_MS เป็น 500 ms ได้ แต่ไม่ควรเกิน 1 วินาที

ต่อยอด

ลองในตัวจำลอง

ตัวจำลองไม่ได้จำลองหน้าเว็บบนมือถือ แต่ขับหุ่นตัวเดียวกันด้วยปุ่ม W A S D บนคีย์บอร์ดได้ ให้ความรู้สึกเหมือนกดค้างบนมือถือ ใช้ดูว่าหุ่นเลี้ยวกว้างแค่ไหนที่ความเร็วต่างๆ ก่อนปรับตัวเลข 0.6 ของปุ่มเลี้ยวในหน้าเว็บ

ขับหุ่นด้วย W A S D ในตัวจำลอง →

ฝังตัวจำลองนี้ในเว็บหรือบล็อกของคุณ

คัดลอกโค้ดนี้ไปวางใน HTML ของหน้าเว็บ ผู้อ่านจะเห็นสนามจำลองพร้อมปุ่มรันโปรแกรมตัวอย่าง

<iframe src="https://ai-rover.app/app/?preset=drive-basic&embed=1" title="ขับมอเตอร์ 2 ล้อด้วย TB6612FNG – AI Rover Studio" width="720" height="520" style="max-width:100%;border:0;border-radius:12px" loading="lazy"></iframe>