นาฬิกาตั้งโต๊ะ ESP32 จอสี ST7789 ดึงเวลาและสภาพอากาศจากเน็ต
ทำนาฬิกาตั้งโต๊ะแบบ SmallTV ด้วย ESP32 และจอ ST7789 1.3" ตั้งเวลาเองจาก NTP ดึงอุณหภูมิจาก Open-Meteo ไม่ต้องใช้ API key หรี่จอตอนกลางคืน กดปุ่มเปลี่ยนหน้า
นาฬิกาตั้งโต๊ะจอสีที่บอกเวลาและสภาพอากาศ ทำได้ด้วย ESP32 กับจอ IPS ST7789 1.3" (240×240) ราคารวมราว ฿285–575 ถูกกว่าตัวสำเร็จรูปที่ขายราว ฿500–900 เวลาตั้งเองอัตโนมัติจากอินเทอร์เน็ต (NTP) สภาพอากาศดึงจาก Open-Meteo ซึ่งใช้ได้ฟรีโดยไม่ต้องสมัคร API key โค้ดทั้งหมดอยู่ในบทความนี้ แก้แค่ชื่อ WiFi และพิกัดบ้านก็ใช้ได้
ซื้อหรือทำเอง
ซื้อสำเร็จ: นาฬิกาตั้งโต๊ะจอเล็กแบบ SmallTV ที่บอกเวลา สภาพอากาศ และแสดงรูปได้ ราคาประมาณ ฿500–900 มาพร้อมกล่อง ตั้งค่า WiFi แล้วใช้ได้เลย
ทำเอง: ใช้ของ 4 อย่าง รวมราว ฿285–575 (รายการพร้อมลิงก์อยู่ในหัวข้อชิ้นส่วนด้านล่าง)
- บอร์ด ESP32 DevKit: ต่อ WiFi ดึงเวลาและสภาพอากาศ
- จอ IPS ST7789 1.3" (240×240): แสดงนาฬิกาและอุณหภูมิ
- ปุ่มกด: เปลี่ยนหน้าจอ
- สายจัมเปอร์ และอะแดปเตอร์ USB 5 V ของโทรศัพท์ กล่องใช้กล่องกระดาษแข็งหรือพิมพ์ 3D
ราคาเป็นค่าประมาณ ทำเองได้ข้อมูลอื่นเพิ่มตามต้องการ เช่น ฝุ่น PM2.5 ในห้อง หรือข้อความจาก Telegram ถ้าต้องการแค่นาฬิกาที่ตั้งได้ทันทีโดยไม่อยากต่อสาย ซื้อสำเร็จสะดวกกว่า
ชิ้นส่วน
| ชิ้นส่วน | จำนวน | ราคาประมาณ | ร้าน |
|---|---|---|---|
| ESP32 DevKit V1 (30 ขา) | 1 | ฿130–฿250 | Shopee |
| IPS TFT ST7789 1.3" 240×240 | 1 | ฿120–฿250 | Shopee |
| ปุ่มกด tactile 12 mm + ฝาครอบ | 1 | ฿5–฿15 | Shopee |
| สายจัมเปอร์ ผู้-ผู้ / ผู้-เมีย ชุด 40 เส้น | 1 | ฿30–฿60 | Shopee |
| รวมโดยประมาณ | ฿285–฿575 | ||
ราคาประมาณการ ณ 2026-10-31 ยังไม่ได้ตรวจกับร้าน · บางลิงก์เป็นลิงก์ affiliate (อ่านเพิ่ม)
ไฟเลี้ยงใช้สาย USB กับอะแดปเตอร์โทรศัพท์ได้เลย ทั้งชุดกินไฟราว 100–200 mA
ต่อสาย
จอ ST7789 1.3" ส่วนใหญ่มี 7 ขาและไม่มีขา CS ขาที่เขียนว่า SCL / SDA บนจอนี้คือสาย SPI (clock และ data)
ไม่ใช่ I²C แม้จะใช้ชื่อเดียวกัน ให้ต่อเข้าขา SPI ของ ESP32:
| ขาจอ | ESP32 | หน้าที่ |
|---|---|---|
GND | GND | |
VCC | 3V3 | จอรุ่นนี้ใช้ 3.3 V |
SCL | GPIO 18 | SPI clock (SCK) |
SDA | GPIO 23 | SPI data (MOSI) |
RES | GPIO 17 | reset |
DC | GPIO 16 | บอกจอว่าเป็นคำสั่งหรือข้อมูลภาพ |
BLK | GPIO 22 | ไฟหลังจอ ใช้ PWM หรี่ได้ ถ้าไม่ต่อไฟหลังจอจะสว่างเต็มตลอด |
CS (ถ้ามี) | GPIO 5 | แก้ TFT_CS ในโค้ดเป็น 5 |
ปุ่มเปลี่ยนหน้าต่อระหว่าง GPIO 27 กับ GND ใช้ pull-up ในตัว (รายละเอียดใน ต่อปุ่มกดกับ ESP32)
ไลบรารีที่ต้องติดตั้ง
- Adafruit ST7735 and ST7789 Library (ติดตั้ง Adafruit GFX ให้ด้วย)
- ArduinoJson ของ Benoit Blanchon เวอร์ชัน 7 ขึ้นไป ใช้แยกค่าจากข้อมูลสภาพอากาศ
อีกตัวเลือกคือไลบรารี TFT_eSPI ซึ่งวาดเร็วกว่าและมี sprite ให้วาดแบบไม่กะพริบ แต่ต้องแก้ไฟล์ User_Setup.h ในโฟลเดอร์ไลบรารีให้ตรงกับขาที่ต่อ
สำหรับนาฬิกาที่เปลี่ยนภาพแค่วินาทีละครั้ง ไลบรารีของ Adafruit เร็วพอและตั้งค่าในโค้ดได้เลย
โค้ด (Arduino IDE)
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <time.h>
const char* WIFI_SSID = "ชื่อ WiFi";
const char* WIFI_PASS = "รหัสผ่าน WiFi";
const float LAT = 13.7563, LON = 100.5018; // กรุงเทพฯ: เปลี่ยนเป็นพิกัดบ้านจาก Google Maps
const int TFT_CS = -1, TFT_DC = 16, TFT_RST = 17, TFT_BL = 22, BUTTON = 27;
Adafruit_ST7789 tft(TFT_CS, TFT_DC, TFT_RST); // SPI ฮาร์ดแวร์: SCL = 18, SDA = 23
float temperature = NAN;
int humidity = -1, weatherCode = -1;
/* ---------- สภาพอากาศจาก Open-Meteo ---------- */
bool fetchWeather() {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
String url = String("https://api.open-meteo.com/v1/forecast?latitude=") + String(LAT, 4) +
"&longitude=" + String(LON, 4) +
"¤t=temperature_2m,relative_humidity_2m,weather_code&timezone=Asia%2FBangkok";
if (!http.begin(client, url)) return false;
if (http.GET() != 200) { http.end(); return false; }
JsonDocument doc;
DeserializationError err = deserializeJson(doc, http.getString());
http.end();
if (err) return false;
temperature = doc["current"]["temperature_2m"] | NAN;
humidity = doc["current"]["relative_humidity_2m"] | -1;
weatherCode = doc["current"]["weather_code"] | -1;
return true;
}
// รหัสสภาพอากาศแบบ WMO ที่ Open-Meteo ส่งมา (ฟอนต์ในตัวไม่มีภาษาไทย จึงแสดงเป็นอังกฤษ)
const char* weatherText(int code) {
if (code == 0) return "Clear";
if (code <= 3) return "Cloudy";
if (code == 45 || code == 48) return "Fog";
if (code >= 51 && code <= 67) return "Rain";
if (code >= 80 && code <= 82) return "Showers";
if (code >= 95) return "Storm";
return "";
}
uint16_t weatherColor(int code) {
if (code == 0) return ST77XX_YELLOW;
if (code >= 51) return ST77XX_CYAN;
return ST77XX_WHITE;
}
/* ---------- วาดหน้าจอ ---------- */
// พิมพ์ข้อความกึ่งกลางจอ ฟอนต์ในตัวกว้าง 6 จุดต่อตัวอักษรที่ size 1
void centered(const char* text, int y, int size, uint16_t color) {
tft.setTextSize(size);
tft.setTextColor(color, ST77XX_BLACK); // ใส่สีพื้นด้วย ทับตัวเลขเก่าโดยไม่ต้องล้างจอ
tft.setCursor((240 - strlen(text) * 6 * size) / 2, y);
tft.print(text);
}
void drawClockPage(const tm &t) {
char buf[32];
strftime(buf, sizeof buf, "%H:%M", &t);
centered(buf, 40, 6, ST77XX_WHITE); // ตัวเลขสูง 48 จุด
strftime(buf, sizeof buf, "%a %d %b", &t);
centered(buf, 104, 2, ST77XX_GREEN);
if (isnan(temperature)) return;
snprintf(buf, sizeof buf, "%.1f C", temperature);
centered(buf, 150, 3, weatherColor(weatherCode));
snprintf(buf, sizeof buf, "%s RH %d%%", weatherText(weatherCode), humidity);
centered(buf, 196, 2, ST77XX_WHITE);
}
void drawWeatherPage() {
char buf[32];
centered("Outside", 24, 2, ST77XX_GREEN);
if (isnan(temperature)) { centered("no data", 100, 2, ST77XX_RED); return; }
snprintf(buf, sizeof buf, "%.1f", temperature);
centered(buf, 70, 7, weatherColor(weatherCode));
snprintf(buf, sizeof buf, "humidity %d%%", humidity);
centered(buf, 150, 2, ST77XX_WHITE);
centered(weatherText(weatherCode), 190, 3, weatherColor(weatherCode));
}
/* ---------- ปุ่ม (debounce 30 ms) ---------- */
bool pressed() {
static bool stable = HIGH, lastRead = HIGH;
static unsigned long changedAt = 0;
bool now = digitalRead(BUTTON);
if (now != lastRead) { lastRead = now; changedAt = millis(); }
if (millis() - changedAt > 30 && now != stable) { stable = now; return stable == LOW; }
return false;
}
/* ---------- โปรแกรมหลัก ---------- */
int page = 0;
unsigned long lastFetch = 0;
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
analogWrite(TFT_BL, 255);
tft.init(240, 240, SPI_MODE3); // จอที่ไม่มีขา CS ต้องใช้ SPI mode 3 (มีขา CS: tft.init(240, 240))
tft.setRotation(2); // ภาพกลับหัว: ลอง 0–3
tft.fillScreen(ST77XX_BLACK);
centered("WiFi...", 110, 2, ST77XX_WHITE);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(200);
configTime(7 * 3600, 0, "pool.ntp.org", "time.google.com"); // เวลาไทย UTC+7
fetchWeather();
lastFetch = millis();
tft.fillScreen(ST77XX_BLACK);
}
void loop() {
if (pressed()) { page = 1 - page; tft.fillScreen(ST77XX_BLACK); }
if (millis() - lastFetch > 15 * 60000UL) { // Open-Meteo ปรับค่าปัจจุบันทุก 15 นาที
lastFetch = millis();
fetchWeather();
}
static int lastSecond = -1;
tm t;
if (!getLocalTime(&t, 10) || t.tm_sec == lastSecond) return;
lastSecond = t.tm_sec;
bool night = t.tm_hour >= 22 || t.tm_hour < 7;
analogWrite(TFT_BL, night ? 20 : 255); // หรี่จอตอนกลางคืน
if (page == 0) drawClockPage(t);
else drawWeatherPage();
}
โค้ดทำงานอย่างไร
- เวลา
configTime()ให้ ESP32 ถามเวลาจากเซิร์ฟเวอร์ NTP แล้วเดินนาฬิกาเองต่อ ถามซ้ำเองเป็นระยะ ไม่ต้องใช้โมดูล RTC ถ้าไฟดับแล้ววายฟายยังไม่กลับ นาฬิกาจะไม่ขึ้นจนกว่าจะต่อเน็ตได้ - สภาพอากาศ Open-Meteo ส่ง JSON กลับมาประมาณนี้ โค้ดอ่านเฉพาะสามค่าใน
current:"current": { "temperature_2m": 31.4, "relative_humidity_2m": 66, "weather_code": 3 } - ไม่กะพริบ
setTextColor(สีตัวอักษร, สีพื้น)วาดพื้นหลังของแต่ละตัวอักษรไปพร้อมกัน ตัวเลขใหม่จึงทับตัวเลขเก่าได้โดยไม่ต้องล้างจอทุกวินาที ล้างจอเฉพาะตอนเปลี่ยนหน้า - หรี่จอ
analogWrite()บนขาBLKปรับความสว่างไฟหลังจอ 0–255 ตั้งช่วงเวลาได้ที่บรรทัดnight
Open-Meteo ให้ใช้ฟรีสำหรับงานที่ไม่ใช่เชิงพาณิชย์ นาฬิกาที่ถามทุก 15 นาทีใช้ไม่ถึง 100 ครั้งต่อวัน ต่ำกว่าโควตาฟรีมาก
ปัญหาที่พบบ่อย
- จอดำ แต่ไฟหลังจอติด ลองเปลี่ยน
SPI_MODE3เป็นSPI_MODE2เช็กว่า SCL / SDA ต่อกับ GPIO 18 / 23 และ DC / RES ไม่สลับกัน - สีเพี้ยนเป็นสีตรงข้าม (ดำเป็นขาว) เพิ่ม
tft.invertDisplay(false)หรือtrueหลังinit() - ภาพเลื่อนหรือมีขอบขยะด้านหนึ่ง จอบางรุ่นเป็น 240×320 หรือ 135×240 แก้ตัวเลขใน
tft.init()ให้ตรงกับขนาดจริง - ค้างที่ "WiFi..." ESP32 ใช้ได้เฉพาะ WiFi 2.4 GHz ถ้าเราเตอร์รวม 2.4 / 5 GHz ไว้ชื่อเดียว บางรุ่นต้องแยกชื่อ
- เวลาเร็วหรือช้าไป 7 ชั่วโมง ตัวเลข
7 * 3600ในconfigTime()คือ UTC+7 ของไทย - ไม่มีสภาพอากาศ (no data) พิมพ์
http.GET()ออก Serial ดูรหัสที่ได้ ถ้าเป็นค่าติดลบแปลว่าต่อเซิร์ฟเวอร์ไม่ได้
ต่อยอด
- เพิ่มหน้าที่สามแสดงฝุ่น PM2.5 จาก เครื่องวัดฝุ่นทำเอง หรือดึงพยากรณ์ฝุ่นจาก Open-Meteo Air Quality API ที่ใช้รูปแบบเดียวกัน
- กดปุ่มค้างเพื่อเปิด-ปิดจอ (โค้ดแยกกดสั้นกับกดค้างอยู่ใน บทความปุ่มกด)
- ใช้จอนี้เป็นหน้าของ หุ่นยนต์ตั้งโต๊ะ ได้ ตัวจำลองรองรับจอ ST7789 ในโมดูล Face Display