ทำหุ่นยนต์มีหน้า แสดงอารมณ์บนจอ OLED ด้วย ESP32

หุ่นยนต์ตั้งโต๊ะที่มีตาบนจอ OLED SSD1306 กะพริบตาเองได้ หันหัวตามแสงด้วย servo และเปลี่ยนอารมณ์ตามเซนเซอร์ เข้าใกล้แล้วตกใจ เจอแสงแล้วดีใจ มีวงจรและโค้ดครบ

อัปเดต 2026-11-02

หุ่นยนต์ตัวนี้ไม่มีล้อ วางบนโต๊ะแล้วทำตัวเหมือนสัตว์เลี้ยง: มีตาสองข้างบนจอ OLED ที่กะพริบเองเป็นระยะ หันหัวตามแสงด้วย servo SG90 ตกใจเมื่อมีมือเข้าใกล้ ดีใจเมื่อห้องสว่าง เศร้าเมื่อมืด และยิ้มเมื่อถูกลูบหัว (กดปุ่ม) เป็นโปรเจกต์ที่ใช้ชิ้นส่วนไม่กี่ชิ้น แต่ได้ฝึกครบทั้งจอ I²C, servo, เซนเซอร์ analog และการเขียนโปรแกรมแบบไม่ใช้ delay()

เปิดหุ่นตั้งโต๊ะในตัวจำลอง →

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

  1. ตาบนจอ วาดตาเป็นสี่เหลี่ยมมุมมนสองอัน แล้วเปลี่ยนรูปร่างตามอารมณ์ ไม่ต้องใช้ไฟล์รูป
  2. กะพริบตา ทุก 2–5 วินาทีแบบสุ่ม ตาจะหรี่ลงแล้วเปิดกลับภายในราว 0.15 วินาที รายละเอียดเล็กๆ นี้ทำให้หุ่นดูมีชีวิตที่สุด
  3. หัวหันตามแสง LDR สองตัวติดบนหัวหันเฉียงซ้ายขวา servo หมุนหัวไปทางตาที่สว่างกว่าจนสองตาอ่านค่าใกล้กัน
  4. อารมณ์จากเซนเซอร์ เลือกตามลำดับความสำคัญ:
เงื่อนไขอารมณ์
ของอยู่ใกล้กว่า 20 cm (HC-SR04)ตกใจ: ตาโตขึ้น
ถูกกดปุ่มบนหัวไม่เกิน 3 วินาทีที่ผ่านมาดีใจ: ตายิ้ม
แสงต่ำกว่า 20 %เศร้า: เปลือกตาตกด้านนอก
แสงมากกว่า 60 %ดีใจ
ของอยู่ในระยะ 20–50 cmสงสัย: ตาข้างหนึ่งหรี่
ไม่มีอะไรเกิดขึ้นปกติ

ชิ้นส่วน

ชิ้นส่วนสำหรับ ESP32 DevKit V1
ชิ้นส่วนจำนวนราคาประมาณร้าน
ESP32 DevKit V1 (30 ขา)1฿130–฿250Shopee
Ultrasonic HC-SR041฿30–฿60Shopee
โมดูลวัดแสง LDR (มีขา AO)2฿15–฿40Shopee
ปุ่มกด tactile 12 mm + ฝาครอบ1฿5–฿15Shopee
Servo SG901฿40–฿80Shopee
OLED SSD1306 0.96" I²C1฿80–฿150Shopee
ถ่าน 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
ตัวต้านทาน 1 kΩ + 2 kΩ (แบ่งแรงดัน 5 V → 3.3 V)1฿2–฿10Shopee
แนะนำเพิ่ม
ที่ชาร์จถ่าน 186501฿100–฿250Shopee
รวมโดยประมาณ฿522–฿1,175

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

