สร้างเครื่องติดตามราคาคริปโต (Crypto Tracker) จิ๋วแต่แจ๋วด้วย ESP32 และจอ OLED

สายเทรดเดอร์ (Trader) หรือใครที่ถือเหรียญคริปโตอยู่ คงจะเบื่อที่ต้องคอยหยิบมือถือขึ้นมาเช็กราคาบ่อยๆ ใช่ไหมครับ? วันนี้เราจะมาทำโปรเจกต์สนุกๆ สร้าง "เครื่องติดตามราคาคริปโตขนาดพกพา" ที่สามารถตั้งไว้บนโต๊ะทำงานได้เลย!

โปรเจกต์นี้ใช้งบไม่เยอะ ชิ้นส่วนหาง่าย โดยมีหัวใจหลักคือบอร์ด ESP32 ที่ต่อ Wi-Fi ได้ และหน้าจอ OLED 0.96 นิ้ว ตัวเครื่องจะดึงราคาเหรียญดังๆ อย่าง BTC, ETH, SOL, และ DOGE มาโชว์แบบเรียลไทม์ พร้อมกราฟสวยๆ และตัวเลขบอกเปอร์เซ็นต์บวกลบตลอด 24 ชั่วโมง แถมเปลี่ยนเหรียญได้ง่ายๆ แค่กดปุ่ม BOOT บนบอร์ด!


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

  • บอร์ดไมโครคอนโทรลเลอร์ ESP32
  • หน้าจอแสดงผล ElectroPeak 0.96" OLED 64x128 Display Module
  • สายจัมเปอร์ (Jumper Wires) ยาวประมาณ 5 ซม.
  • โปรแกรม Arduino IDE สำหรับอัปโหลดโค้ด
Crypto Tracker with ESP32

การต่อวงจร (Wiring) และข้อควรระวัง

การต่อสายไฟจากหน้าจอ OLED เข้ากับบอร์ด ESP32 ทำได้ง่ายๆ ตามนี้ครับ:

  • VCC -> 3.3V
  • GND -> GND
  • SDA -> พิน D19
  • SCL -> พิน D21

⚠️ ข้อควรระวังขั้นสุด: ห้ามเสียบสายไฟเข้าจอ OLED ในขณะที่บอร์ด ESP32 กำลังเสียบปลั๊กไฟอยู่เด็ดขาด! เพราะกระแสไฟอาจกระชากและทำให้จอ OLED พังถาวรได้ครับ ให้ถอดปลั๊กก่อนต่อสายเสมอ

การตั้งค่าโปรแกรมและซอร์สโค้ด

ก่อนจะอัปโหลดโค้ด ให้คุณเข้าไปในโปรแกรม Arduino IDE และติดตั้งไลบรารี 2 ตัวนี้ก่อนครับ:

  • ArduinoJson (พัฒนาโดย Benoit Blanchon) สำหรับอ่านข้อมูล API
  • ESP8266 and ESP32 OLED Driver for SSD1306 displays (พัฒนาโดย ThingPulse) สำหรับคุมหน้าจอ

วิธีใช้โค้ด: ให้ก๊อปปี้โค้ดด้านล่างนี้ไปใส่ในโปรแกรม แล้วอย่าลืมเปลี่ยนคำว่า "YOUR_SSID" เป็นชื่อ Wi-Fi ของคุณ และ "YOUR_PASSWORD" เป็นรหัสผ่าน Wi-Fi ของคุณก่อนกดอัปโหลดนะครับ!

คลิกเพื่อดูและคัดลอกโค้ดฉบับเต็ม (View More)
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <SH1106Wire.h>
#include <Preferences.h>

// --- ตั้งค่า Wi-Fi ของคุณตรงนี้ ---
const char* WIFI_SSID     = "YOUR_SSID";
const char* WIFI_PASSWORD = "YOUR_PASSWORD";

// ตั้งค่าเหรียญคริปโตที่ต้องการติดตาม
const char* COIN_IDS[]    = { "bitcoin", "ethereum", "solana", "dogecoin" };
const char* COIN_SYMS[]   = { "BTC", "ETH", "SOL", "DOGE" };
const int   COIN_COUNT    = 4;

const int          BUTTON_PIN   = 0;       // ปุ่ม BOOT (GPIO0)
const int          FETCH_MS     = 60000;   // อัปเดตราคาจาก API ทุกๆ 1 นาที (60000 ms)
const int          HISTORY_LEN  = 30;      // จำนวนจุดบนกราฟ
const unsigned long SLEEP_MS    = 300000;  // พักหน้าจออัตโนมัติเมื่อไม่ใช้งาน 5 นาที

SH1106Wire display(0x3C, 19, 21);
Preferences prefs;

