[TH] Arduino: ควบคุมการเคลื่อนที่หุ่นยนต์รถผ่านบราวเซอร์ด้วย esp8266

จากบทความการควบคุมหุ่นยนต์รถ 2 ล้อแบบ Servo ใน ESP8266+RoboServo และมอเตอร์ไฟฟ้ากระแสตรงใน VisionRobo Car: Drive Motor ทางเราได้นำหุ่นยนต์รถในตัวที่ 2 เปลี่ยนจาก Raspberry Pi เป็น ESP8266 เพื่อสั่งงานผ่าน WiFi โดยใช้แนวทางจาก บทความ ESP-01s+Relay มาเขียนใหม่ด้วยภาษา C/C++ ของ Arduino ด้วยคลาส WebServer จากที่ในบทความของ ESP8266 เป็น MicroPython ดังนั้น เมื่อทำตามบทความนี้จนเสร็จจะสามารถสั่งงานหุ่นบนต์ในภาพตัวอย่างที่ 1 ได้ด้วยการเชื่อมต่อโทรศัพท์หรืออุปกรณ์สื่อสารไปที่ 192.168.4.1 และสั่งให้เดินหน้า ถอยหลัง เลี้ยวซ้าย เลี้ยวขวา หรือหยุดได้

ภาพที่ 1 ตัวอย่างหุ่นยนต์รถสำหรับการทดลองในบทความนี้

อุปกรณ์

รายการอุปกรณ์สำหรับการทดลองตามภาพที่ 1 เป็นดังนี้

  1. NodeMCU และบอร์ดขยาย
  2. บอร์ดโมดูลขับมอเตอร์ไฟฟ้ากระแสตรง MX1508
  3. แหล่งจ่ายไฟแบบชาร์จได้
  4. หุ่นยนต์รถขับเคลื่อนด้วยมอเตอร์ไฟฟ้า 2 ตัว
    1. มอเตอร์ไฟฟ้ากระแสตรงสำหรับล้อซ้าย
    2. มอเตอร์ไฟฟ้ากระแสตรงสำหรับล้อขวา
    3. ชุดล้อสำหรับมอเตอร์ไฟฟ้าล้อซ้าย
    4. ชุดล้อสำหรับมอเตอร์ไฟฟ้าล้อขวา
    5. โครงยึดอุปกรณ์ตัวหุ่น

โค้ดโปรแกรม

จากรายการอุปกรณ์เมื่อประกอบเป็นหุ่นยนต์ตามตัวอย่างภาพที่ 1 หรืออ่านรายละเอีดจากบทความ ESP8266+RoboServo และขั้นตอนถัดไปคือออกแบบสถาปัตยกรรมของซอฟต์แวร์

โครงสร้างของซอฟต์แวร์ที่จะพัฒนาขึ้นประกอบด้วย 2 ส่วนหลัก

  1. ส่วนการทำงานถายใต้เฟรมเวิร์กของ Arduino คือ setup(), loop() และการทำงานเพิ่มเติม
  2. คลาสของ Actor ที่เป็นตัวหุ่นยนต์เพื่อให้รองรับคุณสมบัติของ Actuator ที่มีอยู่ นั่นคือ ล้อทั้ง 2 ข้าง ทำให้สามารถตอบสนองกับสิ่งแวดล้อมได้ 5 กรณี คือ
    1. หยุดการเคลื่อนที่ โดยสิ่งที่สั่งงานในขั้นตอนนี้เป็นการส่ง low เพื่อหยุดจ่ายแรงดันทำให้เกิดการเคลื่อนที่ตามแรงส่งที่เกิดจากคำสั่งหลังสุด แล้วสั่งหยุดด้วยการส่ง high ไปทั้ง 4 ขั้วของมอเตอร์ทั้ง 2 ตัวเพื่อให้เกิดการหยุด แล้วผ่อนการหยุดด้วยการส่ง low ก่อนสิ้นสุดการทำงาน ทั้งนี้เกิดจากการทดลองจำลองเหมือนการเหยียบคลัช แล้วเหยียบเบรค หลังจากนั้นปล่อยเบรคแล้วเหยียบคลัชไว้เพื่อรอเหยียบคันเร่ง (ผู้อ่านลองดูหลาย ๆ แบบก็ได้ครับ)
    2. การวิ่งไปข้างหน้า
    3. การถอยหลัง
    4. การเลี้ยวซ้ายด้วยการหมุนมอเตอร์ไปทิศตรงกันข้ามกัน
    5. การเลี้ยวขวาด้วยการหมุนตรงข้ามกับการเลี้ยวซ้าย

