Peter Rusňak
Published

A little compact crypto tracker with esp32 and oled display

By using the boot button of the esp32 you cycle through the 4 preset cryptos. The esp32 checks the online price and updates the graf.

IntermediateFull instructions provided97
A little compact crypto tracker with esp32 and oled display

Things used in this project

Hardware components

0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

10 Pc. Jumper Wire Kit, 5 cm Long
10 Pc. Jumper Wire Kit, 5 cm Long

Story

Read more

Code

code

C/C++
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <SH1106Wire.h>
#include <Preferences.h>

//  User config 
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;       // GPIO0  boot button
const int          FETCH_MS     = 60000;   // ms between API calls
const int          HISTORY_LEN  = 30;      // price points to graph
const unsigned long SLEEP_MS    = 300000;  // 5 min inactivity  sleep
// 

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

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

// Price history ring buffer per coin
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;

//  Save history to flash 
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();
}

//  Load history from flash 
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;
}

//  Ring buffer push 
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) {
  // age 0 = newest, age histCount-1 = oldest
  int pos = (histHead[idx] - 1 - age + HISTORY_LEN) % HISTORY_LEN;
  return history[idx][pos];
}

//  Build CoinGecko URL 
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";
}

//  Fetch prices 
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<float>();
          changes[i] = doc[COIN_IDS[i]]["usd_24h_change"].as<float>();
          pushHistory(i, prices[i]);
        }
      }
      dataReady = true;
      saveHistory();
    }
  }
  http.end();
}

//  Format price 
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);
  }
}

//  Draw graph 
// Graph area: x=0..127, y=28..63 (36px tall)
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;
  }

  // Find min/max in history
  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; // avoid div by zero if flat

  // Graph bounds
  const int GX = 0, GY = 29, GW = 128, GH = 34;

  // Draw baseline
  display.drawLine(GX, GY + GH, GX + GW, GY + GH);

  // Plot points oldestnewest leftright
  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); // oldest first
    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;
  }

  // Min/max labels on right side  small font
  display.setFont(ArialMT_Plain_10);
  display.setTextAlignment(TEXT_ALIGN_RIGHT);

  // Shorten labels for display width
  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));
}

//  Draw full coin screen 
void drawCoin(int idx) {
  display.clear();

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

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

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

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

  // Dot indicators (between header and graph)
  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);
  }

  // Graph
  drawGraph(idx);

  display.display();
}

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

//  Setup 
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();
}

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

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

  //  Sleep after inactivity 
  if (now - lastActivity >= SLEEP_MS) {
    display.clear();
    display.display();           // blank the OLED
    display.displayOff();        // OLED power off
    esp_sleep_enable_ext0_wakeup((gpio_num_t)BUTTON_PIN, 0); // wake on BOOT low
    esp_deep_sleep_start();      // ESP32 enters deep sleep
  }

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

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

  delay(100);
}

Credits

Peter Rusňak
2 projects • 0 followers
High school student, who is interested in robotics, WRO competitions and ai development

Comments