ESP32 AI Show Robot

ระบบควบคุมหุ่นยนต์โชว์ตัวด้วยเสียงและจอยสติ๊กไร้สาย

ESP32 Controller
หุ่นยนต์โชว์ตัว ESP32

ระบบควบคุมหุ่นยนต์โชว์ตัวอัจฉริยะ

ระบบประมวลผลหลักด้วย ESP32 ทำหน้าที่รับคำสั่ง ควบคุมการเคลื่อนที่ และสั่งงานอุปกรณ์ต่อพ่วงเอฟเฟกต์แบบเรียลไทม์ ผ่านจอย PS3 ไร้สาย และการสกัดคำสั่งเสียงภาษาไทยพร้อมเวลาสปีดจากบอร์ดเสี่ยวจื่อ (Xiao Zhi) อัตโนมัติ

Interactive Show Robot Xiao Zhi + PS3 Dual Control

1. ระบบขับเคลื่อน & มอเตอร์

ควบคุมไดรเวอร์มอเตอร์ (TB6612FNG / L298N) ผ่าน PWM ปรับความเร็วผ่านจอย PS3 (Triangle/Cross) และแยกสปีดความเร็วเฉพาะสำหรับการคุมด้วยเสียง xiaoZhiSpeed

เดินหน้า ถอยหลัง เลี้ยว

2. คำสั่งเสียงเสี่ยวจื่อ (Xiao Zhi)

ถอดรหัสข้อความผ่าน Serial1 (GPIO 34) สกัดตัวอักษรทิศทาง (F, B, L, R) ร่วมกับสกัดตัวเลขเวลาจากประโยคภาษาไทยยาวๆ เช่น "L เลี้ยวซ้ายเป็นเวลา 1 วินาทีเรียบร้อยครับผม" เพื่อสั่งเลี้ยวและหยุดให้อัตโนมัติ

สกัดตัวเลขวินาที + สั่งหยุดมอเตอร์อัตโนมัติ

3. เอฟเฟกต์ พลุกระดาษ & ฟองสบู่

  • พลุกระดาษแอลกอฮอล์: ต่อ Relay 1 (GPIO 4) กดปุ่มสี่เหลี่ยม (Square)
  • ปืนทำฟองสบู่: ต่อ Relay 2 (GPIO 16) กดปุ่มวงกลม (Circle) หรือสั่งด้วยเสียง
Relay Dual-Channel Active Low

4. เซอร์โวหมุนหัวหุ่นยนต์

ต่อเซอร์โวมอเตอร์ที่ GPIO 5 (ควบคุมมุม 20°–160°) สุ่มหันหัวกวาดสายตาไปมาสไตล์สแกนพื้นที่อัตโนมัติเมื่อหยุดนิ่ง และกด R2/L2 บนจอย PS3 เพื่อ override คุมหันเองได้

Auto-Random Mode (20° - 160°)

5. ไฟแสดงสถานะ NeoPixel 60 ดวง

เชื่อมต่อ GPIO 21 เปลี่ยนสีไฟบอกทิศทาง (เขียว/แดง/เหลือง) มีโหมดไฟกะพริบขณะจอดพัก และเปิดโหมด Light Show แสดงไฟเอฟเฟกต์สีวิ่ง 3 รูปแบบด้วยปุ่ม L1/R1

● เดินหน้า ● ถอยหลัง ● เลี้ยว

6. เสียงเพลง & ป้ายไฟ LED

ต่อเล่นเพลงผ่านโมดูล DFPlayer Mini (Serial2) กดเลือกข้ามเพลงผ่านปุ่ม Select พร้อมพอร์ตสื่อสารรองรับการต่อป้ายไฟ LED Matrix ส่งข้อความเปลี่ยนตัวอักษรต้อนรับแบบ dynamic

DFPlayer Mini + LED Matrix Banner

