สร้างนาฬิกาปลุกพูดได้สุดล้ำ! DIY Speaking Alarm Clock ด้วย XIAO ESP32-S3 ⏰🗣️

XIAO ESP32-S3 Speaking Alarm Clock

สวัสดีชาว Maker สายคราฟต์และนักประดิษฐ์ทุกคนครับ! เบื่อไหมกับนาฬิกาปลุกเสียง "ติ๊ดๆ" แบบเดิมๆ ที่ดังขึ้นมาทีไรก็ต้องมานั่งเดาว่าตั้งปลุกไว้ทำไม? วันนี้เราจะพามาอัปเกรดนาฬิกาปลุกให้ฉลาดขึ้นด้วยโปรเจกต์ DIY Speaking Alarm Clock นาฬิกาปลุกที่สามารถ "พูดอ่านข้อความ" แจ้งเตือนให้เราฟังได้ด้วยเสียงที่เป็นธรรมชาติผ่านพลัง AI!

โปรเจกต์นี้เราจะใช้บอร์ดจิ๋วแต่แจ๋วอย่าง XIAO ESP32-S3 เป็นตัวควบคุมหลัก ผสานเข้ากับระบบ Text-to-Speech (TTS) บน Cloud ทำให้เราไม่ต้องประมวลผลเสียงหนักๆ บนบอร์ดเอง แถมยังเชื่อมต่อ Wi-Fi เพื่อตั้งเวลาปลุกผ่านหน้าเว็บ (Web interface) บนมือถือได้เลย สะดวกสุดๆ ครับ!

ฟีเจอร์เด่นและสถาปัตยกรรมระบบ (Features & Architecture)

การทำงานของนาฬิกาตัวนี้เน้นความเรียบง่ายแต่ล้ำสมัย (Standalone device) โดยมีระบบเบื้องหลังดังนี้:

  • เชื่อมต่อ Wi-Fi และเวลาแม่นยำ: ดึงเวลาจาก NTP Server อัตโนมัติ ไม่ต้องต่อโมดูล RTC แยกให้เปลืองพื้นที่
  • ตั้งปลุกผ่าน Web Browser: บอร์ด ESP32 จะจำลองตัวเองเป็น Web Server ให้เราใช้มือถือหรือคอมพิวเตอร์เข้าไปเพิ่ม/ลบ เวลาปลุกพร้อมพิมพ์ข้อความที่ต้องการให้พูดได้เลย
  • AI Text-to-Speech: เมื่อถึงเวลาปลุก บอร์ดจะส่งข้อความของเราไปที่ Wit.ai (Cloud TTS API) เพื่อแปลงเป็นไฟล์เสียงกลับมาเล่น
  • ระบบเสียงและจอแสดงผล: ส่งเสียงผ่านแอมป์ MAX98357A I2S ออกลำโพงชัดเจน และมีจอ 0.96" OLED คอยบอกเวลาปัจจุบันและคิวปลุกรอบถัดไป

อุปกรณ์ที่ต้องใช้ (Components Required)

ฮาร์ดแวร์ที่ใช้ในโปรเจกต์นี้มีไม่เยอะครับ เน้นชิ้นเล็กๆ เพื่อให้ประกอบออกมาได้กะทัดรัด:

  • บอร์ดไมโครคอนโทรลเลอร์: XIAO ESP32-S3 (หรือบอร์ด ESP32 รุ่นอื่นๆ ก็ประยุกต์ใช้ได้)
  • โมดูลขยายเสียง: MAX98357A I2S Amplifier พร้อมลำโพง 4 Ω หรือ 8 Ω
  • จอแสดงผล: 0.96” OLED Display (SSD1306)
  • ปุ่มกด (Push Button Switch) สำหรับกดหยุดเสียงปลุก
  • สาย Jumper สำหรับต่อวงจร

💡 Maker's Tip: ทำโปรเจกต์นาฬิกาทั้งที จะปล่อยแผงวงจรเปลือยๆ ก็คงไม่เท่ใช่ไหมครับ? แนะนำให้ใช้ เครื่อง 3D Printer ออกแบบและปริ้นท์เคสหรือกล่องใส่นาฬิกาเก๋ๆ ตามสไตล์ของคุณเองเลย!

หากเพื่อนๆ กำลังมองหา เส้นพลาสติก 3D Print เกรดพรีเมียม, บอร์ด ESP32, หรือ จอ OLED เพื่อไปลุยโปรเจกต์นี้ แวะไปช้อปปิ้งของแท้พร้อมส่งได้ที่ Globalbyte ได้เลยครับ ครบจบในที่เดียว!

การต่อวงจร (Hardware Connections & Pinout)

การต่อสายใช้งานกับบอร์ด XIAO ESP32-S3 สามารถทำตามตาราง Pinout ด้านล่างนี้ได้เลยครับ (อย่าลืมว่าปุ่มกดถูกเซ็ตให้เป็น Active LOW นะครับ คือต่อเข้าขา D1 กับ GND)

