top of page
IMG_0959.heic

Web-Sumo Bot: การควบคุมหุ่นยนต์ซูโม่ผ่านเบราว์เซอร์ด้วย Web Interface


1. แนวคิดและการออกแบบ: เมื่อซอฟต์แวร์เป็นตัวกำหนดรูปร่าง

โปรเจกต์ Web-Sumo Bot เกิดจากไอเดียที่อยากทำหุ่นยนต์ที่ "ใครก็บังคับได้" โดยไม่ต้องมีรีโมทเฉพาะทาง แต่ใช้แค่สมาร์ทโฟนหรือ iPad ที่ทุกคนมีติดตัวอยู่แล้ว ผมจึงเลือกวางโครงสร้างทุกอย่างโดยให้ระบบ Web Interface เป็นตัวนำทาง

ในยุคที่เทคโนโลยี IoT (Internet of Things) เข้ามามีบทบาทมากขึ้น การพัฒนาหุ่นยนต์ไม่ได้จำกัดอยู่แค่การใช้รีโมทคอนโทรลแบบเดิมๆ อีกต่อไป บทความนี้จะพาไปทำความรู้จักกับ "Web-Sumo Bot" โปรเจกต์หุ่นยนต์ซูโม่จิ๋วที่รวมเอาศาสตร์ด้านวิศวกรรม การออกแบบ 3D และการพัฒนา Web Application เข้าไว้ด้วยกันใช้สำหรับแนวทางการสอนเด็กๆได้ครับ


Web-Sumo Bot
Web-Sumo Bot

2. โครงสร้างละการออกแบบตัวหุ่น (Hardware)

โครงสร้างหลักของ หุ่นยนต์ Web-Sumo Bot ถูกสร้างขึ้นด้วยเทคโนโลยี 3D Printing ซึ่งช่วยให้เราสามารถปรับเปลี่ยนอุปกรณ์เสริมตามกลยุทธ์การเล่นได้ทันทีและนำมาติดตั้งเข้ากับ บอร์ดFriendRobot Model HiFi ตัวหุ่นใช้ระบบขับเคลื่อน 2 ล้อหลัง โดยใช้ DC gear motor 3 - 6 V (1:48)พร้อมล้อหน้าแบบอิสระ ทำให้มีความคล่องตัวสูงในการหมุนตัวกลับลำในสนามวงกลม

โครงสร้างละการออกแบบตัวหุ่น
โครงสร้างละการออกแบบตัวหุ่น

3. ระบบการควบคุมและ Dashboard (Software & Control)

หัวใจสำคัญที่ทำให้หุ่นยนต์ตัวนี้ทำงานได้ คือการผสมผสานระหว่างซอฟต์แวร์ที่ฝังอยู่ในบอร์ด (Firmware) และหน้าเว็บควบคุม โดยแบ่งจุดเด่นออกเป็น 3 ส่วนหลัก:

  • การเชื่อมต่อไร้สาย (Wi-Fi Access Point): ตัวหุ่นยนต์จะทำหน้าที่เป็นแม่ข่ายปล่อยสัญญาณ Wi-Fi ออกมาเอง (ใช้คำสั่ง WiFi.softAP) ทำให้เราสามารถใช้ iPad หรือสมาร์ทโฟนเชื่อมต่อเข้ากับหุ่นยนต์ได้โดยตรง ไม่ต้องผ่านอินเทอร์เน็ตบ้าน เหมาะมากสำหรับการนำไปใช้สอนตามห้องเรียนหรือสนามแข่งที่ไม่มี Wi-Fi

  • อินเตอร์เฟซการควบคุม (Web Dashboard): หน้าจอควบคุมถูกออกแบบให้ใช้งานง่ายและตอบสนองเร็ว (Low Latency) มีปุ่มบังคับทิศทางที่ชัดเจน พร้อมแถบสไลเดอร์ปรับความเร็วได้แบบ Real-time ตั้งแต่ 0-60 และมีมาตรวัดดิจิทัลเพื่อบอกสถานะความเร็วปัจจุบัน ช่วยให้ผู้บังคับกะจังหวะการเคลื่อนที่ได้แม่นยำ

  • ฟังก์ชันเสริมและโหมดไม้ตาย (Action Features): เราได้เพิ่มฟังก์ชันที่ช่วยให้การแข่งซูโม่สนุกขึ้น เช่น NITRO

    อินเตอร์เฟซการควบคุม
    อินเตอร์เฟซการควบคุม

    (สามารถดูรายละเอียดโค้ดการทำงานทั้งหมดได้ที่ส่วนท้ายของบทความ)