การต่อวงจรและการเชื่อมต่อขาต่างๆ (Hardware Wiring Diagram)

ตารางการเชื่อมต่อพินของ ESP32 กับโมดูลและไดรเวอร์อุปกรณ์ต่อพ่วงทั้งหมด

แผนผังการต่อวงจร ESP32 กับขาต่าง ๆ
ภาพประกอบการเชื่อมต่อขาใช้งาน ESP32 (esp32C.jpg)
อุปกรณ์ / โมดูล ชื่อขาอุปกรณ์ ขา ESP32 (GPIO) หมายเหตุ / การทำงาน
ไดรเวอร์มอเตอร์ (L298N / TB6612FNG) AIN1 / AIN2 / PWMA GPIO 25 / 26 / 27 ควบคุมทิศทางและความเร็ว มอเตอร์ A (ล้อซ้าย)
BIN1 / BIN2 / PWMB GPIO 12 / 13 / 14 ควบคุมทิศทางและความเร็ว มอเตอร์ B (ล้อขวา)
STBY GPIO 33 ขาเปิดการทำงานไดรเวอร์ (Active HIGH)
GND / VCC GND / 5V-12V ต่อไฟเลี้ยงมอเตอร์และกราวด์ร่วมกัน
เซอร์โวหมุนหัว (Servo Head) Signal (PWM) GPIO 5 สัญญาณคุมมุมหมุน 20° – 160°
โมดูลรีเลย์ (Relay Module) Relay 1 (Square) GPIO 4 ควบคุมพลุกระดาษแอลกอฮอล์
Relay 2 (Circle / Bubble) GPIO 16 ควบคุมเครื่องทำฟองสบู่
ไฟ NeoPixel (60 ดวง) DIN (Data In) GPIO 21 ส่งข้อมูลสัญญาณไฟ NeoPixel WS2812B
DFPlayer Mini (Serial2) RX / TX GPIO 17 (TX2) / 18 (RX2) สื่อสารอนุกรมควบคุมการเล่นไฟล์ MP3
บอร์ดเสี่ยวจื่อ Xiao Zhi (Serial1) RX / TX GPIO 34 (RX1) / 22 (TX1) รับ Log คำสั่งเสียงภาษาไทยจากบอร์ดเสี่ยวจื่อ

ซอร์สโค้ด Arduino (.ino) ฉบับสมบูรณ์

คลิกปุ่มคัดลอกด้านขวา หรือกดคัดลอกโค้ดด้านล่างไปใช้งานใน Arduino IDE ได้ทันที

// หุ่นยนต์ไว้โชว์ออกงาน บังคับด้วย esp32 ใช้เซอร์โวในการหมุนหัวหน้าได้
// คุมมอเตอร์ (เดินหน้า ถอยหลัง เลี้ยวซ้าย/ขวา)
// คุมความเร็ว (Triangle เพิ่ม / Cross ลด)
// รีเลย์ 2 ตัว (Square, Circle → กดทีเปิด กดอีกทีปิด)
// Servo3 (หัวหุ่น) มีโหมด หมุนอัตโนมัติไป–กลับ + กด R2/L2 override คุมเอง
// มอเตอร์ (ควบคุมผ่านมอเตอร์ไดร์เวอร์ L298N / TB6612FNG)
// AIN1 → GPIO 25, AIN2 → GPIO 26, PWMA → GPIO 27
// BIN1 → GPIO 12, BIN2 → GPIO 13, PWMB → GPIO 14
// STBY → GPIO 33
// รีเลย์ 1 (Square) → GPIO 4
// รีเลย์ 2 (Circle / เครื่องทำฟองสบู่) → GPIO 16
// Servo3 → GPIO 5 (PWM, ควบคุมมุม 20°–160°)
// === หุ่นยนต์โชว์งาน ESP32 + PS3 Controller + DFPlayer + Servo + Motor + NeoPixel ===
// DFPlayer Mini ต่อขา RX=17, TX=18
// บอร์ดเสี่ยวจื่อ (Xiao Zhi) ต่อขา RX=34, TX=22 (Serial1)

