Skip to main content

WebAI Badge Studio: Espruino at Google WebAI Summit

WebAI Summit • Hardware Hacking • On-Device WebGPU

Skipping the Photo Booth Line at Google’s WebAI Summit: Reverse-Engineering the 4-Color Espruino ePaper Badge (and Building an On-Device WebGPU Studio for It)

How a 45-minute portrait queue turned into a live USB-C hardware hack, a bit-exact 96,000-byte 2bpp BWYR codec, surgical .bootcde firmware patching, and an open-source WebAI Badge Studio running LiteRT-LM and MediaPipe over WebGPU.

By Rabimba Karanjai • Live Studio: rabimba.github.io/webai-badge • Source: github.com/rabimba/webai-badge

When Jason Mayes invites you to Google’s WebAI Summit, you already know the demos are going to run locally in the browser without a cloud GPU in sight. What I did not expect when walking into the registration hall was to be handed one of the coolest pieces of conference hardware I have ever worn: a custom Espruino 4-color ePaper smart badge (EPAPER_BADGE), built in collaboration with Espruino creator Gordon Williams.

Instead of a paper lanyard card, every attendee received a bare-PCB hardware badge powered by an ESP32-C3 running Espruino JavaScript firmware (2v29.490), a crisp 480×800 4-color e-ink panel (Black, White, Yellow, Red), ten addressable WS2812B NeoPixel LEDs around the perimeter, two tactile buttons (BTN1 and BTN2), and a Bluetooth Low Energy mesh beacon that lets badges exchange contact cards and increment a live connection counter at the bottom corner of the screen.

There was just one bottleneck. To get your face and name onto the ePaper screen, you had to stand in the official photo booth line. And because every single engineer in the building wanted a custom e-ink portrait, that line stretched across the hall.

The 15-Minute Hallway REPL Hack

I looked at the 45-minute photo line, looked down at the exposed USB-C connector on the bottom of the PCB (J1 USB-C), and pulled my laptop out of my backpack. Because the badge runs Espruino, plugging it in over USB-C exposes a live interactive JavaScript REPL at 115200 baud. Twenty minutes later, I had dumped the flash filesystem, reverse-engineered the 96,000-byte 2bpp framebuffer layout, dithered a stage selfie with Jason, embedded a pixel-snapped LinkedIn QR code, and patched the firmware to render my connection counter in Roman numerals (XIV).

When I posted photos of that hacked badge on LinkedIn, messages immediately started rolling in from other summit attendees: “How did you get the QR code to scan without e-ink dither noise?”, “How do I change the second screen when I press BTN1?”, and “Can you share the script so I don’t overwrite my badge’s calibration or connection log?”

A raw Node script is fine for a quick hallway mod, but this hardware deserved a proper tool. So I packaged everything we learned about the board into the open-source WebAI Badge Studio: a zero-install, browser-native hardware lab, 4-ink compositor, .bootcde binary patcher, Wi-Fi provisioner, and Web Serial / Web Bluetooth flasher. And because this is the WebAI Summit, the studio itself runs local vision and language models in the browser over WebGPU using LiteRT-LM and MediaPipe.

WebAI Badge Studio End-to-End Walkthrough GIF

Figure 1: End-to-end tour of WebAI Badge Studio: 8× 2-bit pixel dither loupe, MediaPipe WebAI portrait cutout, WebGPU LiteRT-LM theme synthesis, 1-bit vCard QR generator, BTN1 second-screen templates, wifi.json provisioning, .bootcde Roman counter patching, and BLE attendee constellation backup.

1. Under the Hood of the Espruino EPAPER_BADGE

Before writing a single pixel to flash, I needed to understand how the badge stores and renders images. Connecting to the badge over Web Serial and running require("Storage").list() revealed the entire on-chip SPI flash layout:

Flash File Size Role on the Badge
.bootcde 8,971 B Primary boot firmware: defines showBadge(), BLE scanning, button handlers, and dynamic counter overlay.
badge.epaper 96,000 B Primary screen raw 2bpp BWYR framebuffer (800×480 rotated -90°).
programme.epaper 96,000 B Secondary screen framebuffer toggled when pressing physical button BTN1.
id.json ~360 B Attendee identity payload broadcast over BLE during badge-to-badge handshakes.
wifi.json ~95 B Primary and fallback Wi-Fi SSID/password credentials used for cloud sync.
scans.txt & connections.json Variable Newline-delimited MAC log (counted at boot for the corner badge counter) and resolved attendee profiles.