4. ประสบการณ์การใช้งานจริง (Practical Application)

หลังจากที่พัฒนาทั้งตัวหุ่นและระบบ Dashboard เสร็จสิ้น ขั้นตอนที่สำคัญที่สุดคือการนำมาใช้งานจริงในสภาพแวดล้อมต่างๆ ซึ่งทำให้เราเห็นประสิทธิภาพของระบบในหลายมิติ:

  • การเชื่อมต่อที่เสถียรในพื้นที่เปิด: จากการใช้งานจริง ระบบ Wi-Fi Hotspot ของบอร์ด FriendRobot สามารถคลุมพื้นที่สนามแข่งซูโม่ได้อย่างทั่วถึง การควบคุมผ่าน iPad ให้ความรู้สึกไหลลื่น ไม่ต่างจากการใช้รีโมทวิทยุแบบเดิม แต่ได้เปรียบตรงที่สามารถดูสถานะความเร็วและปรับค่าได้ทันทีจากหน้าจอ

  • ความคล่องตัวในสนาม: เนื่องจากใช้มอเตอร์เกียร์ 1:48 คู่กับล้อหน้าแบบอิสระ ในการใช้งานจริงหุ่นยนต์สามารถหมุนตัวกลับลำ (U-Turn) ได้ในพื้นที่แคบๆ ซึ่งเป็นหัวใจสำคัญในการแข่งขันซูโม่เพื่อหลบหลีกการถูกผลักออกนอกสนาม

  • ความยืดหยุ่นของอุปกรณ์เสริม: ในการใช้งานจริงเราสามารถสลับ "โมดูลหน้า" ได้ตามคู่ต่อสู้ หากเจอหุ่นที่ตัวสูงเราจะใช้แขนคีบ (Gripper) เพื่อยกให้เสียสมดุล หรือหากเจอสายพุ่งชนเราจะใช้แผ่นผลัก (Shield) เพื่อรับแรงปะทะ ซึ่งการออกแบบด้วย 3D Print ทำให้การเปลี่ยนหน้างานทำได้รวดเร็วภายในเวลาไม่กี่นาที

  • สื่อการเรียนรู้ที่มีชีวิต: นอกเหนือจากการแข่งขัน เราได้ลองนำไปใช้เป็นสื่อการสอน พบว่าเด็กๆ ให้ความสนใจกับการเปลี่ยนสีหน้าจอ Dashboard และการสั่ง HORN หรือ DANCE มากกว่าการบังคับทิศทางเพียงอย่างเดียว ทำให้การเรียนรู้เรื่อง Coding กลายเป็นเรื่องที่สนุกและเข้าถึงง่ายสำหรับทุกคน

การใช้งานจริง
การใช้งานจริง

สรุปผลการพัฒนา

โปรเจกต์ Web-Sumo Bot นี้พิสูจน์ให้เห็นว่า การเรียนรู้เรื่องหุ่นยนต์ในปัจจุบันไม่จำเป็นต้องเริ่มจากสิ่งที่ยากเสมอไป การใช้บอร์ด FriendRobot Model HiFi ร่วมกับเทคโนโลยีเว็บเบราว์เซอร์ ช่วยลดกำแพงเรื่องอุปกรณ์ควบคุมลงได้มหาศาลและทุนในการสร้างหุ่นยนต์ไว้เล่นเอง ทำให้เด็กๆ หรือผู้ที่สนใจสามารถสนุกไปกับการเขียนโปรแกรม (Coding) และการออกแบบวิศวกรรม (Engineering) ได้พร้อมๆ กัน


สินค้าในบทความ

  • FriendRobot Model HiFi  

  • (DC gear motor 3 - 6 V 1:48) 

  • ล้ออิสระ

  • ดูเพิ่มเติมได้ที่ https://www.friendrobotshop.com/

  • สนใจสอบถามข้อมูลเพิ่มเติมเกี่ยวกับหุ่นยนต์เพื่อการศึกษา ติดต่อ LINE: [ID LINE @friendrobot]


    • NITRO Mode: ปุ่มกดเร่งพลังมอเตอร์ไปที่ 100% ชั่วขณะ เพื่อใช้พุ่งชนหรือเผด็จศึกคู่ต่อสู้

    • HORN & BEEP: ระบบส่งเสียงสัญญาณเพื่อสร้างสีสันในการแข่งขัน

    • AUTOPILOT: โหมดโปรแกรมอัตโนมัติที่ช่วยให้หุ่นยนต์ขยับเองได้ เพื่อเป็นแนวทางในการต่อยอดด้านวิทยาการหุ่นยนต์ (Robotics)

