#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);
}