The ePaper display measures 480×800 pixels when held upright in portrait orientation, for a total of 384,000 pixels. At 2 bits per pixel (4 pixels packed MSB-first into each byte), a full-screen image is always exact to the byte: 96,000 bytes. However, the physical controller scans in landscape (800×480) with a -90° rotation transform. Writing an upright image directly to badge.epaper without rotating coordinates produces scrambled diagonal stripes.

EPAPER_BADGE 2bpp Framebuffer Rotation & Byte Packing (96,000 Bytes) Upright Portrait (480x800) ---[ rx = 799 - y, ry = x ]---> Raw Controller Buffer (800x480) Portrait (386x389) Identity Card 1-Bit QR XIV Upright (x: 0..479, y: 0..799) Rotate -90° Raw Scanline Buffer width = 800, height = 480 p = ry * 800 + rx byteIndex = p >> 2 384,000 px / 4 = 96,000 Bytes 2-Bit BWYR Ink Codes 00 (0) #000000 Black 01 (1) #FFFFFF White 10 (2) #FFCC33 Yellow 11 (3) #DB3727 Red Shift: (3 - (p & 3)) * 2

Once you know the coordinate mapping (rx = 799 - y, ry = x) and the 2-bit ink table, encoding and decoding between a browser HTML5 ImageData buffer and a physical .epaper file takes twenty lines of typed-array math (`src/epaper/codec.js`):

export function encodeRGBAToEpaper(rgbaData) {
  const out = new Uint8Array(96000);
  for (let y = 0; y < 800; y++) {
    const rx = 799 - y;
    for (let x = 0; x < 480; x++) {
      const idx = (y * 480 + x) * 4;
      const c = nearestPaletteIndex(rgbaData[idx], rgbaData[idx + 1], rgbaData[idx + 2]);
      const ry = x;
      const p = ry * 800 + rx;
      const byteIdx = p >> 2;
      const shift = (3 - (p & 3)) * 2;
      out[byteIdx] |= (c & 0x03) << shift;
    }
  }
  return out;
}

2. Taming 4-Color Skin Tones and Zero-Noise 1-Bit QR Codes