Full Source Code (Arduino)

#include <ModelHifi.h>  // เรียกใช้คำสั่งพื้นฐานของบอร์ด FriendRobot
#include <WiFi.h>       // เรียกใช้ระบบ Wi-Fi สำหรับสร้างจุดเชื่อมต่อ
#include <WebServer.h>  // เรียกใช้ระบบจัดการหน้าเว็บควบคุม

// --- ตั้งชื่อ Wi-Fi และ รหัสผ่าน ---
const char* ssid = "My_Robot_iPad4"; 
const char* password = "12345678";
int mySpeed = 30;
unsigned long nitroStartTime = 0; // เก็บเวลาที่เริ่มกดไนตรัส
bool isNitroActive = false;       // เช็คสถานะว่ากำลังใช้ไนตรัสอยู่ไหม
WebServer server(80); // กำหนดให้ Server ทำงานที่พอร์ต 80 (พอร์ตมาตรฐานเว็บ)
String carDirection = "STOP"; // ตัวแปรเก็บทิศทาง (STOP, FORWARD, BACKWARD, LEFT, RIGHT)
// --- ส่วนโครงสร้างหน้าเว็บ (HTML) ---
// ใช้ R"raw( ... )raw" เพื่อให้เขียน HTML ได้หลายบรรทัดโดยไม่ Error
String html = R"raw(
<!DOCTYPE html>
<html lang="th">
<head>
    <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, orientation=landscape'>
    <style>
        :root {
            --bg-color: #1e2124;
            --panel-color: #2f3136;
            --accent-blue: #7289da;
            --accent-mint: #43b581;
            --accent-nitro: #faa61a; /* สีส้มทอง สบายตากว่าแดง */
            --text-main: #ffffff;
            --text-dim: #b9bbbe;
        }

        body { 
            background-color: var(--bg-color); color: var(--text-main); font-family: 'Segoe UI', Roboto, sans-serif; 
            margin: 0; padding: 0; height: 100vh; overflow: hidden;
            display: flex; justify-content: space-between; align-items: center;
        }

        /* --- ฝั่งซ้าย: การควบคุมทิศทาง --- */
        .left-controls { padding-left: 50px; display: flex; align-items: center; gap: 30px; }

        .d-pad {
            display: grid;
            grid-template-areas: ". up ." "left stop right" ". down .";
            gap: 12px;
        }

        .btn { 
            width: 70px; height: 70px; border: none; border-radius: 18px; 
            background: var(--panel-color); color: var(--text-main); 
            font-size: 16px; font-weight: 600; 
            box-shadow: 0 4px 0px #18191c; transition: all 0.1s;
            cursor: pointer;
        }
        .btn:active { transform: translateY(3px); box-shadow: 0 1px 0px #18191c; background: var(--accent-blue); }

        /* ปุ่มเลี้ยวซ้าย-ขวา ใช้สีที่ต่างกันเล็กน้อยเพื่อความชัดเจน */
        .btn-dir { background: #36393f; }

        /* --- กลาง: Dashboard --- */
        .dashboard { text-align: center; flex-grow: 1; }
        #val { font-size: 70px; font-weight: 200; color: var(--accent-mint); font-family: 'Courier New', monospace; }
        .unit { font-size: 14px; letter-spacing: 2px; color: var(--text-dim); text-transform: uppercase; }

        /* --- ฝั่งขวา: ปุ่ม Action --- */
        .right-controls { padding-right: 50px; display: flex; align-items: center; gap: 25px; }

        .nitro-btn {
            width: 110px; height: 110px; border-radius: 50%; border: none;
            background: var(--accent-nitro); color: white; font-size: 20px; font-weight: 800;
            box-shadow: 0 6px 15px rgba(250, 166, 26, 0.3); transition: 0.1s;
        }
        .nitro-btn:active { transform: scale(0.95); box-shadow: 0 2px 5px rgba(250, 166, 26, 0.5); }

        /* --- สไลเดอร์ความเร็ว --- */
        .speed-container { display: flex; flex-direction: column; align-items: center; }
        .vertical-slider {
            -webkit-appearance: none; width: 180px; height: 8px; background: #4f545c; 
            border-radius: 10px; transform: rotate(-90deg); margin: 80px -80px;
        }
        .vertical-slider::-webkit-slider-thumb {
            -webkit-appearance: none; width: 30px; height: 30px;
            background: var(--text-main); border-radius: 50%; border: 4px solid var(--accent-blue);
        }

        /* --- ปุ่มล่าง --- */
        .extra-btns {
            position: absolute; bottom: 25px; left: 50%; transform: translateX(-50%);
            display: flex; gap: 20px;
        }
        .small-btn {
            padding: 10px 25px; background: rgba(255,255,255,0.05); color: var(--text-dim); 
            border: 1px solid #4f545c; border-radius: 20px; font-size: 12px;
        }
    </style>
</head>
<body>

    <div class="left-controls">
        <div class="speed-container">
            <input type="range" min="0" max="60" value="30" class="vertical-slider" id="spd" oninput="updateVal(this.value)">
        </div>
        <div class="d-pad">
            <button class="btn" style="grid-area: up" onmousedown="send('F')" onmouseup="send('S')" ontouchstart="send('F')" ontouchend="send('S')">UP</button>
            <button class="btn btn-dir" style="grid-area: left" onmousedown="send('L')" onmouseup="send('S')" ontouchstart="send('L')" ontouchend="send('S')">Left</button>
            <button class="btn" style="grid-area: stop; background: #f04747; color: white;" onclick="send('S')">STOP</button>
            <button class="btn btn-dir" style="grid-area: right" onmousedown="send('R')" onmouseup="send('S')" ontouchstart="send('R')" ontouchend="send('S')">Right</button>
            <button class="btn" style="grid-area: down" onmousedown="send('B')" onmouseup="send('S')" ontouchstart="send('B')" ontouchend="send('S')">DOWN</button>
        </div>
    </div>

    <div class="dashboard">
        <div id="val">30</div>
        <div class="unit">Kilometers / H</div>
    </div>

    <div class="right-controls">
        <div style="display:flex; flex-direction:column; gap:15px;">
            <button class="btn" style="background:var(--accent-blue);" onclick="send('HORN')">HORN</button>
            <button class="btn" style="background:var(--panel-color);" onclick="send('LIGHT')">LIGHT</button>
        </div>
        <button class="nitro-btn" onmousedown="send('NITRO')" onmouseup="send('F')" ontouchstart="send('NITRO')" ontouchend="send('F')">NITRO</button>
    </div>

    <div class="extra-btns">
        <button class="small-btn" onclick="fetch('/DANCE')">AUTOPILOT</button>
        <button class="small-btn" onclick="fetch('/BEEP')">BEEP MODE</button>
    </div>

    <script>
        function updateVal(v) {
            document.getElementById('val').innerHTML = v;
            fetch('/set?v=' + v);
        }

        function send(dir) {
            let s = document.getElementById('spd').value;
            let speedValue = (dir === 'NITRO') ? 100 : s;
            fetch('/' + dir + '?v=' + speedValue);
        }
    </script>
</body>
</html>
)raw";

void setup() {
  XIO(); 
  oled(0, 0, "Robot Starting..."); 
  WiFi.softAP(ssid, password);
  IPAddress IP = WiFi.softAPIP();
  oled(0, 0, "Wi-Fi: %s", ssid);      
  oled(0, 20, "IP: %s", IP.toString().c_str()); 
  oled(0, 40, "Status: Ready!");      
  server.on("/", []() { server.send(200, "text/html", html); });
  // --- ปรับปรุงการรับค่าความเร็วให้ Real-time ---
  // สั่งเดินหน้า
  motor(1, 0); motor(2, 0); 
  server.on("/F", []() { 
    carDirection = "FORWARD"; // ตัวแปรเก็บทิศทาง (STOP, FORWARD, BACKWARD, LEFT, RIGHT)
    if(server.hasArg("v")) mySpeed = server.arg("v").toInt();
    motor(1, mySpeed); motor(2, mySpeed); // สั่งทั้ง 2 ล้อ
    oled(0, 40, "Action: FORWARD"); 
    server.send(200, "text/plain", "OK"); 
  });

  // สั่งถอยหลัง
  server.on("/B", []() { 
    carDirection = "BACKWARD"; // ตัวแปรเก็บทิศทาง (STOP, FORWARD, BACKWARD, LEFT, RIGHT)
    if(server.hasArg("v")) mySpeed = server.arg("v").toInt();
    motor(1, -mySpeed); motor(2, -mySpeed); // ใส่เครื่องหมายติดลบเพื่อถอยหลัง
    oled(0, 40, "Action: BACKWARD"); 
    server.send(200, "text/plain", "OK"); 
  });

  // สั่งเลี้ยวซ้าย (ล้อ 1 ถอย, ล้อ 2 เดินหน้า)
  server.on("/L", []() { 
    carDirection = "LEFT"; // ตัวแปรเก็บทิศทาง (STOP, FORWARD, BACKWARD, LEFT, RIGHT)
    if(server.hasArg("v")) mySpeed = server.arg("v").toInt();
    motor(1, -mySpeed); motor(2, mySpeed); 
    server.send(200, "text/plain", "OK"); 
  });

  // สั่งเลี้ยวขวา (ล้อ 1 เดินหน้า, ล้อ 2 ถอย)
  server.on("/R", []() { 
    carDirection = "RIGHT"; // ตัวแปรเก็บทิศทาง (STOP, FORWARD, BACKWARD, LEFT, RIGHT)
    if(server.hasArg("v")) mySpeed = server.arg("v").toInt();
    motor(1, mySpeed); motor(2, -mySpeed); 
    server.send(200, "text/plain", "OK"); 
  });

  // สั่งหยุด
  server.on("/S", []() { 
    motor(1, 0); motor(2, 0); 
    oled(0, 40, "Action: STOPPED "); 
    server.send(200, "text/plain", "OK"); 
  });

  // ปุ่ม NITRO (วิ่งเต็มกำลัง 100)
  server.on("/NITRO", []() { 
    motor(1, 100); motor(2, 100); 
    nitroStartTime = millis(); // บันทึกเวลาที่เริ่มกด
    isNitroActive = true;      // เปิดโหมดไนตรัส
    oled(0, 40, "!!! NITRO !!!"); 
    server.send(200, "text/plain", "OK"); 
  });

  // ปุ่ม DANCE (สะบัดซ้ายขวา)
  server.on("/DANCE", []() { 
    motor(1, -80); motor(2, 80); delay(200); 
    motor(1, 80); motor(2, -80); delay(200); 
    motor(1, 0); motor(2, 0); 
    server.send(200, "text/plain", "OK"); 
  });
  // 2. HORN: บีบแตร
  server.on("/HORN", []() { 
    beep(); 
    oled(0, 40, "Action: HORN");
    server.send(200, "text/plain", "OK"); 
  });

  // 3. LIGHT: เปิดไฟ (สมมติว่าใช้ขา 2 หรือคำสั่งเฉพาะของบอร์ด)
  server.on("/LIGHT", []() { 
    // หากบอร์ดมีคำสั่งเปิดไฟ เช่น led(1) ให้ใส่ตรงนี้
    oled(0, 40, "Action: LIGHT");
    server.send(200, "text/plain", "OK"); 
  });
  server.on("/BEEP", []() { 
    beep(); delay(100); beep(); 
    server.send(200, "text/plain", "OK"); 
  });
  server.on("/set", []() {
    mySpeed = server.arg("v").toInt();
    server.send(200, "text/plain", "OK");
  });
  server.begin();
}
void loop() {
  server.handleClient(); // วนลูปเช็คว่ามีการกดปุ่มบนหน้าเว็บเข้ามาหรือไม่
  // --- ระบบนับเวลาไนตรัส 5 วินาที ---
  if (isNitroActive) {
    // ถ้าเวลาปัจจุบัน ลบ เวลาที่เริ่ม มากกว่า 5000 มิลลิวินาที (5 วินาที)
    if (millis() - nitroStartTime >= 2000) {
      isNitroActive = false; // ปิดโหมดไนตรัส
      motor(1, 0); // เพื่อความปลอดภัย แนะนำให้สั่งหยุดก่อน หรือปรับเป็นความเร็วเดิม
      motor(2, 0); 
      
      oled(0, 40, "Nitro Timeout! ");
    }
  }
}

ความคิดเห็น


bottom of page