#include <Ps3Controller.h>
#include <ESP32Servo.h>
#include <Adafruit_NeoPixel.h>
#include <DFRobotDFPlayerMini.h>
#include "HardwareSerial.h"

// === DFPlayer Mini (Serial2) ===
HardwareSerial mySerial(2);
DFRobotDFPlayerMini myDFPlayer;
bool musicPlaying = false;
int currentTrack = 1;          // เริ่มต้นที่เพลงที่ 1
const int MAX_TRACKS = 10;     // 🔴 ปรับจำนวนเพลงทั้งหมดที่มีใน SD Card ตรงนี้

// === Xiao Zhi Log Parser Serial (Serial1) ===
HardwareSerial xiaoZhiSerial(1);
const int EXT_RX_PIN = 34; // สาย TX จากเสี่ยวจื่อต่อเข้า GPIO 34
const int EXT_TX_PIN = 22; // สาย RX จากเสี่ยวจื่อต่อเข้า GPIO 22

// === Motor Pins ===
const int AIN1 = 25, AIN2 = 26, PWMA = 27;
const int BIN1 = 12, BIN2 = 13, PWMB = 14;
const int STBY = 33;

// === Relay Pins ===
const int relayPin1 = 4;   // Square
const int relayPin2 = 16;  // Circle (เครื่องทำฟองสบู่)
bool relayState1 = false;
bool relayState2 = false;

// === Servo Head ===
Servo myservo3;
int angle5 = 90;
bool autoMode = true;
bool forceTo90 = false;
unsigned long lastServoMove = 0;
const int servoInterval = 30;
const int servoMin = 20;
const int servoMax = 160;

// === Random Target Servo Behavior ===
int targetAngle = 90;
unsigned long lastTargetChange = 0;
unsigned long nextTargetTime = 0;

// === PWM ===
const int freq = 5000;
const int resolution = 8;
const int pwmA = 2, pwmB = 3;

// === Speed Control ===
int maxSpeed = 120;
const int minSpeed = 80;
const int maxSpeedLimit = 255;
const int speedStep = 10;
String lastDirection = "";
String currentDirection = "Stop";

// === Voice Speed Control (ความเร็วเฉพาะของบอร์ดเสี่ยวจื่อ) ===
int xiaoZhiSpeed = 70; // 🔴 ปรับความเร็วเมื่อสั่งด้วยเสียงตรงนี้ (ค่า 0 - 255)

// === Voice Control Flag ===
bool isVoiceControl = false; // สถานะเช็คว่ากำลังคุมด้วยเสียงอยู่หรือไม่

// === NeoPixel ===
#define NEOPIXEL_PIN 21
#define NUMPIXELS 60
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

// === Mode ไฟหยุด ===
int stopMode = 0;
unsigned long stopStartTime = 0;
bool stopActive = false;

// === Light Show ===
int lightMode = 0;
bool lightShowActive = false;
unsigned long lightStartTime = 0;
const unsigned long LIGHT_SHOW_DURATION = 15000; // 15 วินาที

// Function Declarations
void updateServoHead();
void updateNeoPixels();
void checkMusicFinished();
void readFromXiaoZhi();
void onConnect();
void notify();
void moveMotor(int aSpeed, int bSpeed);
void moveForward();
void moveBackward();
void turnRight();
void turnLeft();
void stopMotors();
void disableAutoAndReturnTo90();
void printDirection(int speed, int turn);
showLightShowPattern(int mode, unsigned long t);
void playNextTrack();

