E-paper TTY for CrowPanel ESP32-S3
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