จากบทความการควบคุมหุ่นยนต์รถ 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 เป็นดังนี้
- NodeMCU และบอร์ดขยาย
- บอร์ดโมดูลขับมอเตอร์ไฟฟ้ากระแสตรง MX1508
- แหล่งจ่ายไฟแบบชาร์จได้
- หุ่นยนต์รถขับเคลื่อนด้วยมอเตอร์ไฟฟ้า 2 ตัว
- มอเตอร์ไฟฟ้ากระแสตรงสำหรับล้อซ้าย
- มอเตอร์ไฟฟ้ากระแสตรงสำหรับล้อขวา
- ชุดล้อสำหรับมอเตอร์ไฟฟ้าล้อซ้าย
- ชุดล้อสำหรับมอเตอร์ไฟฟ้าล้อขวา
- โครงยึดอุปกรณ์ตัวหุ่น
โค้ดโปรแกรม
จากรายการอุปกรณ์เมื่อประกอบเป็นหุ่นยนต์ตามตัวอย่างภาพที่ 1 หรืออ่านรายละเอีดจากบทความ ESP8266+RoboServo และขั้นตอนถัดไปคือออกแบบสถาปัตยกรรมของซอฟต์แวร์
โครงสร้างของซอฟต์แวร์ที่จะพัฒนาขึ้นประกอบด้วย 2 ส่วนหลัก
- ส่วนการทำงานถายใต้เฟรมเวิร์กของ Arduino คือ setup(), loop() และการทำงานเพิ่มเติม
- คลาสของ Actor ที่เป็นตัวหุ่นยนต์เพื่อให้รองรับคุณสมบัติของ Actuator ที่มีอยู่ นั่นคือ ล้อทั้ง 2 ข้าง ทำให้สามารถตอบสนองกับสิ่งแวดล้อมได้ 5 กรณี คือ
- หยุดการเคลื่อนที่ โดยสิ่งที่สั่งงานในขั้นตอนนี้เป็นการส่ง low เพื่อหยุดจ่ายแรงดันทำให้เกิดการเคลื่อนที่ตามแรงส่งที่เกิดจากคำสั่งหลังสุด แล้วสั่งหยุดด้วยการส่ง high ไปทั้ง 4 ขั้วของมอเตอร์ทั้ง 2 ตัวเพื่อให้เกิดการหยุด แล้วผ่อนการหยุดด้วยการส่ง low ก่อนสิ้นสุดการทำงาน ทั้งนี้เกิดจากการทดลองจำลองเหมือนการเหยียบคลัช แล้วเหยียบเบรค หลังจากนั้นปล่อยเบรคแล้วเหยียบคลัชไว้เพื่อรอเหยียบคันเร่ง (ผู้อ่านลองดูหลาย ๆ แบบก็ได้ครับ)
- การวิ่งไปข้างหน้า
- การถอยหลัง
- การเลี้ยวซ้ายด้วยการหมุนมอเตอร์ไปทิศตรงกันข้ามกัน
- การเลี้ยวขวาด้วยการหมุนตรงข้ามกับการเลี้ยวซ้าย
โค้ดสำหรับ 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
โค้ดโปรแกรมที่เพิ่มเข้ามานั้นเป็นการเปิดให้บริการพอร์ต 80 และกำหนดตัวเองเป็น AP ชื่อ “JarutEx” พร้อมระบุรหัสผ่านไว้เป็น “123456789” โดยกำหนดการเข้าถึงการทำงาน 5 แบบ คือ
- / เรียกใช้ htmlPage เพื่อแสดงสถานะของการเคลื่อนที่ด้วยภาพของปุ่มในภาพที่ 2 โดยสถานะที่กำลังทำงานเป็นปุ่มที่มีสีพื้นหลังเป้นสีเขียว และพื้นหลังสีแดงสำหรับปุ่มอื่น ๆ เพื่อแยกให้เห็นความแตกต่าง
- /stop เรียกใช้ robotStop เพื่อหยุดการเคลื่อนที่
- /forward เรียกใช้ robotForward เพื่อให้เคลื่อนที่ไปด้านหน้า
- /backward สำหรับถอยหลังด้วยการเรียก robotBackward
- /left เพื่อสั่งหมุนไปทางซ้ายด้วยการตอบสนองจากคำสั่ง robotLeft
- /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ขอบคุณ รศ.ดร.เที่ยง เหมียดไธสง และ ผศ.ศิวาพร เหมียดไธสง ที่สนับสนุนอุปกรณ์การทดลองครับ