โมดูล (Module) พอร์ต (Signal) ต่อเข้า ESP32-S3 (Pin)
MAX98357A Amplifier BCLK D8
LRC D9
DIN D10
GND GND
Vin 5V
OLED Display SDA D5
SCL D4
VCC 3.3V
GND GND
Push Button ขาที่ 1 D1
ขาที่ 2 GND

ระบบตั้งเวลาและเสียงพูดทำงานยังไง? (How it Works)

หัวใจสำคัญของโปรเจกต์นี้คือการจัดการเวลา (Alarm Scheduling Logic) บอร์ด ESP32 จะเก็บข้อมูลเวลาที่เราตั้งไว้สูงสุด 10 รายการในหน่วยความจำชั่วคราว (Array) และคอยเช็คเวลาจากตัวมันเอง (ใช้วิธีเช็ค millis() เทียบกับฐานเวลา NTP ทำให้ไม่ต้องดึงเน็ตตลอดเวลา)

เมื่อเวลาปัจจุบันตรงกับเวลาที่ตั้งไว้ บอร์ดจะนำข้อความของเรามารวมกับประโยคบอกเวลา แล้วส่งไปให้ Wit.ai ผ่าน API Token เมื่อแปลงเป็นเสียงเสร็จก็จะส่งออกไปที่ลำโพงทันที (ข้อควรระวังคือ อุปกรณ์ต้องต่อ Wi-Fi ไว้ตลอดเวลานะครับ ไม่งั้นจะใช้ฟีเจอร์แปลงเสียงไม่ได้)

วิดีโอสาธิตการทำงาน (Demonstration)

อยากรู้ว่าเสียงพูดที่ได้จากบอร์ดจะสมูทแค่ไหน ลองกดดูวิดีโอสาธิตการทำงานจากช่อง Circuit Digest ด้านล่างนี้ได้เลยครับ!

โค้ดฉบับเต็ม (Source Code)

ในการรันโค้ดนี้ผ่าน Arduino IDE คุณต้องลงไลบรารีเพิ่มเติมอย่าง WitAITTS, Adafruit_SSD1306 และอย่าลืมเปลี่ยน WIFI_SSID, WIFI_PASSWORD รวมถึง WIT_TOKEN ให้เป็นของคุณเองด้วยนะครับ (สามารถสมัคร Wit.ai ได้ฟรี)

C++ (Arduino IDE - Speaking Alarm Clock)
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
#include <WitAITTS.h>
#include <ESPmDNS.h>
// -------- OLED ADDITIONS --------
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ================= CREDENTIALS =================
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; 
const char* WIT_TOKEN = "YOUR_API_KEY";

// ================= I2S (XIAO ESP32-S3) =================
#define I2S_BCLK 7
#define I2S_LRC  8
#define I2S_DIN  9
#define BTN_STOP 2  // Active LOW

WitAITTS tts(I2S_BCLK, I2S_LRC, I2S_DIN);
WebServer server(80);

// ================= TIME =================
uint32_t baseEpochUTC = 0;
uint32_t baseMillis = 0;
const uint32_t IST_OFFSET = 19800;

uint32_t nowEpochUTC() {
  return baseEpochUTC + (millis() - baseMillis) / 1000;
}

// ================= ALARMS =================
#define MAX_ALARMS 10
struct Alarm {
  uint8_t hour;
  uint8_t minute;
  String message;
  bool played;
};
Alarm alarms[MAX_ALARMS];
uint8_t alarmCount = 0;
bool alarmKilled = false;

// ================= OLED DEFINITIONS =================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET   -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

/* HTML Web Page String omitted for brevity. 
  Please refer to the GitHub repository for the full HTML string.
*/
const char PAGE[] PROGMEM = R"rawliteral(
  )rawliteral";

// ================= HANDLERS =================
void handleRoot(){ server.send_P(200,"text/html",PAGE); }
void handleEpoch(){ server.send(200,"text/plain",String(nowEpochUTC())); }

void handleSet(){
  if(alarmCount>=MAX_ALARMS){ server.send(400,"text/plain","Limit"); return; }
  alarms[alarmCount++] = {
    (uint8_t)server.arg("h").toInt(),
    (uint8_t)server.arg("m").toInt(),
    server.arg("msg"),
    false
  };
  server.send(200,"text/plain","OK");
}

// Additional handlers (Edit, Delete, GetAlarms) are available in the full code.