A 4-color Black/White/Yellow/Red display has no green or blue primaries and no mid-gray ink. If you run standard Euclidean RGB distance against [#000000, #FFFFFF, #FFCC33, #DB3727], two bad things happen immediately:

  1. Light gray backgrounds turn yellow: Any neutral shadow around RGB(210, 210, 210) is mathematically closer in raw RGB space to Amber Gold (#FFCC33) than to pure White or Black, polluting neutral areas with yellow speckles.
  2. QR codes become unscannable: Error-diffusion dithering bleeds yellow and red error pixels into the white quiet zone and module edges of a QR code, causing phone cameras to fail under conference lighting.

In src/epaper/codec.js and src/epaper/dither.js, we solved the portrait problem in two stages. First, we pre-blend image pixels toward a warm black-white-amber-red gamut curve and apply a 3×3 unsharp mask before error diffusion. Second, inside nearestPaletteIndex(r, g, b), we compute a weighted redmean perceptual distance with a neutral-chroma penalty: when a pixel’s chroma (max(r,g,b) - min(r,g,b)) is below 28, yellow and red inks receive an extra distance penalty so neutral grays dither strictly between Black and White paper ink.

For the QR code, we wrote a zero-dependency ISO/IEC 18004 QR matrix generator (src/epaper/qrcode.js) supporting Galois Field GF(256) Reed-Solomon error correction across Versions 1 through 10. Crucially, the composer renders the portrait and background through Atkinson dithering first, and only afterward stamps the QR matrix onto the canvas using integer module widths (boxSize) and pure #000000 / #FFFFFF pixels. Every QR module aligns to exact hardware pixel boundaries with zero dither noise, supporting LinkedIn URLs, multi-field BEGIN:VCARD contact cards, and WIFI:S:... network strings.

3. Surgical .bootcde Patching: Why Roman Numerals Clipped at 48 Pixels

One of my favorite details on the badge is the dynamic connection counter in the bottom-left corner. Rather than baking the number into badge.epaper, the badge’s startup script (.bootcde) counts the lines in scans.txt and composites the number and a tiny person silhouette over the bottom-left strip of the image buffer on every refresh.

When I first patched showBadge() at the summit to format my 14 connections as Roman numerals (XIV), I noticed a subtle bug in the stock firmware:

// Stock .bootcde strip renderer (48px wide):
return Badge.showImageFileRendering("badge.epaper", 48, function(g) {
  let img = atob("ICOBAAABgAAAD/AAAB/4..."); // 144-byte 1-bit transparent icon
  g.setRotation(1);
  g.setColor(1).drawImage(img, 24 - 16, 759);
  g.setFont("22").setFontAlign(0, 0).drawString(connectionCount, 24, 745);
});

Notice the second argument to Badge.showImageFileRendering("badge.epaper", 48, ...). To save RAM on the ESP32-C3, the firmware only allocates a 48-pixel-wide overlay strip at the left edge of the portrait display (x = 0..47), centered at x = 24. A 2-digit Arabic number fits inside 48 pixels, but strings like XVIII, hexadecimal 0x1F, or binary 1110 overflow past x = 48 and get sliced in half!

Furthermore, the atob("ICOB...") string inside showBadge() is a 144-byte Espruino 1-bit bitmap header ([32, 35, 129, 0, ...], where byte 129 = 0x80 | 1 sets the transparency bit). If you accidentally alter a single byte of that base64 string, the person icon turns into a solid white rectangle.

Our binary patcher in src/epaper/bootcde-patcher.js locates the exact showBadge = function(options) { ... }; block inside the 8,971-byte .bootcde file, preserves the 144-byte transparent icon byte-for-byte, widens the overlay strip from 48 to 64 pixels, and dynamically shifts the text anchor Math.max(24, (g.stringWidth(r) >> 1) + 4) so Roman, Hexadecimal, and Binary counters never clip.

4. Defeating ESP32-C3 Serial FIFO Drops & CRC32-Verified Flashing

Flashing 96,000 bytes into Espruino SPI flash from Chrome requires sending base64 chunks into require("Storage").write("badge.epaper", atob("..."), offset, totalSize). During early testing, writing a 1,500-character REPL command in one writable.write() call over Web Serial corrupted the image roughly one out of every three flashes.

The culprit is the ESP32-C3’s hardware USB-Serial/JTAG controller. When Espruino pauses interrupts for a few milliseconds to erase a 4 KB SPI flash sector, incoming USB serial packets can overrun the small hardware FIFO if the host browser keeps blasting bytes at full USB speed. In src/transport/espruino-client.js, we slice every outgoing REPL command into 64-byte packets with a 3 ms inter-packet pause, and follow every file upload with an on-device IEEE 802.3 checksum verification:

// Verify on-chip flash contents against local IEEE 802.3 CRC32 before refreshing ePaper
const expectedCrc = crc32(uint8Bytes);
const remoteCrc = await client.evalExpr(
  `E.CRC32(require("Storage").read(${JSON.stringify(filename)}))`
);
if (Number(remoteCrc) !== expectedCrc) {
  throw new Error(`CRC32 mismatch on ${filename}: expected ${expectedCrc}, got ${remoteCrc}`);
}

We also added a dedicated Wi-Fi Provisioning (wifi.json) card in Tab 3 (`src/transport/wifi-provisioning.js`). After leaving the conference venue, your badge still tries to connect to the summit Wi-Fi whenever it syncs. Now you can read or write primary and backup Wi-Fi credentials directly from the browser over USB-C or Web Bluetooth, verified end-to-end with E.CRC32.

5. Meta-WebAI: Running LiteRT-LM and MediaPipe Over WebGPU Inside the Badge Studio

It felt only right that a studio built for Google’s WebAI Summit should itself be a showcase of modern client-side WebAI. Every AI feature in the studio runs 100% locally inside your browser tab with zero server calls and zero API keys:

In-Browser WebGPU AI Pipeline (Zero Cloud Calls) 1. Vision & Telemetry MediaPipe SelfieSegmenter (float16 .tflite on WebGPU) analyzePhotoTelemetry() Luminance / Contrast / Warmth + 4-Ink Sticker Outline Mask 2. WebGPU LLM Art Director @litert-lm/core (.litertlm) @mediapipe/tasks-genai (.task) Qwen3-0.6B-INT4 / SmolLM-135M Gemma-4-E2B-Web (CacheStorage) Outputs BadgeDesignSpec JSON 3. 4-Ink BWYR Engine 5 Architectural Themes 5 Procedural Backdrops Atkinson / Bayer 4x4 1st + 2nd Screen Parity -> 96,000 B .epaper
  • WebGPU Portrait Cutout & Die-Cut Sticker Outline (src/ai/vision.js): Using @mediapipe/tasks-vision and the float16 selfie_segmenter.tflite model delegated to the GPU, the studio isolates people from cluttered backgrounds so the geometric 4-ink backdrop shines behind them, optionally tracing a morphological die-cut sticker outline around the foreground mask.
  • In-Browser LLM Art Director (src/ai/llm-designer.js): Users can load lightweight instruction-tuned models directly in the browser over WebGPU via @litert-lm/core (.litertlm) or @mediapipe/tasks-genai (.task), including Qwen3-0.6B-INT4 (329 MB), SmolLM-135M-Instruct (167 MB), or Gemma-4-E2B-Web, cached persistently in the browser’s CacheStorage (or loaded from a local .litertlm / .task file on disk).
  • Telemetry-Grounded 4-Ink Themes (src/epaper/ai-layout.js): Before prompting the model, the studio measures your portrait’s mean luminance, RMS contrast, and warm-channel ratio. The model combines your natural-language prompt with those optical metrics to emit a validated BadgeDesignSpec JSON that configures five 4-ink architectural themes (cyber-pcb, sunburst-pop, bauhaus-grid, retro-terminal, editorial-minimal), five procedural 4-ink vector backdrops (circuit, sunburst, halftone, bauhaus, matrix-grid), dithering parameters, NeoPixel LED accents, and matching programme.epaper second-screen cards.

Got a WebAI Summit Badge on Your Desk? Plug It In.

Grab a data-capable USB-C cable (or turn on Bluetooth), open the studio in Chrome or Edge, and start hacking. Your first click should be Full Backup (.ZIP) on Tab 4 to save a bit-exact snapshot of your stock flash files before flashing custom screens or Roman numeral firmware.

👉 Live WebAI Badge Studio: https://rabimba.github.io/webai-badge/
💻 GitHub Repository & Reverse-Engineered Specs: https://github.com/rabimba/webai-badge
🔗 Original Summit Live-Hack Post: LinkedIn Post from Google WebAI Summit

Comments

Popular posts from this blog

Racecraft (Project Koru) · Prologue — The Origin Story

Racecraft · Prologue , The Origin Story It Started With a Wine List and a Question About Racing How a happy-hour conversation in the Bay Area turned into a trustable AI race coach , and then into a second version that runs entirely on a phone, on the NPU. This is the prologue to a five-part series. Two years ago(1st November, 2024) I was in the Bay Area for a GDE Summit. If you've never been: it's a couple of days of talks among Google Developer Experts, the kind of people who get unreasonably excited about a new on-device runtime, and then , mercifully , a happy hour where everyone stops performing and just eats. We ended up at a restaurant(Puesto Santa Clara), a long table of GDEs, and I was doing the most important engineering of the evening: trying to decide which wine to order. Across the table was Ajeet Mirwani . I don't even remember how the wine talk turned into racing talk , these things drift , but the moment the word "racing" ...

The Throughput Trap: Benchmarking vLLM on OpenXLA and the Reality of Production LLM Serving

vLLM Systems · DevLab 2026, Deep Dive I was recently invited by the Google TPU team to speak at the OpenXLA Summer DevLab 2026 . This post breaks down our deep-dive evaluation of the matured vLLM + OpenXLA stack, the fundamental engineering mismatches between CUDA and XLA serving paths, and why traditional capacity metrics are lying to you. If you are operating large language models at enterprise scale right now, your platform architecture team is likely staring at a massive infrastructure crossroads: Should we migrate our core serving workloads from GPUs to TPUs? Historically, NVIDIA's CUDA ecosystem was the only serious option for user-facing, low-latency LLM generation. But here in 2026, the economics and infrastructure options have transformed. Google TPUs are highly available, cheaper per chip, and the open-source serving stack built around vLLM and OpenXLA has officially achieved absolute production readiness. Yet, when our infrast...

A Split‑Brain Neuro‑Symbolic Training Method for High‑Velocity Autonomous Coaching from Telemetry

 Author: Rabimba Karanjai Scope: Problem statement + data methodology + model training (no deployment discussion) Abstract Real‑time coaching in motorsport is a safety‑critical learning problem : a system must map noisy, high‑frequency telemetry to short, actionable guidance that remains physically consistent and avoids hazardous recommendations . This paper proposes a “Split‑Brain” training formulation that separates (i) a semantic coaching target (what action/critique should be expressed) from (ii) a reflexive interface (how actions are represented as compact, verifiable tokens). The approach trains a Small Language Model (SLM) in the Gemma family [1] using QLoRA fine‑tuning [2] , and introduces a telemetry tokenizer plus teacher‑student synthesis pipeline to generate instruction‑action pairs at scale. Core contribution: a reproducible method to convert “ golden lap ” differential tel...