---
layout: post
title: Building an ePaper badge
date: 2025-08-06 19:53 +0200
lang: en
categories: tech
---
## Foreword
Two weeks before [Awoostria]({% post_url 2025-07-30-awoostria-con-report %}):
> Hey, I should build something for my Tinkering Projects Show And Tell panel!
So it begins… The story how I built myself an ePaper badge.
Actually, the story begins way earlier (unrelated, when I still had a physical
Raspberry Pi running stuff in my home network). I wanted to tinker around a bit
and bought myself a Waveshare ePaper. These are simple black-and-white displays
which maintain their content when the power switches off. They are also inside
eBook readers.
This was also when I wanted to build myself an electronic door sign for the
EAST convention with these, and I wanted to go "as minimal as possible". I
wanted to use one of the MSP430 controllers I had laying around, and I wanted
to change motives via MiFare RFID transponders (using an MFRC5xx reader). Work
on that development never really took off. (Ugh, too little flash for all the
pictures, too much stuff to code myself!)
## Requirements
So this time, I simply said "fuck it", and threw an ESP32 on the problem.
Also, I decided to use [PlatformIO](https://platformio.org/), a
toolchain/SDK/library manager. I started with the Arduino framework, which is…
pretty wasteful in terms of resources (Flash, RAM, CPU etc.), but speeds up
development significantly.
I had a simple ESP32 devboard, and one of the Waveshare modules, and started
coding.
…
But wait, what do I even want to achieve? Well, I wanted to "mood badge", i.e.
show my current mood with funny pictures. I couldn't get one on previous
conventions, so I was just gonna build one myself.
This involves several sub-problems:
* [Control the ePaper display](#control)
* [Get the pictures on the display](#pic)
* [Set what is displayed](#setdisp)
* [Power-saving](#powersave)
* [Attach the badge to myself](#attach)
## Control the display
Usually, you never talk to the displays themselves, but to a display
controller. You talk to these via a digital interface, e.g. SPI. There are
different display controllers with different command sets.
But why bother with implementing this myself? There are ready-made libraries.
For myself, I decided to use [GxEPD2](https://github.com/ZinggJM/GxEPD2). They
support *some* Waveshare displays. The problem with Waveshare displays is, they
don't disclose which display controller they use. So it's kind of an
trial-or-error procedure. Or rather, you can look at their example code, figure
out which commands they are using, and compare what commands GxEPD2 uses.
That's a bit cumbersome. But still better than writing everything myself. Also,
it supports graphics primitives!
## Get the picture on the display
You can't just simply throw a JPEG onto the display. The display doesn't
understand that. It only understands pixel data. Also, the display can only
draw black and white pixels. I also have a display with yellow color support,
but that makes it even more complicated, actually. Even when you don't use it,
refresh is slow.
So, you definitely can't throw a color picture on the display, nor a monochrome
one. There are displays which support a few gray-levels, but I don't have one
of those.
So. What to? The solution is "dithering". I.e. you trick your eye into
perceiving grey by having clusters of black and white pixels. There is some
technical background to dithering (see the Reference section), but I simply
used either GIMP with the Floyd-something algorithm, or one of the "ordered"
modes of ImageMagick. It was a bit of trial-and-error and
seeing-what-looks-best.
The result, then, looks like this:
Now, about the image format… GxEPD2 supports "XBitmaps", or XBMs, which are
basically just a C array declaration, so you can GCC that file and throw in the
array into the GxEPD2 function call. And voilá, it works. You need to set the
rotation first, though.
## How to attach the badge to myself
I have a Waveshare module/PiHat (which is too heavy), and a simple "ePaper
sheet" including a plastic housing for it. The housing can only fit the ePaper,
not the devboard, though. Also, it would be too cumbersome to attach to the
devboard - loose wires! So, at this point, I decided to switch from the
prototyping platform onto something better.
Fortunately, Elecrow provides a
[CrowPanel](https://www.elecrow.com/wiki/CrowPanel_ESP32_E-paper_2.9-inch_HMI_Display.html),
which is exactly what I need. It has a display, a built-in ESP32 controller, a
housing, and even some switches! As a huge plus, they even specify which
display controller they use. I had to try some of the GxEPD2 display classes,
but finally found one working.
I decided to glue magnets onto the housing, and attach the display via magnets
on the inner side of my shirt — not ideal. I positioned the magnets in
the (vertical) middle of the housing, so it wobbles and is not readable. Also,
I accidentally washed the shirt after Awoostria with the magnets still sticking
inside — and now there's a hole in it :( . This problem is still
unsolved. I can kinda attach the magnets to the housing screws at the top, but
that's not *very* stable.
When starting to work with the Elecrow display, at first nothing would work.
When I looked at their example code I noticed there's an additional power pin
that needs to be toggled.
## Set what is displayed
In addition to these switches, which allow choosing the motive, I wanted
something "more direct", so I added the
[NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) library. With a bit
of coding, I added a service and some characteristics, so the available motives
could be read via BLE. Also, the motive could be selected via another
characteristic. I started writing the characteristic with *nRF Connect For
Mobile*, but started writing an app [later](#theapp).
Actually, the switch-selection was a bit troublesome. Redrawing the whole
display takes around 2 seconds — but that is not acceptable when
navigating the presets one-by-one. By looking at the API, I found out you can
select a "partial region".
What I didn't mention yet, the text that shows the mood is drawn at runtime,
not integrated into the picture. So, I simply update a region in the
vertical-center-right of the display with the mood text. The picture stays the
same, but the text reflects the selected mood. The selection is confirmed my
pressing the rotary switch.
This is still not ideal. The first update after power-up must be a full one,
and I don't save the selected preset in NVS — I don't want to destroy the
flash by lots of write cycles. I don't have a solution to this, yet. Maybe I'm
gonna integrate an microSD card (there's a slot for that in the CrowPanel).
I'm gonna research a good wear-levelling file system for that. Probably not
FAT. That doesn't need to be readable on the PC. (And even if, simple stuff
can be written in FUSE). Alternatively, drawing the first motive on power-up
would also be an option, but I don't like it that much.
## Power saving
I first measured the current and was shocked. The whole thing draws around
80-100 mA. Not a big surprise, given that Arduino basically calls `loop()` over
and over again.
Using a bit of experimenting, and failing to cancel light sleep with a GPIO
interrupt, I implemented power saving by using a timer-based light sleep (10
ms, gives good user response) and reducing the CPU frequency to 80 MHz. And lo
and behold, my USB current measuring equipment (resolution 10mA) showed 0 mA.
Success.
Actually, reducing the CPU frequency was a requirement! If I didn't do that,
the CPU would constantly crash when entering or exiting light sleep. No idea
why!
Apropos of powering: The badge is normally unpowered and only powered if I need
to change the motive. I don't want to have an USB cable hanging on me the whole
time! Using a battery might be an option, but I had problems with mismatched
connectors — the Elecrow display doesn't have a standard JST connector
like the LiPo battery I bought (Li-Ion might even be the safer option? I have
no clue about this stuff. With a quick search, I only found these cylindrical
Li-Ion batteries and have no idea how I would connect them). The badge seems to
have a "mini" variant of that connector.
For the time being, I power it with a USB power bank, but this I still consider
an unsolved problem.
## The app
Writing the BLE characteristics with nRF Connect is all and well, but not
really user-friendly. I didn't want to install the Android SDK, so I looked at
cloud based development for a start. I found [MIT
AppInventor](https://appinventor.mit.edu/). First, I was disgusted, because
apparently they require Login with Google. But I found [an alternative
way](https://code2.appinventor.mit.edu/) by which you simply get a
"Passphrase-like" codeword you use for login.
The graphical programming is unusual to me. I used Scratch shortly in the past,
so it was not completely foreign. Actually, it was kinda fun coding this, in
"event style", once I figured out how to to like stripping, list filtering,
etc.
This was good enough for a while, but then I decided I wanted to actually have
the source code available. So I looked again at development options, and
settled for Flutter.
Again, this was completely new to me. I started off with a popular BLE library,
which turned out to be an unfortunate choice, as Linux support had a few
quirks. That was probably a good thing in hindsight, as this made me abstract
away the BLE stuff in implementation classes, so I could easily try out
different libraries, and only use the abstract base classes in the code. Well,
you can see what the code looks like, I linked my repo below. This is what the
motive selection looks like:
## Resources
- [surma.dev about dithering](https://surma.dev/things/ditherpunk/)
- [git repo with badge source code](https://git.uvok.de/espadge/)
- [git repo with app source code](https://git.uvok.de/espadge-flutter/)