// === Setup ===
void setup() {
  Serial.begin(115200);

  // PS3
  Ps3.begin();
  String address = Ps3.getAddress();
  Serial.print("The ESP32's Bluetooth MAC address is: ");
  Serial.println(address);
  Ps3.attach(notify);
  Ps3.begin("6c:c8:40:55:4d:fa");
  Ps3.attachOnConnect(onConnect);
  Serial.println("📶 Waiting for PS3 Controller...");

  // Servo
  myservo3.attach(5, 500, 2400);
  myservo3.write(angle5);

  // Motor (สำหรับ ESP32 Core v2.x)
  pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT);
  pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT);
  pinMode(STBY, OUTPUT); digitalWrite(STBY, HIGH);
  ledcSetup(pwmA, freq, resolution); ledcAttachPin(PWMA, pwmA);
  ledcSetup(pwmB, freq, resolution); ledcAttachPin(PWMB, pwmB);

  // Relay
  pinMode(relayPin1, OUTPUT); digitalWrite(relayPin1, HIGH);
  pinMode(relayPin2, OUTPUT); digitalWrite(relayPin2, HIGH);

  // NeoPixel
  pixels.begin();
  pixels.clear();
  pixels.show();

  // Random
  randomSeed(analogRead(0));
  nextTargetTime = millis() + random(1000, 4001);

  // DFPlayer (ใช้ Serial2: RX=17, TX=18)
  mySerial.begin(9600, SERIAL_8N1, 17, 18);
  if (!myDFPlayer.begin(mySerial)) {
    Serial.println("⚠️ DFPlayer ไม่ตอบกลับ");
  } else {
    Serial.println("✅ DFPlayer พร้อมใช้งาน");
    myDFPlayer.volume(20);
  }

  // Xiao Zhi Log Parser (ใช้ Serial1: RX=34, TX=22)
  xiaoZhiSerial.begin(115200, SERIAL_8N1, EXT_RX_PIN, EXT_TX_PIN);
  Serial.println("✅ Xiao Zhi Serial Log Parser Ready (RX: GPIO 34)");
}

// === Main Loop ===
void loop() {
  updateServoHead();
  updateNeoPixels();
  checkMusicFinished();   // เช็คเมื่อเพลงจบจะหยุดเล่นทันที
  readFromXiaoZhi();      // อ่านคำสั่งเสียงจากเสี่ยวจื่อ
}

// === PS3 Connect ===
void onConnect() { Serial.println("✅ PS3 Controller connected."); }