float prices[4]  = {0};
float changes[4] = {0};

float history[4][30];
int   histCount[4] = {0};
int   histHead[4]  = {0};

bool  dataReady    = false;
int   currentCoin  = 0;

unsigned long lastFetch    = 0;
unsigned long lastDebounce = 0;
unsigned long lastActivity = 0;
bool          lastBtn      = HIGH;

void saveHistory() {
  prefs.begin("ticker", false);
  for (int i = 0; i < COIN_COUNT; i++) {
    char key[16];
    sprintf(key, "h%d", i);
    prefs.putBytes(key, history[i], sizeof(float) * HISTORY_LEN);
    sprintf(key, "hc%d", i);
    prefs.putInt(key, histCount[i]);
    sprintf(key, "hh%d", i);
    prefs.putInt(key, histHead[i]);
  }
  prefs.end();
}

void loadHistory() {
  prefs.begin("ticker", true);
  for (int i = 0; i < COIN_COUNT; i++) {
    char key[16];
    sprintf(key, "h%d", i);
    prefs.getBytes(key, history[i], sizeof(float) * HISTORY_LEN);
    sprintf(key, "hc%d", i);
    histCount[i] = prefs.getInt(key, 0);
    sprintf(key, "hh%d", i);
    histHead[i] = prefs.getInt(key, 0);
  }
  prefs.end();
  if (histCount[0] > 0) dataReady = true;
}

void pushHistory(int idx, float price) {
  history[idx][histHead[idx]] = price;
  histHead[idx] = (histHead[idx] + 1) % HISTORY_LEN;
  if (histCount[idx] < HISTORY_LEN) histCount[idx]++;
}

float getHistory(int idx, int age) {
  int pos = (histHead[idx] - 1 - age + HISTORY_LEN) % HISTORY_LEN;
  return history[idx][pos];
}

String buildURL() {
  String ids = "";
  for (int i = 0; i < COIN_COUNT; i++) {
    if (i > 0) ids += "%2C";
    ids += COIN_IDS[i];
  }
  return "https://api.coingecko.com/api/v3/simple/price?ids=" + ids +
         "&vs_currencies=usd&include_24hr_change=true";
}

void fetchPrices() {
  if (WiFi.status() != WL_CONNECTED) return;

  HTTPClient http;
  http.begin(buildURL());
  http.setTimeout(8000);
  int code = http.GET();

  if (code == 200) {
    String payload = http.getString();
    DynamicJsonDocument doc(2048);
    if (deserializeJson(doc, payload) == DeserializationError::Ok) {
      for (int i = 0; i < COIN_COUNT; i++) {
        if (doc.containsKey(COIN_IDS[i])) {
          prices[i]  = doc[COIN_IDS[i]]["usd"].as();
          changes[i] = doc[COIN_IDS[i]]["usd_24h_change"].as();
          pushHistory(i, prices[i]);
        }
      }
      dataReady = true;
      saveHistory();
    }
  }
  http.end();
}

String formatPrice(float p) {
  if (p >= 10000.0f) {
    char buf[12];
    sprintf(buf, "$%.0f", p);
    return String(buf);
  } else if (p >= 1000.0f) {
    char buf[12];
    sprintf(buf, "$%.1f", p);
    return String(buf);
  } else if (p >= 1.0f) {
    char buf[12];
    sprintf(buf, "$%.2f", p);
    return String(buf);
  } else {
    char buf[14];
    sprintf(buf, "$%.4f", p);
    return String(buf);
  }
}

void drawGraph(int idx) {
  int n = histCount[idx];
  if (n < 2) {
    display.setFont(ArialMT_Plain_10);
    display.setTextAlignment(TEXT_ALIGN_CENTER);
    display.drawString(64, 42, "Building history...");
    return;
  }

  float minP = history[idx][0], maxP = history[idx][0];
  for (int i = 1; i < n; i++) {
    if (history[idx][i] < minP) minP = history[idx][i];
    if (history[idx][i] > maxP) maxP = history[idx][i];
  }
  float range = maxP - minP;
  if (range < 0.0001f) range = 1.0f;

  const int GX = 0, GY = 29, GW = 128, GH = 34;
  display.drawLine(GX, GY + GH, GX + GW, GY + GH);

  int pts = min(n, HISTORY_LEN);
  float xStep = (float)GW / (float)(pts - 1);

  int prevX = -1, prevY = -1;
  for (int i = 0; i < pts; i++) {
    float val = getHistory(idx, pts - 1 - i);
    int px = GX + (int)(i * xStep);
    int py = GY + GH - 1 - (int)((val - minP) / range * (GH - 2));
    py = constrain(py, GY, GY + GH - 1);

    if (prevX >= 0) {
      display.drawLine(prevX, prevY, px, py);
    }
    prevX = px;
    prevY = py;
  }

  display.setFont(ArialMT_Plain_10);
  display.setTextAlignment(TEXT_ALIGN_RIGHT);

  auto shortFmt = [](float p) -> String {
    if (p >= 1000.0f) {
      char buf[10];
      sprintf(buf, "%.1fk", p / 1000.0f);
      return String(buf);
    } else if (p >= 1.0f) {
      char buf[10];
      sprintf(buf, "%.1f", p);
      return String(buf);
    } else {
      char buf[10];
      sprintf(buf, "%.3f", p);
      return String(buf);
    }
  };

  display.drawString(128, GY - 1, shortFmt(maxP));
  display.drawString(128, GY + GH - 10, shortFmt(minP));
}