โค้ดสำหรับ RobotAgent เป็นดังนี้


#define MOTOR_L1  D4
#define MOTOR_L2  D5
#define MOTOR_R1  D6
#define MOTOR_R2  D7

class RobotAgent {
  private:
  public:
    RobotAgent() {
      // Actuator
      pinMode(MOTOR_L1, OUTPUT);
      pinMode(MOTOR_L2, OUTPUT);
      pinMode(MOTOR_R1, OUTPUT);
      pinMode(MOTOR_R2, OUTPUT);
    }
    ~RobotAgent() {

    }
    void stop() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, LOW );
      delay(5);
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, HIGH );
      delay(100);
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, LOW );
    }
    void forward() {
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, HIGH );
    }
    void left() {
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, LOW );
    }
    void right() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, HIGH );
    }
    void backward() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, LOW );
    }
};

ควบคุมการเคลื่อนที่

ตัวอย่างโปรแกรมสำหรับทดสอบการทำงานของตัวหุ่นบนต์ (ผู้อ่านต้องปรับเปลี่ยนการหมุนให้ตรงกับหุ่นบนต์รถของตัวเองด้วยนะครับ) เพื่อเดินหน้า ถอยหลัง เลี้ยวซ้าย และเลี้ยวขวาอย่างละ 3 วินาที เขียนได้ดังนี้


#define MOTOR_L1  D4
#define MOTOR_L2  D5
#define MOTOR_R1  D6
#define MOTOR_R2  D7

class RobotAgent {
  private:
  public:
    RobotAgent() {
      // Actuator
      pinMode(MOTOR_L1, OUTPUT);
      pinMode(MOTOR_L2, OUTPUT);
      pinMode(MOTOR_R1, OUTPUT);
      pinMode(MOTOR_R2, OUTPUT);
    }
    ~RobotAgent() {

    }
    void stop() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, LOW );
      delay(5);
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, HIGH );
      delay(100);
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, LOW );
    }
    void forward() {
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, HIGH );
    }
    void left() {
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, LOW );
    }
    void right() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, HIGH );
    }
    void backward() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, LOW );
    }
};

RobotAgent car;

void setup() {
}

void loop() {
  car.stop();
  car.forward();
  delay(3000);
  car.stop();
  car.backward();
  delay(3000);
  car.stop();
  car.left();
  delay(3000);
  car.stop();
  car.right();
  delay(3000);
}

ควบคุมการเคลื่อนที่ผ่านบราวเซอร์

หลังจากที่ได้ทดลองสั่งงานหุ่นยนต์ให้เคลื่อนที่เป้นผลสำเร็จ ขั้นตอนถัดไปเป็นการเพิ่มโค้ดส่วนของการสั่งงานผ่านเว็บดังภาพที่ 2

ภาพที่ 2 ตัวอย่างหน้าเว็บเมื่ออยู่ในถานะ stop

โค้ดโปรแกรมที่เพิ่มเข้ามานั้นเป็นการเปิดให้บริการพอร์ต 80 และกำหนดตัวเองเป็น AP ชื่อ “JarutEx” พร้อมระบุรหัสผ่านไว้เป็น “123456789” โดยกำหนดการเข้าถึงการทำงาน 5 แบบ คือ

  1. / เรียกใช้ htmlPage เพื่อแสดงสถานะของการเคลื่อนที่ด้วยภาพของปุ่มในภาพที่ 2 โดยสถานะที่กำลังทำงานเป็นปุ่มที่มีสีพื้นหลังเป้นสีเขียว และพื้นหลังสีแดงสำหรับปุ่มอื่น ๆ เพื่อแยกให้เห็นความแตกต่าง
  2. /stop เรียกใช้ robotStop เพื่อหยุดการเคลื่อนที่
  3. /forward เรียกใช้ robotForward เพื่อให้เคลื่อนที่ไปด้านหน้า
  4. /backward สำหรับถอยหลังด้วยการเรียก robotBackward
  5. /left เพื่อสั่งหมุนไปทางซ้ายด้วยการตอบสนองจากคำสั่ง robotLeft
  6. /right สำหรับหมุนขวาด้วยการเรียก robotRight

