I have a confession: I own four smart displays, and every single one of them is plugged into a wall outlet, humming quietly, showing me the weather 24/7 whether I'm looking at it or not. That always bothered me — burning constant power to display information that only changes a few times a day. So I built something different: an e-ink dashboard that refreshes once every 15 minutes, sips power only during that refresh, and sleeps the rest of the time. Three months in, it's still running on its original battery charge.
This guide walks through the full build: wiring an e-ink display to an ESP32, pulling live data from a weather API, structuring the code around deep sleep so the battery actually lasts, and a few layout tricks that make the display genuinely pleasant to glance at instead of looking like a debug console.
📑 Table of Contents
-
- Why E-Ink (And Why It's Having a Moment)
-
- Components & Cost Breakdown
-
- Circuit Wiring Diagram
-
- Understanding E-Ink Refresh Behavior
-
- Getting Weather Data
-
- Complete Code
-
- The Deep Sleep Power Strategy
-
- Calibration Tips
-
- Troubleshooting
-
- Taking It Further
1. Why E-Ink (And Why It's Having a Moment)
E-ink panels have quietly gone from "that thing on my Kindle" to one of the most popular materials in the DIY electronics scene over the past couple of years. Two things drove that: panel prices dropped enough that a decent-sized display costs less than a fast-food meal, and the maker community discovered that e-ink's biggest "limitation" — it can't refresh quickly — is actually a feature for dashboards that don't need to update every second anyway.
⚡ The core appeal: E-ink only draws power while the image is changing. Once it's drawn, the display holds that image with zero power draw — even if you unplug it entirely, the image stays. Pair that with an ESP32 that spends 99% of its life in deep sleep, and you get a device that sips a fraction of what an always-on LCD panel burns, with a screen that's genuinely easier on the eyes because it reflects ambient light instead of emitting it.
2. Components & Cost Breakdown
| Component | Model | Purpose | Price |
|---|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit | WiFi + processing | $6 |
| Display | 4.2" e-ink panel (GDEW042T2, 400x300) | Main display | $22 |
| Battery | 3.7V 2000mAh LiPo | Power source | $8 |
| Charge circuit | TP4056 USB-C charging module | Safe LiPo charging | $2 |
| Enclosure | 3D-printed or picture frame | Housing | $5 |
| Misc | Jumper wires, JST connector | Assembly | $2 |
💰 Total Cost: ~$45 (one-time), compared to $60-150 for commercial e-ink smart displays that often require a subscription for weather data.
3. Circuit Wiring Diagram
E-ink panels communicate over SPI, so wiring is straightforward but pin-count-heavy compared to something like an I2C sensor.
Display to ESP32:
- BUSY → GPIO4
- RST → GPIO16
- DC → GPIO17
- CS → GPIO5
- SCK → GPIO18 (SPI clock)
- DIN → GPIO23 (SPI MOSI)
- GND → GND
- VCC → 3.3V
Battery & Charging:
- LiPo battery → TP4056 B+/B− terminals
- TP4056 OUT+ → ESP32 VIN (through a switch if you want a physical power toggle)
- TP4056 OUT− → ESP32 GND
- USB-C on TP4056 → charging port, accessible from outside the enclosure
⚠️ Important: Don't power the e-ink panel's VCC directly from a GPIO pin expecting it to double as an enable line unless your specific panel's datasheet confirms it's designed for that — most 4.2" panels want a clean, continuous 3.3V and use the RST/DC/CS/BUSY lines for control instead.
4. Understanding E-Ink Refresh Behavior
This is the part that trips up almost everyone building their first e-ink project, so it's worth slowing down on.
E-ink pixels are physically flipped by an electric field — there's no backlight, no constant refresh like an LCD. A "full refresh" briefly flashes the whole screen black-then-white-then-black a few times before settling on the new image; this clears any "ghosting" from the previous image but takes 1-2 seconds and is visually jarring if you do it constantly. A "partial refresh" updates just the pixels that changed, is much faster and quieter, but accumulates faint ghosting over many updates.
The practical rule most e-ink projects settle on: do a partial refresh most of the time, and force a full refresh once every 10-20 updates (or once a day) to clear accumulated ghosting. The code below implements exactly this pattern.
5. Getting Weather Data
Rather than scraping a website (fragile, and often against terms of service), use a free weather API with a generous free tier:
- Sign up for a free account at openweathermap.org and grab an API key.
- The One Call API endpoint gives you current conditions plus a short forecast in a single request — exactly what fits on a small e-ink panel.
- Test the endpoint in a browser first before wiring it into code, so you know what the JSON response actually looks like for your location.
6. Complete Code
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <GxEPD2_BW.h>
#include <esp_sleep.h>
#define BUSY_PIN 4
#define RST_PIN 16
#define DC_PIN 17
#define CS_PIN 5
GxEPD2_BW<GxEPD2_420, GxEPD2_420::HEIGHT> display(
GxEPD2_420(CS_PIN, DC_PIN, RST_PIN, BUSY_PIN));
const char* ssid = "YOUR_WIFI";
const char* password = "YOUR_PASSWORD";
const char* apiKey = "YOUR_OPENWEATHERMAP_KEY";
const char* city = "YOUR_CITY";
RTC_DATA_ATTR int refreshCount = 0; // survives deep sleep, resets on power loss
void fetchAndDrawWeather() {
HTTPClient http;
String url = "http://api.openweathermap.org/data/2.5/weather?q=" +
String(city) + "&appid=" + String(apiKey) + "&units=metric";
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
JsonDocument doc;
deserializeJson(doc, payload);
float temp = doc["main"]["temp"];
int humidity = doc["main"]["humidity"];
const char* condition = doc["weather"][0]["main"];
display.setRotation(1);
display.setTextColor(GxEPD_BLACK);
// Force a full refresh every 20 updates to clear ghosting
bool fullRefresh = (refreshCount % 20 == 0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.setFont(&FreeSansBold24pt7b);
display.setCursor(20, 80);
display.print(temp, 1);
display.print(" C");
display.setFont(&FreeSans12pt7b);
display.setCursor(20, 130);
display.print(condition);
display.setCursor(20, 160);
display.print("Humidity: ");
display.print(humidity);
display.print("%");
} while (display.nextPage());
refreshCount++;
}
http.end();
}
void setup() {
Serial.begin(115200);
display.init();
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
fetchAndDrawWeather();
}
WiFi.disconnect(true);
display.hibernate(); // puts the e-ink driver into its lowest-power state
// Sleep for 15 minutes, then wake and repeat
esp_sleep_enable_timer_wakeup(15ULL * 60ULL * 1000000ULL);
esp_deep_sleep_start();
}
void loop() {
// never reached — setup() runs once per wake cycle, then back to sleep
}
Note on the display library: this example uses GxEPD2, the most widely used Arduino library for SPI e-ink panels, since it supports the vast majority of common panel sizes with fairly consistent syntax. Check the library's panel list against your exact model before wiring, since pin requirements shift slightly between panel generations.
7. The Deep Sleep Power Strategy
The setup()-only structure above isn't an accident — it's the entire point. Here's the power math that makes this project worth building:
- Active time per cycle: roughly 3-5 seconds (WiFi connect + API call + display refresh)
- Sleep time per cycle: 15 minutes (900 seconds)
- Active duty cycle: under 0.6% of total runtime
During deep sleep, the ESP32 draws roughly 10-150 microamps depending on the board (cheap dev boards with onboard voltage regulators and LEDs often draw more than you'd expect — this is the single biggest lever for extending battery life, more on that below). During the brief active window, it draws 80-200mA. Averaged out, a well-optimized setup can run for months on a 2000mAh battery, where the same ESP32 running continuously would drain that battery in under a day.
8. Calibration Tips
- Cut the power LED. Most ESP32 dev boards have an always-on power indicator LED that draws a surprising amount of current relative to deep-sleep draw. Desoldering it (or scratching the trace) on a board dedicated to this project measurably extends battery life.
- Adjust the refresh interval to match how often you actually check the display. 15 minutes is a reasonable default for weather; a calendar dashboard might only need to refresh once an hour.
- Watch your city name formatting in the API request — ambiguous city names return the wrong location's weather. Use the API's geocoding endpoint to get precise latitude/longitude coordinates instead of a city string for anything beyond casual testing.
9. Troubleshooting
Display shows nothing after upload:
- Double-check BUSY/RST/DC/CS pin assignments match both your wiring and the constructor call in code — a single swapped pin here is the most common cause of a blank screen.
- Confirm you selected the correct panel model in the
GxEPD2_420(or equivalent) class name; using the wrong panel class produces silent failures rather than clear errors.
Image looks smeared or has visible ghosting:
- This is expected between full refreshes — it's the nature of partial e-ink updates. If it's excessive, decrease the
refreshCount % 20interval to force full refreshes more often.
Battery drains faster than expected:
- Measure deep-sleep current directly with a multimeter in series with the battery. If it's above ~1mA, check for an always-on LED, or a voltage regulator on your specific dev board that doesn't fully power down in deep sleep — some boards need pins pulled to specific states before sleeping to avoid leakage current through unused peripherals.
WiFi connection fails intermittently:
- Weak signal at the display's mounting location is the usual culprit. Since the ESP32 only needs a WiFi connection for a few seconds every 15 minutes, even a marginal connection that occasionally times out is usually fine — just make sure the
whileloop insetup()has a reasonable attempt limit so a failed connection doesn't keep the radio active (and draining battery) indefinitely.
10. Taking It Further
Once the weather dashboard is solid, the same deep-sleep-plus-e-ink pattern extends easily: pull calendar events from a Google Calendar API instead of weather, display today's train departure times, or build a "family dashboard" that rotates between a few different data sources on alternating wake cycles. The hardware doesn't change — only what you're fetching and drawing.
Final Thoughts
This project ended up teaching me more about power optimization than any other build on this blog, mostly because e-ink makes the payoff so visible — you're not chasing an abstract efficiency number, you're watching a battery percentage barely move over weeks. If you're used to building things that stay plugged in, forcing yourself to think in terms of "wake, work, sleep" cycles is a genuinely useful shift, and it's one that applies to a lot of battery-powered IoT projects beyond just displays.
If you build this, I'd be curious what refresh interval you land on and how long your battery actually lasts in practice — real-world numbers vary more than datasheets suggest.
Related Projects You'll Love

