Tiny monitor based on ESP32C3 and 0.96" OLED

Tiny monitor based on ESP32C3 and 0.96" OLED


It automatically connects to Wi-Fi and obtains real-time updates of the CNN Fear Index. You can reprogram it with Arduino just like you would a regular ESP32C3.

Order link

Example code is as follows:

/**************************************************************************
 This is an example for our Monochrome OLEDs based on SSD1306 drivers

 Pick one up today in the adafruit shop!
 ------> http://www.adafruit.com/category/63_98

 This example is for a 128x32 pixel display using SPI to communicate
 4 or 5 pins are required to interface.

 Adafruit invests time and resources providing this open
 source code, please support Adafruit and open-source
 hardware by purchasing products from Adafruit!

 Written by Limor Fried/Ladyada for Adafruit Industries,
 with contributions from the open source community.
 BSD license, check license.txt for more information
 All text above, and the splash screen below must be
 included in any redistribution.
 **************************************************************************/

#include <WiFi.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <time.h>

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ==================== WiFi Configuration ====================
const char* ssid     = "TELUSxxxx";
const char* password = "xxxxxxx";

// ==================== OLED Configuration ====================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ==================== NTP Time Server Configuration ====================
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -28800;  // Vancouver timezone UTC-8 (Pacific Time)
const int daylightOffset_sec = 3600; // Daylight saving time +1 hour

// ==================== Data Structure ====================
struct FearGreedData {
  float value;
  String classification;
  String timestamp;
  bool isValid;
};

// ==================== Global Variables ====================
FearGreedData lastData;
unsigned long lastFetchTime = 0;
const unsigned long fetchInterval = 3600000; // Update every hour

// ==================== Function Declarations ====================
void connectWiFi();
void initTime();
FearGreedData fetchCnnFearGreedIndex();
void displayFearGreedIndex(const FearGreedData& data, int hour, int minute);
String getClassificationSymbol(float value);
String getClassificationText(float value);
void drawProgressBar(int x, int y, int width, int height, float value, float minVal, float maxVal);

// ==================== Initialization ====================
void setup() {
  Serial.begin(115200);
  delay(100);
 
  // Initialize I2C (SDA=3, SCL=4)
  Wire.begin(3, 4);
 
  // Initialize OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 initialization failed"));
    for(;;);
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(2000); // Pause for 2 seconds

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println(F("Initializing..."));
  display.println(F("Connecting WiFi..."));
  display.display();
 
  // Connect WiFi
  connectWiFi();
 
  // Initialize time
  display.println(F("Syncing time..."));
  display.display();
  initTime();
 
  display.println(F("Ready!"));
  display.display();
  delay(2000);
 
  // Initialize data
  lastData.isValid = false;
  lastFetchTime = 0;
}

// ==================== Main Loop ====================
void loop() {
  // Get current time
  struct tm timeinfo;
  int currentHour = 0;
  int currentMinute = 0;
 
  if(getLocalTime(&timeinfo)) {
    currentHour = timeinfo.tm_hour;
    currentMinute = timeinfo.tm_min;
  }
 
  // Periodically fetch fear & greed index data
  unsigned long currentTime = millis();
  if(currentTime - lastFetchTime >= fetchInterval || !lastData.isValid) {
    lastData = fetchCnnFearGreedIndex();
    lastFetchTime = currentTime;
  }
 
  // Display data
  displayFearGreedIndex(lastData, currentHour, currentMinute);
 
  // Refresh display every 10 seconds (update time)
  delay(10000);
}

// ==================== WiFi Connection ====================
void connectWiFi() {
  const unsigned long timeout = 15000;
  unsigned long start = millis();

  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi");

  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    display.print(".");
    display.display();

    if (millis() - start > timeout) {
      Serial.println("\nConnection timeout, retrying...");
      display.println("\nRetrying...");
      display.display();
      WiFi.disconnect(true);
      delay(1000);
      WiFi.begin(ssid, password);
      start = millis();
    }
    delay(500);
  }

  Serial.println("\nWiFi Connected");
  display.println("\nWiFi OK!");
  display.display();
  delay(1000);
}

// ==================== Time Synchronization ====================
void initTime() {
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
 
  struct tm timeinfo;
  int retry = 0;
  while(!getLocalTime(&timeinfo) && retry < 10) {
    Serial.println("Failed to get time, retrying...");
    delay(1000);
    retry++;
  }
 
  if(retry < 10) {
    Serial.println("Time synchronized successfully");
  } else {
    Serial.println("Time synchronization failed");
  }
}