// ================= OLED UPDATE =================
void updateOLED(){
  display.clearDisplay();
  uint32_t tIST = nowEpochUTC() + IST_OFFSET;
  uint8_t h24 = (tIST / 3600) % 24;
  uint8_t m = (tIST / 60) % 60;
  uint8_t h12 = h24 % 12;
  if(h12 == 0) h12 = 12;
  String ap = (h24 >= 12) ? "PM" : "AM";
  
  display.setTextSize(2);
  int timeX = (SCREEN_WIDTH - 60) / 2;
  display.setCursor(timeX, 0);
  display.printf("%02d:%02d", h12, m);
  
  display.setTextSize(1);
  display.setCursor(timeX + 64, 6);
  display.print(ap);
  display.drawLine(0, 26, SCREEN_WIDTH, 26, SSD1306_WHITE);
  
  display.setCursor(0, 30);
  display.print("NEXT ALARM");
  display.drawLine(0, 40, 60, 40, SSD1306_WHITE);
  
  bool found=false;
  for(int i=0;i=12)?"PM":"AM";
      display.setCursor(0,44);
      display.printf("%02d:%02d %s", ah12, alarms[i].minute, aap.c_str());
      found=true;
      break;
    }
  }
  if(!found){
    display.setCursor(0,44);
    display.print("None");
  }
  display.display();
}

// ================= SETUP =================
void setup(){
  Serial.begin(115200);
  pinMode(BTN_STOP, INPUT_PULLUP);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(300);
  
  if (!MDNS.begin("alarm")) {
    Serial.println("mDNS failed");
  } else {
    Serial.println("mDNS started: http://alarm.local");
  }
  configTime(0, 0, "pool.ntp.org");
  struct tm ti;
  while (!getLocalTime(&ti) || ti.tm_year < 123) delay(500);
  baseEpochUTC = mktime(&ti);
  baseMillis = millis();
  
  tts.begin(WIFI_SSID, WIFI_PASSWORD, WIT_TOKEN);
  server.on("/", handleRoot);
  server.on("/epoch", handleEpoch);
  server.on("/set", handleSet);
  server.begin();
  
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("OLED init failed");
  } else {
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(1);
    display.setCursor(0,0);
    display.println("Speaking Alarm");
    display.println("Clock Ready");
    display.display();
  }
}

// ================= LOOP =================
void loop(){
  if(!alarmKilled) tts.loop();
  server.handleClient();
  
  uint32_t tIST = nowEpochUTC() + IST_OFFSET;
  uint8_t h24 = (tIST / 3600) % 24;
  uint8_t m = (tIST / 60) % 60;
  
  for(int i=0;i=12)?"PM":"AM";
      tts.speak("The time is "+String(h12)+" "+String(m)+" "+ap+". "+alarms[i].message);
    }
  }
  
  if(digitalRead(BTN_STOP)==LOW && !alarmKilled){
    alarmKilled=true;
    tts.stop();
  }
  
  updateOLED();
  delay(10);
}

การแก้ปัญหาเบื้องต้น (Troubleshooting)

  • ต่อ Wi-Fi ไม่ได้: เช็คชื่อ SSID และรหัสผ่านในโค้ดให้ถูกต้อง และลองรีเซ็ตบอร์ดใหม่
  • เข้าหน้าเว็บตั้งปลุกไม่ได้: เปิดดู IP ของบอร์ดใน Serial Monitor (Arduino IDE) แล้วพิมพ์ IP นั้นลงในเว็บบราว์เซอร์ตรงๆ (หากหาผ่าน alarm.local ไม่เจอ)
  • ลำโพงไม่มีเสียง: ตรวจสอบสาย I2S ขา BCLK, LRC, DIN ที่ต่อไปยัง MAX98357A ว่าสลับกันหรือไม่
  • เสียงไม่ยอมพูดข้อความ: โทเคน Wit.ai อาจหมดอายุหรือผิดพลาด ให้เข้าไปสร้าง API Key ใหม่ในเว็บ Wit.ai แล้วมาอัปเดตในโค้ด
  • จอ OLED ไม่ติด: เช็คสาย SDA, SCL ว่าต่อถูกขาหรือไม่ และตรวจสอบ I2C Address (ปกติคือ 0x3C)
*คำเตือน: เนื้อหานี้เป็นการสรุปและเรียบเรียงจากโปรเจกต์ต้นฉบับภาษาอังกฤษ ข้อมูลและขั้นตอนการต่อวงจรบางส่วนอาจถูกย่อทอนเพื่อให้เข้าใจง่าย โค้ดที่แสดงในบทความมีการตัดส่วนของหน้าเว็บ HTML ออกเพื่อความกระชับ ผู้สนใจทำโปรเจกต์สามารถดาวน์โหลดโค้ดฉบับเต็มและศึกษารายละเอียดเชิงลึกทางเทคนิคได้จาก เว็บไซต์ต้นฉบับ

 

แท็ก


Blog posts

เข้าสู่ระบบ

ลืมรหัสผ่านใช่ไหม?

ยังไม่มีบัญชีใช่ไหม?
สร้างบัญชี