// === ฟังก์ชันอ่าน Log จากบอร์ดเสี่ยวจื่อ (รองรับประโยคยาวและสกัดตัวเลขเวลา) ===
void readFromXiaoZhi() {
  if (xiaoZhiSerial.available() > 0) {
    String input = xiaoZhiSerial.readStringUntil('\n');
    input.trim();

    if (input.length() == 0) return;

    int tagIndex = input.indexOf("<<");

    if (tagIndex != -1) {
      String responseText = input.substring(tagIndex + 2);
      responseText.trim();

      if (responseText.length() > 0) {
        // 1. แยกตัวอักษรคำสั่งแรก (F, B, L, R, S, O, C, X)
        char commandCode = toupper(responseText.charAt(0));

        // 2. ดึงเฉพาะตัวเลขแรกที่พบในข้อความ (รองรับทั้งจำนวนเต็มและทศนิยม)
        float durationSec = 0;
        String numStr = "";
        bool foundDigit = false;

        for (size_t i = 0; i < responseText.length(); i++) {
          char c = responseText.charAt(i);
          if (isDigit(c) || (c == '.' && foundDigit && numStr.indexOf('.') == -1)) {
            numStr += c;
            foundDigit = true;
          } else if (foundDigit && !isDigit(c) && c != '.') {
            // เมื่อสกัดกลุ่มตัวเลขชุดแรกจบแล้ว ให้หยุดค้นหา
            break;
          }
        }

        if (numStr.length() > 0) {
          durationSec = numStr.toFloat(); // แปลงชุดตัวเลขเป็นทศนิยม (วินาที)
        }

        // 3. ทำงานตามคำสั่งทิศทาง
        switch (commandCode) {
          case 'F':
            Serial.printf("🗣️ เสี่ยวจื่อสั่ง: เดินหน้า (ความเร็ว %d)", xiaoZhiSpeed);
            if (durationSec > 0) Serial.printf(" เป็นเวลา %.1f วินาที", durationSec);
            Serial.println();

            moveMotor(xiaoZhiSpeed, xiaoZhiSpeed);
            printDirection(xiaoZhiSpeed, 0);
            disableAutoAndReturnTo90();
            isVoiceControl = true;
            break;

          case 'B':
            Serial.printf("🗣️ เสี่ยวจื่อสั่ง: ถอยหลัง (ความเร็ว %d)", xiaoZhiSpeed);
            if (durationSec > 0) Serial.printf(" เป็นเวลา %.1f วินาที", durationSec);
            Serial.println();

            moveMotor(-xiaoZhiSpeed, -xiaoZhiSpeed);
            printDirection(-xiaoZhiSpeed, 0);
            disableAutoAndReturnTo90();
            isVoiceControl = true;
            break;

          case 'L':
            Serial.printf("🗣️ เสี่ยวจื่อสั่ง: เลี้ยวซ้าย (ความเร็ว %d)", xiaoZhiSpeed);
            if (durationSec > 0) Serial.printf(" เป็นเวลา %.1f วินาที", durationSec);
            Serial.println();

            moveMotor(xiaoZhiSpeed, -xiaoZhiSpeed);
            printDirection(0, 100);
            disableAutoAndReturnTo90();
            isVoiceControl = true;
            break;

          case 'R':
            Serial.printf("🗣️ เสี่ยวจื่อสั่ง: เลี้ยวขวา (ความเร็ว %d)", xiaoZhiSpeed);
            if (durationSec > 0) Serial.printf(" เป็นเวลา %.1f วินาที", durationSec);
            Serial.println();

            moveMotor(-xiaoZhiSpeed, xiaoZhiSpeed);
            printDirection(0, -100);
            disableAutoAndReturnTo90();
            isVoiceControl = true;
            break;

          case 'S':
            Serial.println("🗣️ เสี่ยวจื่อสั่ง: หยุด");
            stopMotors();
            isVoiceControl = false;
            break;

          // เปิดเครื่องทำฟองสบู่ (Relay 2)
          case 'O':
            Serial.println("🗣️ เสี่ยวจื่อสั่ง: เปิดเครื่องทำฟองสบู่ (Relay 2 ON)");
            relayState2 = true;
            digitalWrite(relayPin2, LOW);
            break;

          // ปิดเครื่องทำฟองสบู่ (Relay 2)
          case 'C':
          case 'X':
            Serial.println("🗣️ เสี่ยวจื่อสั่ง: ปิดเครื่องทำฟองสบู่ (Relay 2 OFF)");
            relayState2 = false;
            digitalWrite(relayPin2, HIGH);
            break;

          default:
            break;
        }

        // 4. หากมีระยะเวลาส่งมาด้วย (durationSec > 0) ให้หน่วงเวลาตามสั่งแล้วหยุดมอเตอร์
        if (durationSec > 0 && (commandCode == 'F' || commandCode == 'B' || commandCode == 'L' || commandCode == 'R')) {
          delay((unsigned long)(durationSec * 1000)); // แปลงวินาทีเป็นมิลลิวินาที
          stopMotors();
          isVoiceControl = false;
          Serial.println("⏱️ ครบกำหนดเวลา สั่งหยุดมอเตอร์อัตโนมัติ");
        }
      }
    }
  }
}

