Meefik's Blog

Freedom and Open Source

E-paper TTY for CrowPanel ESP32-S3

04 Aug 2026 | diy

I got a CrowPanel ESP32-S3 — it’s a 5.79” e-paper display with the ESP32-S3 built in, USB-C powered, all in one board. The screen is big enough to be useful and small enough to sit on my desk without being obnoxious.

The obvious thing to do with an e-paper display that holds its image with zero power is write something to it and leave it alone. So I wrote papertty: firmware that listens on USB serial and renders whatever text you pipe to it. Basically a physical tty backed by paper.

The display runs at 272×792 in B&W — two SSD1683 panels stitched together via SPI. Firmware is Arduino/C++, built with PlatformIO. Nothing exotic.

Source on GitHub. There’s also a demo video if you want to see it actually working instead of trusting my word for it.

How it works

The firmware does one thing: reads bytes from USB CDC serial at 115200 baud, buffers them, and renders accumulated text onto the e-paper frame buffer. The display driver handles pushing pixels to the SSD1683 chips. Loop, wait, repeat.

Input buffering

Serial data is messy — you get partial lines, then more data, then nothing for a while. Rendering every byte as it arrives makes the screen look like it’s stuttering. So the firmware waits: it accumulates bytes and only commits a frame after 1 second of silence. If something keeps streaming in, it just buffers (up to ~8KB) until the sender catches its breath.

This also matters for e-paper specifically — each refresh takes time because you’re literally moving charged particles in microcapsules. Fewer updates means less wear on the panel over months of use. Not a huge deal short-term, but e-paper panels do degrade with constant refreshing.

Auto font scaling

Four fixed font sizes: 48px, 24px, 16px, and 12px. Before rendering, the firmware scans the buffer for the longest line and total number of lines, then picks the biggest font that fits everything on screen. No manual configuration needed — short messages render large, long ls -la output renders small.

Font Char width Max columns Max lines
48 px 24 33 5
24 px 12 66 11
16 px 8 99 17
12 px 6 132 22

The algorithm is just some division and a min() call. Simple, but I had to handle the edge cases — empty input, content that barely fits, trailing partial lines without a newline. Those took longer than the main logic honestly.

Character filtering

E-paper is just pixels — no Unicode rendering, no font fallbacks. The firmware passes printable ASCII through (0x20–0x7E plus newline) and turns everything else into spaces. If your output has UTF-8 characters, run it through iconv -f UTF-8 -t ASCII//TRANSLIT first. That’s the only limitation worth knowing about.

Using it

Configure the serial port once after flashing:

stty -F /dev/ttyUSB0 115200 raw

Then pipe anything to the display:

echo -e 'Hello\nWorld' > /dev/ttyUSB0
ls -la --color=never > /dev/ttyUSB0
cowsay "Have you mooed today?" | tee /dev/ttyUSB0
curl https://wttr.is/?1nQFT0 | iconv -f UTF-8 -t ASCII//TRANSLIT > /dev/ttyUSB0

tee instead of > if you want to see the output in your terminal too, which is usually what you want. Or just pipe an entire shell session straight through:

bash | tee /dev/ttyUSB0

AI-powered ASCII art

If you run Ollama locally, drop this alias into your ~/.bashrc:

alias papertty='jq -Rs "{
    model: \"ornith:9b\",
    think: false,
    stream: false,
    prompt: .,
    system: \"Max 11 lines, max 66 chars per line. Printable ASCII only (0x20-0x7E). Raw text — no markdown, no explanation, no Unicode or emoji.\"
}" | curl -s http://localhost:11434/api/generate -d @- | jq -r ".response" | tee /dev/ttyUSB0'

Then send anything to the display:

echo 'Draw a cat' | papertty
echo 'List my morning tasks' | papertty

It’s more fun than useful, but it works. The display doesn’t care whether the text comes from ls or an LLM — it just renders whatever arrives on serial.

Building and flashing

PlatformIO handles everything:

python3 -m venv .venv && source .venv/bin/activate && pip install platformio
pio run -t upload

The EPD driver is bundled locally from Elecrow’s repo — no library dependencies to chase. The firmware builds in a few seconds and flashes just as fast. No RTOS, no networking stack, just the display driver and about 100 lines of C++ for the main loop.

Wrap up

Small project, specific use case. I wanted a passive display for text output that doesn’t draw constant power or compete for my attention — firmware was the missing piece between the hardware and actually using it for anything useful. If you have the CrowPanel sitting around and want something simple to flash:

Stack: ESP32-S3, Arduino/C++, PlatformIO, SSD1683 e-paper driver

Comments