From 669f3bf7b63a50d09b4004f405697eb6d227c40f Mon Sep 17 00:00:00 2001 From: uvok Date: Sun, 10 Aug 2025 18:31:06 +0200 Subject: Publish! --- _posts/2025-08-10-building-an-epaper-badge.md | 1216 +++++++++++++++++++++++++ 1 file changed, 1216 insertions(+) create mode 100644 _posts/2025-08-10-building-an-epaper-badge.md (limited to '_posts/2025-08-10-building-an-epaper-badge.md') diff --git a/_posts/2025-08-10-building-an-epaper-badge.md b/_posts/2025-08-10-building-an-epaper-badge.md new file mode 100644 index 0000000..4567787 --- /dev/null +++ b/_posts/2025-08-10-building-an-epaper-badge.md @@ -0,0 +1,1216 @@ +--- +layout: post +title: Building an ePaper badge +lang: en +categories: tech +date: 2025-08-10 18:30 +0200 +--- +## 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: + +a dithered image of my fursona + +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. + +Screenshot of AppInventor showing part of the program. + +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: + +Screenshot of the Flutter app, allowing selection from
+  sleepy, hungry, hugs?, uvok, overstim, contact, games? + +## 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/) + + -- cgit v1.2.3