A standalone daily XKCD comic reader built on CircuitPython 10. The device connects to WiFi once per day, fetches the latest comic, dithers it to 1-bit, and renders it on a 7.5" black/white/red e-paper display. A physical button wakes the device from deep sleep to show the comic's alt-text. Everything runs on a Li-Po cell.
![Hardware: XIAO ESP32-S3 + Waveshare 7.5" e-paper display]
| Component | Detail |
|---|---|
| MCU | Seeed XIAO ESP32-S3 — 512 KB SRAM + 8 MB PSRAM, 8 MB Flash, Wi-Fi |
| Display | DEPG0750RNU790F30HP — 7.5", 800×480, Black/White/Red, SPI (UC8179) |
| Button | Momentary push-button |
| Power | Single Li-Po cell via XIAO onboard USB-C charger (BQ25101) |
| E-Paper Pin | XIAO ESP32-S3 Pin |
|---|---|
| VCC | 3V3 |
| GND | GND |
| DIN (MOSI) | D10 / GPIO9 |
| CLK (SCK) | D8 / GPIO7 |
| CS | D3 / GPIO4 |
| DC | D2 / GPIO3 |
| RST | D1 / GPIO2 |
| BUSY | D0 / GPIO1 |
| Button | XIAO ESP32-S3 Pin |
|---|---|
| One leg | D5 / GPIO6 |
| Other leg | GND |
GPIO6 is configured with an internal pull-up; pressing the button pulls it LOW.
Boot / wake from deep sleep
│
├─ Button press (PinAlarm)
│ Load alt-text from /last_alt.txt
│ Render text → 1-bit bitmap
│ Display on e-paper
│ Deep sleep again
│
└─ Scheduled wake (TimeAlarm) or first boot
Read last comic number from /last_comic.txt
Connect to WiFi
GET xkcd.com/info.0.json
If new comic:
Download PNG
Decode → grayscale → scale → Floyd-Steinberg dither
Display 1-bit bitmap on e-paper
Save comic number + alt-text to flash
Disconnect WiFi
Deep sleep for 24 h (or until button press)
The device spends almost all its time in deep sleep, drawing ~1 µA from the display and the ESP32-S3's deep sleep current.
CIRCUITPY/
├── code.py # Main entry point
├── settings.toml # WiFi credentials
├── epaper.py # UC8179 SPI driver
├── image_utils.py # PNG download, decode, scale, dither
├── text_render.py # Alt-text → 1-bit bitmap renderer
├── fonts/
│ └── LeagueSpartan-Bold-16.bdf # Bitmap font for alt-text
└── lib/
├── adafruit_requests.mpy
├── adafruit_connection_manager.mpy
├── adafruit_pngdecoder.mpy
└── adafruit_bitmap_font/
Download the CircuitPython 10.x UF2 for the XIAO ESP32-S3 from circuitpython.org and flash it to the board.
Copy the following from the Adafruit CircuitPython 10.x bundle into the lib/ folder on your CIRCUITPY drive:
adafruit_requests.mpyadafruit_connection_manager.mpyadafruit_pngdecoder.mpyadafruit_bitmap_font/(directory)
Copy a .bdf bitmap font into fonts/ on the CIRCUITPY drive. The default expected path is fonts/LeagueSpartan-Bold-16.bdf. Any Adafruit-bundled .bdf font works — update FONT_PATH in text_render.py if you use a different file name.
Edit settings.toml and fill in your credentials:
CIRCUITPY_WIFI_SSID = "your_ssid"
CIRCUITPY_WIFI_PASSWORD = "your_password"Copy code.py, epaper.py, image_utils.py, and text_render.py to the root of the CIRCUITPY drive.
Open a serial console at 115 200 baud and watch the output. On first boot the device will connect to WiFi, fetch the latest comic, and display it. The first BWR refresh takes ~15 seconds.
To test the sleep/wake cycle quickly, set SLEEP_SECONDS = 30 in code.py, then restore it to 86400 for normal use.
- PNG downloaded in chunks into a
bytearray(8 MB PSRAM gives ample headroom) - Decoded row-by-row with
adafruit_pngdecoder - Converted to 8-bit grayscale:
g = (77·R + 150·G + 29·B) >> 8 - Nearest-neighbor scaled to 800×480 with white letterbox padding
- Floyd-Steinberg dithered to 1-bit
- Packed MSB-first into a 48 000-byte buffer and sent to the display
The red plane is left empty — XKCD comics are black and white.
The XIAO ESP32-S3 includes a BQ25101 Li-Po charger accessible via its USB-C port. Deep sleep current is dominated by the display (~1 µA image retention) and the ESP32-S3 deep sleep mode (~20 µA). A 1 000 mAh cell should last several weeks between charges.