/*
ESP32-C3 Super Mini + 2.13" E-Paper (250x122, SSD1680) — 10-in-1 Project Firmware
Arvind Patil / AI Centre Nandurbar
Library: GxEPD2 (Jean-Marc Zingg) -> Install "GxEPD2" via Library Manager
Display: GDEY0213B74 / DEPG0213BN (SSD1680, 250x122, B/W)
Wiring (ESP32-C3 Super Mini):
EPD CS -> GPIO 7
EPD DC -> GPIO 2
EPD RST -> GPIO 3
EPD BUSY -> GPIO 5
EPD CLK -> GPIO 4 (SPI SCK, custom via SPI.begin)
EPD DIN -> GPIO 6 (SPI MOSI, custom via SPI.begin)
EPD GND -> GND
EPD VCC -> 3V3
Note: C3 Super Mini has no free hardware MISO pin needed for e-paper
(write-only display), so SPI.begin(SCK, -1, MOSI, CS) is used.
BOOT button (onboard, GPIO9): short press = next mode (partial refresh cycle)
long press (>800ms) = force full refresh
GPIO9 is a strapping pin — only read after boot completes, never driven low
externally at power-on, which is safe here since it is only used as an input.
Modes: 0 Weather 1 Clock 2 To-Do 3 Quote 4 Timetable
5 Price Tag 6 Plant Monitor 7 Business Card 8 Ticker 9 System
*/
#include <SPI.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMono9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <WiFi.h>
#include <time.h>
#define CS_PIN 7
#define DC_PIN 2
#define RST_PIN 3
#define BUSY_PIN 5
#define SCK_PIN 4
#define MOSI_PIN 6
#define BOOT_BTN 9
GxEPD2_BW<GxEPD2_213_B74, GxEPD2_213_B74::HEIGHT> display(
GxEPD2_213_B74(CS_PIN, DC_PIN, RST_PIN, BUSY_PIN));
const char* WIFI_SSID = "Airtel_arvi_6035";
const char* WIFI_PASS = "air26289";
const long GMT_OFFSET_SEC = 5.5 * 3600; // IST
int mode = 0;
const int MODE_COUNT = 10;
unsigned long lastPress = 0;
unsigned long pressStart = 0;
bool btnDown = false;
void setup() {
Serial.begin(115200);
pinMode(BOOT_BTN, INPUT_PULLUP);
SPI.begin(SCK_PIN, -1 /* MISO unused */, MOSI_PIN, CS_PIN);
display.init(115200);
display.setRotation(1); // landscape, 250x122
display.setTextColor(GxEPD_BLACK);
WiFi.begin(WIFI_SSID, WIFI_PASS);
unsigned long t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 8000) { delay(200); }
if (WiFi.status() == WL_CONNECTED) {
configTime(GMT_OFFSET_SEC, 0, "pool.ntp.org", "time.google.com");
}
drawMode(mode, true);
}
void loop() {
bool pressed = (digitalRead(BOOT_BTN) == LOW);
if (pressed && !btnDown) { btnDown = true; pressStart = millis(); }
if (!pressed && btnDown) {
btnDown = false;
unsigned long held = millis() - pressStart;
if (held > 800) {
drawMode(mode, true); // long press -> full refresh, same mode
} else {
mode = (mode + 1) % MODE_COUNT;
drawMode(mode, mode == 0); // full refresh only when wrapping to mode 0
}
}
delay(20);
}
// ---- helpers -------------------------------------------------
void hline(int x1, int y, int x2) { display.drawLine(x1, y, x2, y, GxEPD_BLACK); }
void drawFrame() {
display.drawRect(0, 0, display.width(), display.height(), GxEPD_BLACK);
}
void getTimeParts(char *hhmm, char *datestr) {
struct tm ti;
if (getLocalTime(&ti)) {
sprintf(hhmm, "%02d:%02d", ti.tm_hour, ti.tm_min);
strftime(datestr, 24, "%a %d %b %Y", &ti);
} else {
strcpy(hhmm, "--:--");
strcpy(datestr, "no NTP sync");
}
}
// ---- mode renderers -------------------------------------------
void modeWeather() {
display.setFont(&FreeMonoBold12pt7b);
display.setCursor(6, 20); display.print("NANDURBAR");
display.setFont(&FreeMono9pt7b);
display.setCursor(6, 34); display.print("26 Aug 2026");
hline(4, 40, 246);
display.drawCircle(35, 68, 14, GxEPD_BLACK);
display.setFont(&FreeMonoBold12pt7b);
display.setCursor(70, 68); display.print("34C");
display.setFont(&FreeMono9pt7b);
display.setCursor(70, 84); display.print("Clear Sky");
hline(4, 96, 246);
display.setCursor(6, 112); display.print("Hum 41% Wind 9km/h");
}
void modeClock() {
char hhmm[8], datestr[24];
getTimeParts(hhmm, datestr);
display.setFont(&FreeMonoBold12pt7b);
int16_t x1,y1; uint16_t w,h;
display.getTextBounds(hhmm, 0,0,&x1,&y1,&w,&h);
display.setCursor((display.width()-w)/2, 55); display.print(hhmm);
display.setFont(&FreeMono9pt7b);
display.getTextBounds(datestr, 0,0,&x1,&y1,&w,&h);
display.setCursor((display.width()-w)/2, 78); display.print(datestr);
hline(4, 100, 246);
display.setCursor(6, 116); display.print("IST NTP synced");
}
void modeTodo() {
display.setFont(&FreeMonoBold9pt7b);
display.setCursor(6, 16); display.print("TODAY");
hline(4, 22, 246);
display.setFont(&FreeMono9pt7b);
const char* items[] = {"[x] Print worksheets", "[ ] ESP32 lab setup",
"[ ] Call parent-fees", "[ ] Update blog"};
for (int i = 0; i < 4; i++) { display.setCursor(8, 40 + i*18); display.print(items[i]); }
}
void modeQuote() {
display.setFont(&FreeMono9pt7b);
const char* l1 = "Small daily improvements";
const char* l2 = "compound into staggering";
const char* l3 = "results. -- James Clear";
display.setCursor(10, 40); display.print(l1);
display.setCursor(10, 60); display.print(l2);
display.setCursor(10, 80); display.print(l3);
hline(4, 100, 246);
display.setCursor(6, 116); display.print("AI Centre Nandurbar");
}
void modeTimetable() {
display.setFont(&FreeMonoBold9pt7b);
display.setCursor(6, 16); display.print("BUS STAND");
hline(4, 20, 246);
display.setFont(&FreeMono9pt7b);
display.setCursor(6, 36); display.print("17:05 Dhule OnTime");
display.setCursor(6, 54); display.print("17:40 Shirpur Delay10");
display.setCursor(6, 72); display.print("18:15 Jalgaon OnTime");
hline(4, 92, 246);
display.setCursor(6, 112); display.print("Updated 14:22");
}
void modePriceTag() {
display.setFont(&FreeMonoBold9pt7b);
display.setCursor(6, 18); display.print("Basmati Rice 5kg");
hline(4, 24, 246);
display.setFont(&FreeMonoBold12pt7b);
display.setCursor(20, 78); display.print("Rs 449");
display.setFont(&FreeMono9pt7b);
display.setCursor(150, 55); display.print("MRP 520");
display.setCursor(150, 72); display.print("Save 14%");
hline(4, 96, 246);
display.setCursor(6, 116); display.print("SKU RC-5049");
}
void modePlant(float moisture, float lux, float temp, float hum) {
display.setFont(&FreeMonoBold9pt7b);
display.setCursor(6, 16); display.print("TULSI - POT 2");
hline(4, 22, 246);
display.setFont(&FreeMono9pt7b);
display.setCursor(6, 40); display.printf("Moisture %.0f%%", moisture);
display.drawRect(150, 30, 80, 12, GxEPD_BLACK);
display.fillRect(150, 30, (int)(80*moisture/100.0), 12, GxEPD_BLACK);
display.setCursor(6, 62); display.printf("Temp %.1fC Hum %.0f%%", temp, hum);
hline(4, 92, 246);
display.setCursor(6, 112); display.print(moisture > 40 ? "Status: Healthy" : "Status: Water soon");
}
void modeCard() {
display.setFont(&FreeMonoBold12pt7b);
display.setCursor(6, 24); display.print("ARVIND PATIL");
display.setFont(&FreeMono9pt7b);
display.setCursor(6, 40); display.print("Founder, AI Centre Nandurbar");
hline(4, 48, 246);
display.setCursor(6, 64); display.print("AI Literacy / EdTech / ESP32");
display.setCursor(6, 78); display.print("Nandurbar, Maharashtra");
display.drawRect(190, 58, 50, 50, GxEPD_BLACK);
}
void modeTicker() {
display.setFont(&FreeMonoBold9pt7b);
display.setCursor(6, 16); display.print("MARKET WATCH");
hline(4, 22, 246);
display.setFont(&FreeMono9pt7b);
display.setCursor(6, 40); display.print("NIFTY 24812 +0.6%");
display.setCursor(6, 60); display.print("SENSEX 81240 +0.4%");
display.setCursor(6, 80); display.print("BTC 61900 -1.2%");
hline(4, 92, 246);
display.setCursor(6, 112); display.print("as of 14:22 IST");
}
void modeSystem() {
display.setFont(&FreeMonoBold9pt7b);
display.setCursor(6, 16); display.print("SYSTEM STATUS");
hline(4, 22, 246);
display.setFont(&FreeMono9pt7b);
display.setCursor(6, 38); display.printf("WiFi RSSI %ddBm", WiFi.RSSI());
display.setCursor(6, 54); display.printf("Uptime %lus", millis()/1000);
display.setCursor(6, 70); display.printf("Free Heap %uKB", ESP.getFreeHeap()/1024);
hline(4, 90, 246);
display.setCursor(6, 112); display.print("Deep-sleep next: 30s");
}
// ---- dispatcher -------------------------------------------------
void drawMode(int m, bool full) {
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
switch (m) {
case 0: modeWeather(); break;
case 1: modeClock(); break;
case 2: modeTodo(); break;
case 3: modeQuote(); break;
case 4: modeTimetable(); break;
case 5: modePriceTag(); break;
case 6: modePlant(62.0, 780.0, 29.4, 55.0); break;
case 7: modeCard(); break;
case 8: modeTicker(); break;
case 9: modeSystem(); break;
}
drawFrame();
} while (display.nextPage());
// Note: GxEPD2 handles partial vs full internally per-driver;
// for true partial refresh use display.setPartialWindow(x,y,w,h)
// around the specific region that changed (e.g. clock digits).
}