// === PS3 Events (ตัดการคุมด้วยเสียงทันทีเมื่อจอยทำงาน) ===
void notify() {
  static unsigned long lastSpeedChangeTime = 0;
  unsigned long now = millis();

  // --- Relay ---
  if (Ps3.event.button_down.square) {
    relayState1 = !relayState1;
    digitalWrite(relayPin1, relayState1);
  }
  if (Ps3.event.button_down.circle) {
    relayState2 = !relayState2;
    digitalWrite(relayPin2, relayState2);
  }

  // --- Speed ---
  if (now - lastSpeedChangeTime >= 200) {
    if (Ps3.data.button.triangle) maxSpeed = min(maxSpeedLimit, maxSpeed + speedStep);
    if (Ps3.data.button.cross) maxSpeed = max(minSpeed, maxSpeed - speedStep);
    lastSpeedChangeTime = now;
  }

  // --- Light Show ---
  if (Ps3.event.button_down.l1 || Ps3.event.button_down.r1) {
    if (!lightShowActive) {
      lightShowActive = true;
      lightStartTime = millis();
      Serial.printf("🌈 Start Light Show Mode %d\n", lightMode);
    } else {
      lightMode = (lightMode + 1) % 3;
      lightStartTime = millis();
      Serial.printf("🔄 Change Light Mode to %d\n", lightMode);
    }
  }

  // --- Auto Servo Toggle ---
  if (Ps3.event.button_down.start) {
    autoMode = !autoMode;
    forceTo90 = false;
    Serial.printf("🌀 Auto Servo Mode: %s\n", autoMode ? "ON" : "OFF");
    if (autoMode) nextTargetTime = millis() + random(1000, 4001);
  }

  // --- Servo Manual ---
  if (Ps3.data.button.r2) {
    if (autoMode) autoMode = false, forceTo90 = false;
    angle5 = max(servoMin, angle5 - 1);
    myservo3.write(angle5);
    delay(20);
  }
  if (Ps3.data.button.l2) {
    if (autoMode) autoMode = false, forceTo90 = false;
    angle5 = min(servoMax, angle5 + 1);
    myservo3.write(angle5);
    delay(20);
  }

  // --- กดปุ่ม SELECT เพื่อเล่น / ข้ามไปเพลงถัดไป ---
  if (Ps3.event.button_down.select) {
    playNextTrack();
  }

  // --- Movement (บังคับจากจอย PS3) ---
  if (Ps3.data.button.up) {
    isVoiceControl = false; // ยกเลิกคุมด้วยเสียง
    moveForward();
  }
  else if (Ps3.data.button.down) {
    isVoiceControl = false; // ยกเลิกคุมด้วยเสียง
    moveBackward();
  }
  else if (Ps3.data.button.left) {
    isVoiceControl = false; // ยกเลิกคุมด้วยเสียง
    turnLeft();
  }
  else if (Ps3.data.button.right) {
    isVoiceControl = false; // ยกเลิกคุมด้วยเสียง
    turnRight();
  }
  else {
    int ly = Ps3.data.analog.stick.ly;
    int rx = -Ps3.data.analog.stick.rx;
    if (abs(ly) < 15) ly = 0;
    if (abs(rx) < 15) rx = 0;

    if (ly == 0 && rx != 0) {
      isVoiceControl = false; // ยกเลิกคุมด้วยเสียงเมื่อโยกหมุน
      int spin = map(rx, -128, 127, -maxSpeed, maxSpeed);
      moveMotor(spin, -spin);
      printDirection(0, spin);
    } 
    else if (ly != 0) {
      isVoiceControl = false; // ยกเลิกคุมด้วยเสียงเมื่อโยกเดินหน้า/ถอยหลัง
      int speedVal = map(ly, -128, 127, maxSpeed, -maxSpeed);
      int turnVal  = map(rx, -128, 127, -maxSpeed / 1.2, maxSpeed / 1.2);
      int motorA = constrain(speedVal + turnVal, -255, 255);
      int motorB = constrain(speedVal - turnVal, -255, 255);
      moveMotor(motorA, motorB);
      printDirection(speedVal, turnVal);
    } 
    else {
      if (!isVoiceControl) {
        stopMotors();
      }
    }
  }
}