// ==================== Fetch CNN Fear & Greed Index ====================
FearGreedData fetchCnnFearGreedIndex() {
  FearGreedData data;
  data.isValid = false;
 
  if(WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi not connected");
    return data;
  }
 
  // ---------- Get yesterday's date ----------
  time_t now = time(nullptr);
  struct tm timeinfo;
  localtime_r(&now, &timeinfo);
  timeinfo.tm_mday -= 1;           // Yesterday
  mktime(&timeinfo);
  char dateStr[11];
  strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", &timeinfo);
 
  String url = "https://production.dataviz.cnn.io/index/fearandgreed/graphdata/" + String(dateStr);
  Serial.print("Request URL: ");
  Serial.println(url);
 
  HTTPClient http;
  http.begin(url);
 
  // Critical: Complete browser request headers
  http.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36");
  http.addHeader("Accept", "application/json, text/plain, */*");
  http.addHeader("Accept-Language", "en-US,en;q=0.9");
  http.addHeader("Referer", "https://www.cnn.com/markets/fear-and-greed");  // Simulate coming from official site
  http.addHeader("Origin", "https://www.cnn.com");
  http.addHeader("Connection", "keep-alive");
 
  http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
  http.setTimeout(10000);
 
  Serial.println("Requesting CNN Fear & Greed Index...");
  int httpCode = http.GET();
 
  if(httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    Serial.println("CNN API response successful, parsing...");
   
    // Memory allocation: conservative 16KB
    DynamicJsonDocument doc(16384);
    DeserializationError error = deserializeJson(doc, payload);
   
    if(!error) {
      // Parse fear_and_greed node
      if(doc.containsKey("fear_and_greed")) {
        JsonObject fg = doc["fear_and_greed"];
       
        data.value = fg["score"].as<float>();
        data.classification = fg["rating"].as<String>();
       
        if(fg.containsKey("timestamp")) {
          data.timestamp = fg["timestamp"].as<String>();
        }
       
        data.isValid = true;
       
        Serial.print("✓ CNN Index Success! Score: ");
        Serial.print(data.value);
        Serial.print(" | Rating: ");
        Serial.println(data.classification);
      } else {
        Serial.println("'fear_and_greed' node not found in JSON");
      }
    } else {
      Serial.print("JSON parsing failed: ");
      Serial.println(error.c_str());
    }
  } else {
    Serial.print("CNN API request failed, HTTP code: ");
    Serial.println(httpCode);
    if(httpCode == 418) {
      Serial.println("Server returned 418 I'm a teapot - IP may be temporarily blocked");
    }
  }
 
  http.end();
  return data;
}

// ==================== Get Status Symbol (ASCII alternative to emoji) ====================
String getClassificationSymbol(float value) {
  if(value <= 25) return "!!!";     // Extreme Fear
  if(value <= 45) return ":-(";     // Fear
  if(value <= 55) return ":-|";     // Neutral
  if(value <= 75) return ":-)";     // Greed
  return ":-D";                      // Extreme Greed
}

// ==================== Get Status Text ====================
String getClassificationText(float value) {
  if(value <= 25) return "Extreme Fear";
  if(value <= 45) return "Fear";
  if(value <= 55) return "Neutral";
  if(value <= 75) return "Greed";
  return "Extreme Greed";
}

// ==================== Draw Progress Bar ====================
void drawProgressBar(int x, int y, int width, int height, float value, float minVal, float maxVal) {
  // Calculate fill width
  int fillWidth = map((int)value, (int)minVal, (int)maxVal, 0, width);
  fillWidth = constrain(fillWidth, 0, width);
 
  // Draw background frame
  display.drawRect(x, y, width, height, SSD1306_WHITE);
 
  // Choose fill style based on value
  if(value <= 25) {
    // Extreme Fear - sparse dots
    for(int i = 0; i < fillWidth; i += 2) {
      for(int j = 0; j < height; j += 2) {
        display.drawPixel(x + i, y + j, SSD1306_WHITE);
      }
    }
  } else if(value <= 45) {
    // Fear - dense dots
    for(int i = 0; i < fillWidth; i++) {
      for(int j = 0; j < height; j += 2) {
        display.drawPixel(x + i, y + j, SSD1306_WHITE);
      }
    }
  } else {
    // Neutral/Greed/Extreme Greed - solid fill
    display.fillRect(x, y, fillWidth, height, SSD1306_WHITE);
  }
}

// ==================== Display Fear & Greed Index ====================
void displayFearGreedIndex(const FearGreedData& data, int hour, int minute) {
  display.clearDisplay();
 
  // ========== Display time (large font) ==========
  display.setTextSize(2);
  display.setCursor(0, 0);
  char timeStr[6];
  sprintf(timeStr, "%02d:%02d", hour, minute);
  display.println(timeStr);
 
  // ========== Separator line ==========
  display.setTextSize(1);
  display.drawLine(0, 16, SCREEN_WIDTH, 16, SSD1306_WHITE);
 
  if(data.isValid) {
    // ========== Display index value ==========
    display.setTextSize(2);
    display.setCursor(0, 20);
    display.print((int)data.value);
    display.setTextSize(1);
    display.print(" pts");
   
    // ========== Display status symbol ==========
    display.setCursor(SCREEN_WIDTH - 20, 22);
    display.setTextSize(1);
    display.print(getClassificationSymbol(data.value));
   
    // ========== Display classification text ==========
    display.setTextSize(1);
    display.setCursor(0, 38);
    display.print(getClassificationText(data.value));
   
    // ========== Display data source ==========
    display.setCursor(SCREEN_WIDTH - 18, 38);
    display.setTextSize(0);
    display.print("CNN");
   
    // ========== Draw progress bar ==========
    drawProgressBar(0, 48, SCREEN_WIDTH, 8, data.value, 0, 100);
   
    // ========== Progress bar labels ==========
    display.setTextSize(0);
    display.setCursor(0, 57);
    display.print("0");
    display.setCursor(SCREEN_WIDTH - 18, 57);
    display.print("100");
    display.setCursor(SCREEN_WIDTH/2 - 6, 57);
    display.print("50");
   
  } else {
    // ========== Display error message ==========
    display.setTextSize(1);
    display.setCursor(0, 25);
    display.println("Unable to get data");
    display.setCursor(0, 35);
    display.println("Check network");
  }
 
  display.display();
}