ถ้าหุ่นวางอยู่บนโต๊ะข้างคอมพิวเตอร์ตลอด ใช้ไฟจาก adapter USB 5 V 2 A แทนชุดแบตได้ แต่ servo ต้องรับไฟจากแหล่งที่จ่ายกระแสได้พอ (ดู ต่อ Servo SG90 กับ ESP32 ให้ไม่กระตุก)

วงจรและการต่อขา

การต่อขาบน ESP32 DevKit V1 (จัดขาอัตโนมัติจากตัวจำลอง)
HC-SR04
Front sonar
VCC → 5V
TRIG → GPIO26
ECHO → GPIO34 (ผ่าน 1 kΩ / 2 kΩ)
GND → GND
LDR
Left eye LDR
VCC → 3V3
GND → GND
AO → GPIO32
LDR
Right eye LDR
VCC → 3V3
GND → GND
AO → GPIO33
Tactile 12mm
Head button
COM → GND
NO → GPIO27
SG90
Head servo
GND → GND
VCC → 5V
PWM → GPIO25
SSD1306
Face Display
GND → GND
VCC → 3V3
SCL → GPIO22
SDA → GPIO21

ตัวตรวจวงจรในตัวจำลองเตือนเรื่องเดียว คือขา ECHO ของ HC-SR04 ส่งสัญญาณ 5 V ต้องผ่านตัวต้านทานแบ่งแรงดันก่อนเข้า ESP32 (วิธีต่ออยู่ใน บทความ HC-SR04):

โค้ด (Arduino IDE)

ติดตั้งไลบรารี Adafruit SSD1306 (จะติดตั้ง Adafruit GFX ให้ด้วย) และ ESP32Servo จาก Library Manager

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Servo.h>

// ขาตามตารางด้านบน
const int LDR_LEFT = 32, LDR_RIGHT = 33;
const int TRIG = 26, ECHO = 34;            // ECHO ผ่านตัวต้านทาน 1 kΩ / 2 kΩ
const int BUTTON = 27, SERVO_PIN = 25;

Adafruit_SSD1306 oled(128, 64, &Wire, -1);
Servo head;

enum Mood { NEUTRAL, HAPPY, SAD, SURPRISED, CURIOUS };

/* ---------- ตา ---------- */

// open: 1 = ลืมตาเต็มที่, ค่าน้อยลง = กำลังกะพริบ
void drawEyes(Mood mood, float open) {
  oled.clearDisplay();
  for (int side = -1; side <= 1; side += 2) {        // -1 = ตาซ้าย, +1 = ตาขวา
    int cx = 64 + side * 28, cy = 30;
    int w = 32, h = 28;
    if (mood == SURPRISED) { w = 30; h = 40; }
    if (mood == CURIOUS && side > 0) h = 16;       // ตาขวาหรี่ = สงสัย
    h = max(2, int(h * open));
    oled.fillRoundRect(cx - w / 2, cy - h / 2, w, h, min(8, h / 2), SSD1306_WHITE);
    if (mood == HAPPY)                                 // ตัดขอบล่างเป็นโค้ง = ตายิ้ม
      oled.fillCircle(cx, cy + h / 2 + 10, w / 2 + 4, SSD1306_BLACK);
    if (mood == SAD) {                                 // เปลือกตาตกด้านนอก
      int outer = cx + side * w / 2, inner = cx - side * w / 2, top = cy - h / 2;
      oled.fillTriangle(outer, top, inner, top, outer, cy + 2, SSD1306_BLACK);
    }
  }
  oled.display();
}

float blink() {                       // คืนค่าการลืมตา 0.1–1 ตามจังหวะกะพริบ
  static unsigned long next = 3000, start = 0;
  unsigned long now = millis();
  if (now > next) { start = now; next = now + random(2000, 5000); }
  unsigned long t = now - start;
  if (t < 75) return 1.0 - t / 75.0 * 0.9;      // หลับตาลง
  if (t < 150) return 0.1 + (t - 75) / 75.0 * 0.9;   // ลืมตาขึ้น
  return 1.0;
}