// === Movement ===
void moveMotor(int aSpeed, int bSpeed) {
  aSpeed = constrain(aSpeed, -255, 255);
  bSpeed = constrain(bSpeed, -255, 255);
  digitalWrite(AIN1, aSpeed > 0 ? HIGH : LOW);
  digitalWrite(AIN2, aSpeed < 0 ? HIGH : LOW);
  ledcWrite(pwmA, abs(aSpeed));
  digitalWrite(BIN1, bSpeed > 0 ? HIGH : LOW);
  digitalWrite(BIN2, bSpeed < 0 ? HIGH : LOW);
  ledcWrite(pwmB, abs(bSpeed));
}

void moveForward()  { moveMotor(maxSpeed, maxSpeed); printDirection(maxSpeed, 0); disableAutoAndReturnTo90(); }
void moveBackward() { moveMotor(-maxSpeed, -maxSpeed); printDirection(-maxSpeed, 0); disableAutoAndReturnTo90(); }
void turnRight()    { moveMotor(-maxSpeed, maxSpeed); printDirection(0, -100); disableAutoAndReturnTo90(); }
void turnLeft()     { moveMotor(maxSpeed, -maxSpeed); printDirection(0, 100); disableAutoAndReturnTo90(); }
void stopMotors()   { moveMotor(0, 0); printDirection(0, 0); }

void disableAutoAndReturnTo90() {
  if (autoMode) {
    autoMode = false;
    forceTo90 = true;
    Serial.println("🚫 AutoMode OFF: Return head to 90°");
  }
}

// === Servo ===
void updateServoHead() {
  unsigned long now = millis();
  if (autoMode) {
    if (currentDirection == "Stop") {
      if (now >= nextTargetTime) {
        targetAngle = random(servoMin, servoMax + 1);
        nextTargetTime = now + random(1000, 4001);
      }
      if (now - lastServoMove >= servoInterval) {
        if (angle5 < targetAngle) angle5++;
        else if (angle5 > targetAngle) angle5--;
        myservo3.write(angle5);
        lastServoMove = now;
      }
    } else if (angle5 != 90) {
      angle5 = 90;
      myservo3.write(angle5);
    }
  } else if (forceTo90) {
    if (now - lastServoMove >= 30) {
      if (angle5 < 90) angle5++;
      else if (angle5 > 90) angle5--;
      myservo3.write(angle5);
      lastServoMove = now;
      if (angle5 == 90) forceTo90 = false;
    }
  }
}

// === Direction Tracking ===
void printDirection(int speed, int turn) {
  String dir;
  if (speed > 0) dir = "Forward";
  else if (speed < 0) dir = "Backward";
  else if (turn > 0) dir = "Left";
  else if (turn < 0) dir = "Right";
  else dir = "Stop";

  if (dir != currentDirection) currentDirection = dir;
  lastDirection = dir;
}