โค้ดโปรแกรมเป็นดังนี้

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

#define AP_NAME "JarutEx"
#define AP_PASSWD "123456789"

#define MOTOR_L1  D4
#define MOTOR_L2  D5
#define MOTOR_R1  D6
#define MOTOR_R2  D7

class RobotAgent {
  private:
  public:
    RobotAgent() {
      // Actuator
      pinMode(MOTOR_L1, OUTPUT);
      pinMode(MOTOR_L2, OUTPUT);
      pinMode(MOTOR_R1, OUTPUT);
      pinMode(MOTOR_R2, OUTPUT);
    }
    ~RobotAgent() {

    }
    void stop() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, LOW );
      delay(5);
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, HIGH );
      delay(100);
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, LOW );
    }
    void forward() {
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, HIGH );
    }
    void left() {
      digitalWrite( MOTOR_L1, HIGH );
      digitalWrite( MOTOR_L2, LOW );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, LOW );
    }
    void right() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, LOW );
      digitalWrite( MOTOR_R2, HIGH );
    }
    void backward() {
      digitalWrite( MOTOR_L1, LOW );
      digitalWrite( MOTOR_L2, HIGH );
      digitalWrite( MOTOR_R1, HIGH );
      digitalWrite( MOTOR_R2, LOW );
    }
};

RobotAgent car;
IPAddress myIP(192, 168, 4, 1);
IPAddress gwIP(192, 168, 4, 10);
IPAddress subnet(255, 255, 255, 0);
int motionState = 0;

ESP8266WebServer server(80);

void setup() {
  motionState = 0;
  car.stop();
  if (WiFi.softAPConfig( myIP, gwIP, subnet )) {
    if (WiFi.softAP(AP_NAME, AP_PASSWD, 8, false, 5)) {
    } else {
      while (true);
    }
  } else {
    while (true) {
    }
  }
  server.on("/", htmlPage);
  server.on("/stop", robotStop);
  server.on("/forward", robotForward);
  server.on("/backward", robotBackward);
  server.on("/left", robotLeft);
  server.on("/right", robotRight);
  
  server.begin();
}

void robotStop() {
  motionState = 0;
  car.stop();
  htmlPage();
}
void robotForward() {
  motionState = 1;
  car.forward();
  htmlPage();
}
void robotBackward() {
  motionState = 2;
  car.backward();
  htmlPage();
}
void robotLeft(){ 
  motionState = 3;
  car.left();
  htmlPage();
}
void robotRight(){ 
  motionState = 4;
  car.right();
  htmlPage();
}

