0

www.bentasker.co.uk · 1mo · discuss

As part of my back-away-from-burnout plan, I've been trying to re-awaken my enthusiasm for tech by finding things to tinker with. Someone recently posted in the fediverse about a small e-ink name badge that they'd had delivered. Unfortunately, the kit that they bought only seems to be available in the US, but I instead stumbled upon Badgeware's Badger. Badger is a name badge made of a 2.7" e-ink display and powered by a Raspberry Pi RP2350. I've played around with programmable wearables before, so I thought that this might be a good way to encourage some unstructured tinkering. This post talks about getting started with the Badger 2350. Connecting To A PC Badger has MicroPython installed on it and, to be programmed, can be used as USB mass storage: Plug into computer Press the Reset button twice Badger switched to mass storage mode and could now be mounted. The phrase "mass storage" might be somewhat overselling things though, because we have quite limited space to work with: $ df -h | grep BADGER /dev/sda 12M 1.7M 11M 14% /media/ben/BADGER I started by exploring the root of the storage: ben@optimus:/media/ben/BADGER$ ls -a . .. apps assets main.py secrets.py The secrets.py file contains the means to set timezone as well as to provide wifi creds: $ cat secrets.py WIFI_SSID = "" WIFI_PASSWORD = "" REGION = "eu" # Options are us, cuba, eu, moldova, lebanon, egypt, chile, australia, nz TIMEZONE = 0 # Offset from GMT as number of hours, i.e. 0, 1, -7 etc. main.py is the badge's entrypoint: $ cat main.py # This file is copied from /system/main.py to /main.py on first run import os import sys import machine from badgeware import State, fatal_error, run, file_exists def quit_to_launcher(pin): global running_app state["running"] = "/system/apps/menu" State.modify("menu", state) getattr(running_app, "on_exit", lambda: None)() # If we reset while boot is low, bad times while not pin.value(): pass machine.reset() state = { "active": 0, "running": "/system/apps/menu" } State.load("menu", state) app = state["running"] machine.Pin.board.BUTTON_HOME.irq( trigger=machine.Pin.IRQ_FALLING, handler=quit_to_launcher ) # Trying to launch an app that has been removed if not file_exists(app): state["running"] = "/system/apps/menu" State.modify("menu", state) app = state["running"] sys.path.insert(0, app) try: os.chdir(app) running_app = __import__(app) getattr(running_app, "init", lambda: None)() except Exception as e: # noqa: BLE001 fatal_error("Error!", e) run(running_app.update) machine.reset() The apps/ directory had a bunch of entries: $ ls apps/ badge clock hydrate mass_storage menu the_compendium So, even the menu is defined as an app. It iterates through the apps directory to list available apps and, if there's an icon file available, uses that: for path in sorted(os.listdir(root)): name = " ".join([capitalize(word) for word in path.split("_")]) if is_dir(f"{root}/{path}"): if file_exists(f"{root}/{path}/icon.png"): App(self.apps, name, path, image.load(f"{root}/{path}/icon.png")) The raison d'être of a Badger though, is to be a badge, so I decided to start there: $ ls badge/ assets avatar.png icon.png __init__.py The code itself lives in __init__.py and hardcodes the display values: # details to be shown on the card id_photo = image.load("avatar.png") id_name = "Your Name" id_role = "Job title" It also includes a section to set socials: # see the 'assets/social' folder to see what's supported id_socials = {"bluesky": {"icon": None, "handle": "@bluesky"}, "instagram": {"icon": None, "handle": "@instagram"}, "github": {"icon": None, "handle": "@github"}, "discord": {"icon": None, "handle": "@discord"} } There was pre-existing support for the following social networks: $ ls badge/assets/socials/ blogger.png drive.png flickr.png itch.io.png linktree.png patreon.png reddit.png steam.png tumblr.png wordpress.png bluesky.png dropbox.png github.png kofi.png mastodon.png paypal.png rss.png telegram.png twitch.png xbl.png deviantart.png etsy.png google.png lastfm.png medium.png pinterest.png soundcloud.png threads.png twitter.png x.png discord.png facebook.png instagram.png linkedin.png nso.png psn.png spotify.png tiktok.png whatsapp.png youtube.png Mastodon's in there! Customising the Badge There's more to the fediverse than Mastodon though, so I decided to try adding "fediverse" to the list of supported socials. Although I like the Asterism logo (⁂), the fedigram is probably more recognised, so I decided to use that. To make sure it was sized and formatted appropriately, I examined an existing logo: $ identify mastodon.png mastodon.png PNG 16x16 16x16+0+0 8-bit sRGB 193B 0.000u 0:00.000 There's a trap here: although the file's colourspace was sRGB, the existing logos were all actually grayscale. Although the badger should handle this, it seemed wise not to change too much at once, so I decided to force my logo to grayscale too. I grabbed a copy of the fedigram, and then adjusted it accordingly. cd badge/assets/socials # Grab the file from wikipedia wget "https://upload.wikimedia.org/wikipedia/commons/9/93/Fediverse_logo_proposal.svg" # Convert to 8 bit, resize and output as png convert Fediverse_logo_proposal.svg \ -depth 8 \ -colors 256 \ -type Palette \ -resize 16x16 \ -set colorspace Gray \ -separate -average \ fediverse.png # Tidy away the old file rm Fediverse_logo_proposal.svg The result looked like this: Zooming in on a computer display, it looked a bit muddled. I decided to push on, though, as I could always swap it back out later. I also loaded my avatar onto the badge: $ cd badge $ identify avatar.png avatar.png PNG 80x80 80x80+0+0 8-bit sRGB 731B 0.000u 0:00.000 $ wget "https://www.bentasker.co.uk/images/eagle_black_large_trans.png" $ convert eagle_black_large_trans.png -resize 80x80 avatar.png $ rm eagle_black_large_trans.png Then updated the details for the badge to display: # details to be shown on the card id_photo = image.load("avatar.png") id_name = "Ben Tasker" id_role = "Master Of The Bits" # see the 'assets/social' folder to see what's supported id_socials = { "fediverse": { "icon": None, "handle": "@ben@mastodon.bentasker.co.uk" } } I unmounted the drive and the badge partially displayed: There were a couple of issues: Neither the logo nor my avatar was displaying My social handle was too long for the screen The image issues must have been my screw-up - clearly I hadn't matched them up to the originals as well as I thought. Socials Font Size I started with the easier fix: reducing the font-size of the social address. The badge code draws social items like this: for account in id_socials.items(): screen.blit(account[1]["icon"], vec2(30, socials_y)) screen.text(account[1]["handle"], 55, socials_y) socials_y += 31 The screen variable is of type image. Documentation for the text method shows that it can be passed a size parameter to control text size. However, it's not quite as simple as that, because the Badger can use both vector and pixel fonts, with each requiring that the font size be set in slightly different ways. So, first, I needed to identify which font was in use. The full block for this part of the code gave the variable name: screen.pen = color.black screen.font = small_font for account in id_socials.items(): screen.blit(account[1]["icon"], vec2(30, socials_y)) screen.text(account[1]["handle"], 55, socials_y) socials_y += 31 The variable was defined further up the script: small_font = rom_font.smart The font documentation tells us that this is likely to be a pixel font, meaning that size needs an integer scale rather than explicit point sizes. The font-gallery tells us that the default size of this font is 9px. Unfortunately, it doesn't seem to be possible to scale a font down, so I instead chose a smaller font: small_font = rom_font.corset Fixing The Images Next up, then, was fixing the images. I considered a few possibilities, including that maybe the colours were inverted (i.e. using white for a background instead of black), but I quickly ruled that out. The other possibility that I considered was a colourspace mismatch. A quick check confirmed that there was a difference: $ identify assets/socials/mastodon.png assets/socials/mastodon.png PNG 16x16 16x16+0+0 8-bit sRGB 193B 0.000u 0:00.000 $ identify assets/socials/fediverse.png assets/socials/fediverse.png PNG 16x16 16x16+0+0 8-bit Gray 256c 447B 0.000u 0:00.000 Not counting the filename, there are three differences in the output, but to understand them I need to look up identify's output format: Filename[frame #] image-format widthxheight page-widthxpage-height+x-offset+y-offset colorspace user-time elapsed-time In practice, then, the three differences are all still one column: colourspace. It seemed that I might have introduced this by overengineering the conversion at the beginning... I re-fetched and kept the conversion simpler: wget "https://upload.wikimedia.org/wikipedia/commons/9/93/Fediverse_logo_proposal.svg" convert Fediverse_logo_proposal.svg -resize 16x16 fediverse.png The output of identify looked more like the original: identify fediverse.png fediverse.png PNG 16x16 16x16+0+0 16-bit sRGB 1355B 0.000u 0:00.000 However, loading this did not go well: This was my fault: I hadn't looked closely enough at the identify output: 16-bit The original images were 8 bit. Rather than iterating through all the other things that I might have missed, I decided to dump out verbose information on each so that I could compare: identify -verbose assets/socials/fediverse.png > /tmp/fedi.txt identify -verbose assets/socials/mastodon.png > /tmp/masto.txt diff -u /tmp/fedi.txt /tmp/masto.txt The interesting parts of the diff looked like this: --- /tmp/fedi.txt 2026-09-14 11:58:17.810030936 +0100 +++ /tmp/masto.txt 2026-09-14 11:58:22.958094172 +0100 - Type: Palette + Type: GrayscaleAlpha @@ -14,369 +14,56 @@ red: 8-bit green: 8-bit blue: 8-bit + alpha: 1-bit - Transparent color: black + Transparent color: none - png:IHDR.color-type-orig: 3 - png:IHDR.color_type: 3 (Indexed) + png:IHDR.color-type-orig: 6 + png:IHDR.color_type: 6 (RGBA) So, my fediverse image used an indexed colour scheme rather than an RGBA palette. Because it didn't use transparency, it lacked an alpha channel - that's unlikely to break anything but it's useful to know that the Badger does support transparency. This means that the correct conversion command should have been: convert Fediverse_logo_proposal.svg \ -resize 16x16 \ -depth 8 \ -strip \ PNG32:fediverse.png No error, but... still... no dice. I was sure that I must still be over-engineering things, so I sat back and thought for a bit. Could it be as simple as contrast? Maybe the image was rendering but the base colours were too light and so didn't convert to black on the e-ink screen? I scaled up the SVG input and told ImageMagick to colourise (essentially forcing the darker bits to be properly dark): convert \ -background none -density 384 \ Fediverse_logo_proposal.svg \ -resize 16x16 \ -depth 8 \ -strip \ -fill '#464646' -colorize 100 \ PNG32:fediverse.png Finally! The next thing to do, then, was to fix my avatar: convert \ -background none \ eagle_black_large_trans.png \ -resize 80x80 \ -depth 8 \ -strip \ -fill '#464646' -colorize 100 \ PNG32:avatar.png It was working! There should be a video here, you may need to visit in a browser Conclusion This was only supposed to be a quick fiddle about with the badge, but ended up dragging me deeper than I'd anticipated. Although the image issue was a bit weird and unintuitive to resolve, having to do a bit of digging is exactly why I bought the badge in the first place - making my brain focus and solve problems in a world otherwise swamped with "talk to the magic box" shortcuts. Future plans involve making use of the badge's wifi support: Building a "Is GitHub Down" app (featuring this familiar character) Integrating with Home Assistant to create a bin day checker The latter might sound a bit boring for a badge, but it's probably going to spend most of its life plugged in, in a stand rather than around my neck, so it makes sense to have it do something helpful.