// === NeoPixel Update ===
void updateNeoPixels() {
  unsigned long now = millis();
  if (lightShowActive) {
    if (now - lightStartTime < LIGHT_SHOW_DURATION)
      showLightShowPattern(lightMode, now - lightStartTime);
    else {
      lightShowActive = false;
      pixels.clear();
      pixels.show();
    }
    return;
  }

  if (currentDirection == "Stop") {
    if (!stopActive) stopStartTime = now, stopActive = true;
    unsigned long elapsed = now - stopStartTime;
    if (elapsed <= 5000) {
      pixels.clear();
      if (stopMode == 0) {
        if ((now / 400) % 2 == 0)
          for (int i = 0; i < NUMPIXELS; i++) pixels.setPixelColor(i, pixels.Color(255, 0, 0));
      } else if (stopMode == 1) {
        int val = ((now / 10) % 512);
        int brightness = abs((int)val - 255);
        for (int i = 0; i < NUMPIXELS; i++) pixels.setPixelColor(i, pixels.Color(0, 0, brightness));
      } else if (stopMode == 2) {
        for (int i = 0; i < NUMPIXELS; i++) pixels.setPixelColor(i, pixels.Color(0, 255, 0));
      }
      pixels.show();
    } else {
      pixels.clear();
      pixels.show();
    }
  } else {
    stopActive = false;
    pixels.clear();
    if (currentDirection == "Forward")
      for (int i = 0; i < NUMPIXELS; i++) pixels.setPixelColor(i, pixels.Color(0, 255, 0));
    else if (currentDirection == "Backward")
      for (int i = 0; i < NUMPIXELS; i++) pixels.setPixelColor(i, pixels.Color(255, 0, 0));
    else if (currentDirection == "Left" || currentDirection == "Right")
      for (int i = 0; i < NUMPIXELS; i++) pixels.setPixelColor(i, pixels.Color(255, 255, 0));
    pixels.show();
  }
}

// === Light Show ===
void showLightShowPattern(int mode, unsigned long t) {
  if (mode == 0) {
    for (int i = 0; i < NUMPIXELS; i++) {
      int hue = (i * 50 + (t / 8)) % 360;
      uint32_t color = pixels.ColorHSV(hue * 182, 255, 255);
      pixels.setPixelColor(i, color);
    }
  }
  else if (mode == 1) {
    float wave = sin(t / 100.0) * 0.5 + 0.5;
    int brightness = (int)(wave * 255);
    int blue = brightness;
    int white = brightness / 2;
    for (int i = 0; i < NUMPIXELS; i++) {
      pixels.setPixelColor(i, pixels.Color(white / 3, white, blue));
    }
  }
  else if (mode == 2) {
    int runner = (t / 50) % NUMPIXELS;
    for (int i = 0; i < NUMPIXELS; i++) {
      int flicker = random(150, 255);
      int r = flicker;
      int g = random(40, 100);
      int b = random(0, 20);
      if (random(0, 10) > 7) r = g = b = 0;
      int diff = abs(i - runner);
      if (diff < 2) {
        r = 255;
        g = 180 - diff * 80;
        b = 0;
      }
      pixels.setPixelColor(i, pixels.Color(r, g, b));
    }
  }
  pixels.show();
}

// === ฟังก์ชันสั่งเล่น / เปลี่ยนเพลงถัดไป ===
void playNextTrack() {
  if (!musicPlaying) {
    myDFPlayer.play(currentTrack);
    musicPlaying = true;
    Serial.printf("🎵 เริ่มเล่นเพลงที่ %d\n", currentTrack);
  } else {
    currentTrack++;
    if (currentTrack > MAX_TRACKS) {
      currentTrack = 1;
    }
    myDFPlayer.play(currentTrack);
    Serial.printf("⏭️ ข้ามไปเล่นเพลงที่ %d\n", currentTrack);
  }
}

// === ฟังก์ชันตรวจจับเมื่อเพลงจบลงเอง ===
void checkMusicFinished() {
  if (myDFPlayer.available()) {
    uint8_t type = myDFPlayer.readType();
    int value = myDFPlayer.read();

    if (type == DFPlayerPlayFinished) {
      musicPlaying = false;
      Serial.printf("⏹️ เพลงที่ %d เล่นจบแล้ว (หยุดเล่นเรียบร้อย)\n", value);
      
      currentTrack++;
      if (currentTrack > MAX_TRACKS) {
        currentTrack = 1;
      }
    }
  }
}

วิดีโอตัวอย่างการทำงานของหุ่นยนต์

สาธิตการควบคุมหุ่นยนต์ด้วยระบบเสียงและเอฟเฟกต์การแสดง

หากวิดีโอไม่แสดงผล สามารถ คลิกที่นี่เพื่อเปิดวิดีโอโดยตรง