/* ---------- เซนเซอร์ ---------- */

float distanceCm() {
  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
  digitalWrite(TRIG, LOW);
  unsigned long us = pulseIn(ECHO, HIGH, 25000);   // ไม่ได้ยินเสียงสะท้อนใน 25 ms = ไกลเกินวัด
  return us ? us / 58.0 : 999;
}

int lightPercent(int pin) { return analogRead(pin) * 100 / 4095; }

/* ---------- หัวหันตามแสง ---------- */

float headAngle = 90;
void trackLight() {
  int l = lightPercent(LDR_LEFT), r = lightPercent(LDR_RIGHT);
  if (l + r < 5) return;                           // มืดเกินไป ไม่รู้ว่าแสงอยู่ทางไหน
  float diff = float(r - l) / (r + l);            // -1..1: บวก = ขวาสว่างกว่า
  if (abs(diff) < 0.05) return;                    // สองตาใกล้กันแล้ว: หันถูกทาง
  headAngle = constrain(headAngle + diff * 3, 10, 170);   // ถ้าหัวหันหนีแสง ให้เปลี่ยน + เป็น -
  head.write(int(headAngle));
}

/* ---------- โปรแกรมหลัก ---------- */

unsigned long patted = 0;

void setup() {
  pinMode(TRIG, OUTPUT);
  pinMode(ECHO, INPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  Wire.begin(21, 22);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);          // จอไม่ขึ้น: ลอง 0x3D
  head.setPeriodHertz(50);
  head.attach(SERVO_PIN, 500, 2400);
  head.write(int(headAngle));
  randomSeed(analogRead(LDR_LEFT));
}

void loop() {
  static unsigned long lastSense = 0;
  static Mood mood = NEUTRAL;

  if (digitalRead(BUTTON) == LOW) patted = millis();

  if (millis() - lastSense > 50) {                 // อ่านเซนเซอร์ 20 ครั้งต่อวินาที
    lastSense = millis();
    trackLight();
    float near = distanceCm();
    int light = max(lightPercent(LDR_LEFT), lightPercent(LDR_RIGHT));
    if (near < 20)                     mood = SURPRISED;
    else if (millis() - patted < 3000) mood = HAPPY;
    else if (light < 20)               mood = SAD;
    else if (light > 60)               mood = HAPPY;
    else if (near < 50)                mood = CURIOUS;
    else                               mood = NEUTRAL;
  }
  drawEyes(mood, blink());                         // วาดทุกรอบ การกะพริบจึงลื่น
}

ค่าแสง 20 % และ 60 % ขึ้นกับห้องและโมดูล LDR แต่ละตัว ให้เปิด Serial แล้วพิมพ์ค่า lightPercent() ตอนเปิดไฟและปิดไฟ แล้วตั้งเกณฑ์ให้อยู่ระหว่างสองค่านั้น โมดูล LDR บางรุ่นให้ค่ายิ่งมากยิ่งมืด ถ้าเป็นแบบนั้นให้ใช้ 100 - …

ปัญหาที่พบบ่อย

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

หุ่นในตัวจำลองมีชิ้นส่วนครบตามบทความ: จอ SSD1306, หัว servo, LDR สองตา, HC-SR04 และปุ่มบนหัว โปรแกรมตัวอย่างในหน้า Code ทำงานแบบเดียวกับโค้ด Arduino ด้านบน ลองทำแบบนี้แล้วดูหน้าเปลี่ยน:

ลองโปรแกรมนี้ในตัวจำลอง →

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

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

<iframe src="https://ai-rover.app/app/?preset=desk-buddy&embed=1" title="หุ่นยนต์ตั้งโต๊ะมีหน้า – AI Rover Studio" width="720" height="520" style="max-width:100%;border:0;border-radius:12px" loading="lazy"></iframe>