void drawCoin(int idx) {
  display.clear();

  display.setFont(ArialMT_Plain_16);
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.drawString(0, 0, COIN_SYMS[idx]);

  display.setFont(ArialMT_Plain_10);
  display.setTextAlignment(TEXT_ALIGN_CENTER);
  display.drawString(64, 4, formatPrice(prices[idx]));

  display.setTextAlignment(TEXT_ALIGN_RIGHT);
  bool up = changes[idx] >= 0;
  String chg = (up ? "+" : "") + String(changes[idx], 1) + "%";
  display.drawString(128, 4, chg);

  display.drawLine(0, 18, 128, 18);

  int dotY = 24;
  int totalW = COIN_COUNT * 10 - 2;
  int startX = (128 - totalW) / 2;
  for (int i = 0; i < COIN_COUNT; i++) {
    int x = startX + i * 10;
    if (i == idx) display.fillCircle(x, dotY, 3);
    else          display.drawCircle(x, dotY, 3);
  }

  drawGraph(idx);
  display.display();
}

void drawLoading(const char* msg) {
  display.clear();
  display.setFont(ArialMT_Plain_10);
  display.setTextAlignment(TEXT_ALIGN_CENTER);
  display.drawString(64, 26, msg);
  display.display();
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  display.init();
  display.flipScreenVertically();
  display.setContrast(200);

  drawLoading("Connecting WiFi...");
  loadHistory();

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  int tries = 0;
  while (WiFi.status() != WL_CONNECTED && tries < 20) {
    delay(500);
    tries++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    drawLoading("Fetching prices...");
    fetchPrices();
  } else {
    drawLoading("WiFi failed!");
    delay(2000);
  }

  lastFetch = millis();
  lastActivity = millis();
}

void loop() {
  unsigned long now = millis();

  bool btn = digitalRead(BUTTON_PIN);
  if (btn == LOW && lastBtn == HIGH && (now - lastDebounce > 200)) {
    currentCoin = (currentCoin + 1) % COIN_COUNT;
    lastDebounce = now;
    lastActivity = now;
  }
  lastBtn = btn;

  if (now - lastActivity >= SLEEP_MS) {
    display.clear();
    display.display();           
    display.displayOff();        
    esp_sleep_enable_ext0_wakeup((gpio_num_t)BUTTON_PIN, 0); 
    esp_deep_sleep_start();      
  }

  if (now - lastFetch >= FETCH_MS) {
    fetchPrices();
    lastFetch = now;
  }

  if (dataReady) drawCoin(currentCoin);
  else           drawLoading("Waiting...");

  delay(100);
}

สรุป: โปรเจกต์นี้เป็นตัวอย่างที่ดีมากในการนำบอร์ด ESP32 มาเชื่อมต่อกับ API (CoinGecko) เพื่อดึงข้อมูลแบบเรียลไทม์มาแสดงผลบนหน้าจอ OLED แถมโค้ดยังฉลาดพอที่จะวาดกราฟและเข้าสู่โหมดประหยัดพลังงาน (Deep Sleep) อัตโนมัติเมื่อไม่มีคนกดปุ่ม ใครอยากเพิ่มเหรียญคริปโตอื่นๆ ก็สามารถแก้ชื่อเหรียญในโค้ดได้เลยครับ!


คำเตือน: เนื้อหานี้เป็นการสรุปและเรียบเรียงจากบทความต้นฉบับภาษาอังกฤษ ข้อมูลฉบับภาษาไทยอาจมีความคลาดเคลื่อนบางประการจากการตีความหรือย่อเนื้อหา

อ้างอิงและเรียบเรียงโดย: Globalbyteshop Blog

แหล่งที่มาหลัก:
- โปรเจกต์โดย peperusnak: A little compact crypto tracker with esp32 and oled display (Hackster.io)

แท็ก


Blog posts

เข้าสู่ระบบ

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

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