void htmlPage() {
  String html;
  html.reserve(2048);               // prevent ram fragmentation
  html = F(
           "<!DOCTYPE HTML>"
           "<html><head>"
           "<meta name='viewport' content='width=device-width, initial-scale=1'>"
           "<style>"

           ".button {  border: none;  color: white;  padding: 20px;  text-align: center;  text-decoration: none;"
           "  display: inline-block;  font-size: 14"
           "  px;  margin: 4px 2px;  cursor: pointer;  border-radius: 4%;"
           "  width: 100%; height: 100%;"
           "}"
           ".button1 {  background-color: #3ABC40; }"
           ".button2 {  background-color: #BC4040; }"
           "</style></head><body><table>"
         );
  if (motionState == 0) {
    html += F(
              "<tr>"
              "<td></td>"
              "<td><a href='/forward'><button class='button button2'>Forward</button></a></td>"
              "<td></td>"
              "</tr>"
              "<tr>"
              "<td><a href='/left'><button class='button button2'>Turn Left</button></a></td>"
              "<td><a href='/stop'><button  class='button button1'>Stop</button></a></td>"
              "<td><a href='/right'><button class='button button2'>Turn Right</button></a></td>"
              "</tr>"
              "<tr>"
              "<td></td>"
              "<td><a href='/backward'><button class='button button2'>Backward</button></a></td>"
              "<td></td>"
              "</tr>"
            );
  }
  else if (motionState == 1) {
    html += F(
              "<tr>"
              "<td></td>"
              "<td><a href='/forward'><button class='button button1'>Forward</button></a></td>"
              "<td></td>"
              "</tr>"
              "<tr>"
              "<td><a href='/left'><button class='button button2'>Turn Left</button></a></td>"
              "<td><a href='/stop'><button  class='button button2'>Stop</button></a></td>"
              "<td><a href='/right'><button class='button button2'>Turn Right</button></a></td>"
              "</tr>"
              "<tr>"
              "<td></td>"
              "<td><a href='/backward'><button class='button button2'>Backward</button></a></td>"
              "<td></td>"
              "</tr>"
            );

  }
  else if (motionState == 2) {
    html += F(
              "<tr>"
              "<td></td>"
              "<td><a href='/forward'><button class='button button2'>Forward</button></a></td>"
              "<td></td>"
              "</tr>"
              "<tr>"
              "<td><a href='/left'><button class='button button2'>Turn Left</button></a></td>"
              "<td><a href='/stop'><button  class='button button2'>Stop</button></a></td>"
              "<td><a href='/right'><button class='button button2'>Turn Right</button></a></td>"
              "</tr>"
              "<tr>"
              "<td></td>"
              "<td><a href='/backward'><button class='button button1'>Backward</button></a></td>"
              "<td></td>"
              "</tr>"
            );

  }
  else if (motionState == 3) {
    html += F(
              "<tr>"
              "<td></td>"
              "<td><a href='/forward'><button class='button button2'>Forward</button></a></td>"
              "<td></td>"
              "</tr>"
              "<tr>"
              "<td><a href='/left'><button class='button button1'>Turn Left</button></a></td>"
              "<td><a href='/stop'><button  class='button button2'>Stop</button></a></td>"
              "<td><a href='/right'><button class='button button2'>Turn Right</button></a></td>"
              "</tr>"
              "<tr>"
              "<td></td>"
              "<td><a href='/backward'><button class='button button2'>Backward</button></a></td>"
              "<td></td>"
              "</tr>"
            );

  }
  else if (motionState == 4) {
    html += F(
              "<tr>"
              "<td></td>"
              "<td><a href='/forward'><button class='button button2'>Forward</button></a></td>"
              "<td></td>"
              "</tr>"
              "<tr>"
              "<td><a href='/left'><button class='button button2'>Turn Left</button></a></td>"
              "<td><a href='/stop'><button  class='button button2'>Stop</button></a></td>"
              "<td><a href='/right'><button class='button button1'>Turn Right</button></a></td>"
              "</tr>"
              "<tr>"
              "<td></td>"
              "<td><a href='/backward'><button class='button button2'>Backward</button></a></td>"
              "<td></td>"
              "</tr>"
            );

  }
  html += F("</table></body></html>\r\n");
  server.send(200, "text/html", html);
}

void loop() {
  server.handleClient();
}

สรุป

จะพบว่า การใช้ภาษาเป็นส่วนหนึ่งของความชอบหรือสะดวกในการพัฒนา แต่ภาษาสำหรับแสดงผลหรือปฏิสัมพันธ์กับผู้ใช้ผ่านเว็บนั้นยังคงเป็น HTML5 ด้วยเหตุนี้ ถ้าผู้อ่านเข้าใจและมีทักษะการเขียนเว็บด้วย HTML, CSS3 และ JavaScript จะทำให้สามารถสร้างผลลัพธ์ของเว็บออกมาได้ดูดีสวยงาม แต่อย่างไรก็ดี ด้วยไมโครคอนโทรลเลอร์ ESP8266 หรือ ESP32 เป็นหน่วยประมวลผลขนาดเล็ก มีปริมาณหน่วยความจำจำกัดอาจจะทำให้ไม่สามารถทำได้เหมือนเครื่องให้บริการในระบบจริง ดังนั้น ผู้อ่านที่ได้ผ่านการทดลองการเขียนโปรแกรมที่หลากหลายรูปแบบจะพบข้อเท็จจริงและได้ประสบการณ์เพิ่มเติมว่า การทำแบบใดสามารถทำได้ และการใช้งานแบบใดที่เป็นอุปสรรค หรืออาจจะทำงานไม่ได้ สุดท้าย ขอให้สนุกกับการเขียนโปรแกรมครับ

(C) 2022, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2022-01-01, 2022-02-06

ขอบคุณ รศ.ดร.เที่ยง เหมียดไธสง และ ผศ.ศิวาพร เหมียดไธสง ที่สนับสนุนอุปกรณ์การทดลองครับ