นาฬิกาตั้งโต๊ะ 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 (รายการพร้อมลิงก์อยู่ในหัวข้อชิ้นส่วนด้านล่าง)

ราคาเป็นค่าประมาณ ทำเองได้ข้อมูลอื่นเพิ่มตามต้องการ เช่น ฝุ่น PM2.5 ในห้อง หรือข้อความจาก Telegram ถ้าต้องการแค่นาฬิกาที่ตั้งได้ทันทีโดยไม่อยากต่อสาย ซื้อสำเร็จสะดวกกว่า

ชิ้นส่วน

ชิ้นส่วนจำนวนราคาประมาณร้าน
ESP32 DevKit V1 (30 ขา)1฿130–฿250Shopee
IPS TFT ST7789 1.3" 240×2401฿120–฿250Shopee
ปุ่มกด tactile 12 mm + ฝาครอบ1฿5–฿15Shopee
สายจัมเปอร์ ผู้-ผู้ / ผู้-เมีย ชุด 40 เส้น1฿30–฿60Shopee
รวมโดยประมาณ฿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หน้าที่
GNDGND
VCC3V3จอรุ่นนี้ใช้ 3.3 V
SCLGPIO 18SPI clock (SCK)
SDAGPIO 23SPI data (MOSI)
RESGPIO 17reset
DCGPIO 16บอกจอว่าเป็นคำสั่งหรือข้อมูลภาพ
BLKGPIO 22ไฟหลังจอ ใช้ PWM หรี่ได้ ถ้าไม่ต่อไฟหลังจอจะสว่างเต็มตลอด
CS (ถ้ามี)GPIO 5แก้ TFT_CS ในโค้ดเป็น 5

ปุ่มเปลี่ยนหน้าต่อระหว่าง GPIO 27 กับ GND ใช้ pull-up ในตัว (รายละเอียดใน ต่อปุ่มกดกับ ESP32)

ไลบรารีที่ต้องติดตั้ง

อีกตัวเลือกคือไลบรารี 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) +
               "&current=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();
}

โค้ดทำงานอย่างไร

Open-Meteo ให้ใช้ฟรีสำหรับงานที่ไม่ใช่เชิงพาณิชย์ นาฬิกาที่ถามทุก 15 นาทีใช้ไม่ถึง 100 ครั้งต่อวัน ต่ำกว่าโควตาฟรีมาก

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

ต่อยอด