A fork of claude-desktop-buddy ported to the Seeed XIAO ESP32-C3 + ST7735 128×128 display — a smaller, cheaper, display-only desk companion for Claude.
Claude for macOS and Windows connects to maker devices over BLE, displaying permission prompts, recent messages, and live session state. This fork adapts that firmware for the XIAO form factor: a coin-sized ESP32-C3 board driving a 128×128 ST7735 SPI display with no onboard IMU or buttons.
Building your own device? You don't need any of the code here. See REFERENCE.md for the wire protocol: Nordic UART Service UUIDs, JSON schemas, and the folder push transport.
The desk pet sleeps when nothing's happening, wakes when sessions start, gets visibly impatient when an approval prompt is waiting, and shows live stats across two auto-cycling pages sized for the 128×128 screen.
| Original (claude-desktop-buddy) | This fork (claude-pixclaw) | |
|---|---|---|
| Board | M5StickC Plus | Seeed XIAO ESP32-C3 |
| Display | 135×240 ST7789 | 128×128 ST7735 |
| Input | Hardware buttons (A/B/Power) | Display-only (no onboard buttons) |
| IMU | MPU6886 (shake to dizzy) | Not used |
| Stats layout | Single-page HUD | Two auto-cycling pages (3 s each) |
| Approval UI | On-device approve/deny | Skipped (display-only gap) |
All upstream BLE protocol, GIF character packs, and pet state logic are preserved. Only the board HAL and display layout have been customized.
The firmware targets the Seeed XIAO ESP32-C3 with a 128×128 ST7735 SPI
display, built with the Arduino framework via PlatformIO. The
[env:xiao] build target compiles the display-only variant
(BOARD_XIAO_ST7735 / BOARD_DISPLAY_ONLY).
Install PlatformIO Core, then:
pio run -e xiao -t uploadIf you're starting from a previously-flashed device, wipe it first:
pio run -e xiao -t erase && pio run -e xiao -t uploadOnce running, you can also wipe everything from the device itself: hold A → settings → reset → factory reset → tap twice.
To pair your device with Claude, first enable developer mode (Help → Troubleshooting → Enable Developer Mode). Then, open the Hardware Buddy window in Developer → Open Hardware Buddy…, click Connect, and pick your device from the list. macOS will prompt for Bluetooth permission on first connect; grant it.
Once paired, the bridge auto-reconnects whenever both sides are awake.
If discovery isn't finding the stick:
- Make sure it's awake (any button press)
- Check the stick's settings menu → bluetooth is on
| Normal | Pet | Info | Approval | |
|---|---|---|---|---|
| A (front) | next screen | next screen | next screen | approve |
| B (right) | scroll transcript | next page | next page | deny |
| Hold A | menu | menu | menu | menu |
| Power (left, short) | toggle screen off | |||
| Power (left, ~6s) | hard power off | |||
| Shake | dizzy | — | ||
| Face-down | nap (energy refills) |
The screen auto-powers-off after 30s of no interaction (kept on while an approval prompt is up). Any button press wakes it.
Eighteen pets, each with seven animations (sleep, idle, busy, attention, celebrate, dizzy, heart). Menu → "next pet" cycles them with a counter. Choice persists to NVS.
If you want a custom GIF character instead of an ASCII buddy, drag a character pack folder onto the drop target in the Hardware Buddy window. The app streams it over BLE and the stick switches to GIF mode live. Settings → delete char reverts to ASCII mode.
A character pack is a folder with manifest.json and 96px-wide GIFs:
{
"name": "bufo",
"colors": {
"body": "#6B8E23",
"bg": "#000000",
"text": "#FFFFFF",
"textDim": "#808080",
"ink": "#000000"
},
"states": {
"sleep": "sleep.gif",
"idle": ["idle_0.gif", "idle_1.gif", "idle_2.gif"],
"busy": "busy.gif",
"attention": "attention.gif",
"celebrate": "celebrate.gif",
"dizzy": "dizzy.gif",
"heart": "heart.gif"
}
}State values can be a single filename or an array. Arrays rotate: each loop-end advances to the next GIF, useful for an idle activity carousel so the home screen doesn't loop one clip forever.
GIFs are 96px wide; height up to ~140px stays on a 135×240 portrait screen.
Crop tight to the character — transparent margins waste screen and shrink
the sprite. tools/prep_character.py handles the resize: feed it source
GIFs at any sizes and it produces a 96px-wide set where the character is the
same scale in every state.
The whole folder must fit under 1.8MB —
gifsicle --lossy=80 -O3 --colors 64 typically cuts 40–60%.
See characters/bufo/ for a working example.
If you're iterating on a character and would rather skip the BLE round-trip,
tools/flash_character.py characters/bufo stages it into data/ and runs
pio run -t uploadfs directly over USB.
| State | Trigger | Feel |
|---|---|---|
sleep |
bridge not connected | eyes closed, slow breathing |
idle |
connected, nothing urgent | blinking, looking around |
busy |
sessions actively running | sweating, working |
attention |
approval pending | alert, LED blinks |
celebrate |
level up (every 50K tokens) | confetti, bouncing |
dizzy |
you shook the stick | spiral eyes, wobbling |
heart |
approved in under 5s | floating hearts |
src/
main.cpp — loop, state machine, UI screens
buddy.cpp — ASCII species dispatch + render helpers
buddies/ — one file per species, seven anim functions each
ble_bridge.cpp — Nordic UART service, line-buffered TX/RX
character.cpp — GIF decode + render
data.h — wire protocol, JSON parse
xfer.h — folder push receiver
stats.h — NVS-backed stats, settings, owner, species choice
characters/ — example GIF character packs
tools/ — generators and converters
The BLE API is only available when the desktop apps are in developer mode (Help → Troubleshooting → Enable Developer Mode). It's intended for makers and developers and isn't an officially supported product feature.

