This document describes how Vibe Pet sends desktop pet state to hardware devices.
Hardware integrations should treat the protocol as display-only. Devices receive small JSON packets that describe state, agent label, title, selected character identity, and timestamp. Devices should ignore unknown fields so protocol version 1 can evolve safely.
| Item | Value |
|---|---|
| Service UUID | 7b71f91a-3c7b-4c3b-9f2d-2dbdccd5c001 |
| State characteristic UUID | 7b71f91a-3c7b-4c3b-9f2d-2dbdccd5c002 |
| Characteristic direction | Desktop writes, device receives |
| Payload encoding | UTF-8 JSON |
| Device name prefixes | VibePet-Wio, VibePet-ESP-AI, VibePet-ESP-Display, VibePet-SenseCAP, VibePet-M5, VibePet-LILYGO, VibePet-Heltec, VibePet-WEMOS |
| Legacy prefixes | CodePet-Wio, CodePet-ESP-AI, CodePet-ESP-Display, CodePet-SenseCAP, CodePet-M5, CodePet-LILYGO, CodePet-Heltec, CodePet-WEMOS |
BLE devices advertise the service UUID and use one writable characteristic. The desktop app writes a single compact JSON packet whenever the active pet state changes or needs to be resent.
{
"v": 1,
"s": "working",
"a": "Codex",
"e": "response_item:function_call",
"n": 1,
"m": "vibe-pet",
"p": "lulu-capybara-2",
"d": "噜噜",
"k": "builtin",
"u": "assets/lulu-capybara.webp",
"ts": 1781500000000
}| Key | Type | Description |
|---|---|---|
v |
number | Protocol version. Currently 1. |
s |
string | Pet state. See States. |
a |
string | Agent label, such as Codex, Cursor, or Windsurf. |
e |
string | Source event name. |
n |
number | Active session count after aggregation. |
m |
string | Short session title or workspace basename. |
p |
string | Selected character/persona slug. |
d |
string | Selected character/persona display name. |
k |
string | Character/persona kind, such as builtin or petdex. |
u |
string | Optional spritesheet URL or asset path. |
ts |
number | Desktop timestamp in milliseconds. |
The color display firmware also accepts long-form aliases such as state, agentName, agent, event, title, activeCount, and nested persona fields. Low-resource devices can ignore character fields and render a local simplified character.
| State | Suggested device behavior |
|---|---|
idle |
Calm idle animation. |
thinking |
Thinking or eye movement animation. |
working |
Active tool-use or editing animation. |
typing |
Text output animation. |
building |
Stronger work/build animation. |
juggling |
Multi-task animation. |
attention |
Soft notification or completed-turn pose. |
notification |
Approval or important action indicator. |
error |
Error color, shake, or alert pose. |
sweeping |
Context cleanup or sweep animation. |
sleeping |
Sleep or dimmed state. |
permission and codex-permission are normalized to notification by the bridge before hardware rendering.
ESP8266 boards do not provide BLE, so they use local Wi-Fi polling:
GET /api/device-snapshot
The response contains the hardware-facing pet list and an aggregate fallback:
{
"v": 1,
"at": 1781500000000,
"pets": [
{
"id": "editor:cursor:/project",
"title": "vibe-pet",
"state": "working",
"stateLabel": "Working",
"agentId": "cursor",
"agentName": "Cursor",
"persona": {
"slug": "lulu-capybara-2",
"displayName": "噜噜",
"kind": "builtin",
"spritesheetUrl": "assets/lulu-capybara.webp"
},
"packet": {
"v": 1,
"s": "working",
"a": "Cursor",
"m": "vibe-pet",
"p": "lulu-capybara-2",
"d": "噜噜",
"k": "builtin",
"u": "assets/lulu-capybara.webp",
"ts": 1781500000000
}
}
],
"aggregate": {
"v": 1,
"s": "working",
"a": "Cursor",
"m": "vibe-pet",
"ts": 1781500000000
}
}Display firmware should select the first pet whose state is not idle or sleeping. If all pets are idle, select the first pet. If pets is empty, render aggregate.
The desktop UI lets each pet choose a character. Hardware receives the selected identity through:
| Field | Meaning |
|---|---|
p |
Character slug, stable id for display caching. |
d |
Display name, such as 噜噜. |
k |
Character source/type, such as builtin or petdex. |
u |
Spritesheet URL or local asset path. |
Color displays can use these fields to choose a palette, local sprite, or downloaded sprite. OLED devices can render the same name and state with a local simplified body.
Desktop may follow a normal state packet with optional image-transfer packets when the selected character uses an external spritesheet. Devices that do not implement image transfer should ignore packets whose im field is unknown.
Wio-class devices receive the legacy per-state frame stream:
im |
Meaning |
|---|---|
s |
Start one RGB565 frame for one visual state. |
c |
Append a base64 data chunk for the active frame transfer. |
e |
Finish the active frame transfer. |
x |
Cancel the active transfer. |
ESP color devices receive a single atlas stream so the device stores one image and selects a display region for the current state:
im |
Meaning |
|---|---|
as |
Start an RGB565 atlas transfer. |
ac |
Append a base64 data chunk for the active atlas transfer. |
ae |
Finish the active atlas transfer. |
x |
Cancel the active transfer. |
The atlas start packet includes the character identity plus layout metadata:
{
"im": "as",
"id": "transfer-id",
"p": "petdex-slug",
"d": "Display name",
"k": "petdex",
"u": "spritesheet-url",
"w": 144,
"h": 156,
"aw": 288,
"ah": 936,
"f": "rgb565-rle",
"z": 539136,
"cols": 2,
"rows": 6,
"fc": 2,
"st": "idle,notification,working,error,thinking,attention",
"ld": 1,
"th": "day"
}w/h describe one frame cell, aw/ah describe the full atlas, z is the decoded RGB565 byte count, and f is either rgb565 or rgb565-rle. Chunk packets use { "im": "ac", "id": "...", "q": 0, "d": "..." }, where q increments from 0 and d is base64 data. ESP color firmware also accepts binary atlas chunks for compatibility experiments, but the desktop bridge uses the JSON chunk path by default because it is easier to validate over BLE. Firmware may discard older dynamic image files before receiving the atlas to keep storage bounded; after ae, it should rename the completed temporary atlas into the current atlas path.
- Advertise a
VibePet-*device name and the service UUID. - Expose the state characteristic as writable.
- Parse incoming UTF-8 JSON.
- Read
s,a,e,m,p,d,k,u, andn. - Ignore unknown fields.
- Fall back to
idle,agent, and a local default character when fields are missing. - Keep rendering the last valid packet if a malformed packet arrives.
Pseudo-code:
void applyPacket(JsonVariantConst src) {
String state = src["s"] | src["state"] | "idle";
String agent = src["a"] | src["agentName"] | src["agent"] | "agent";
String title = src["m"] | src["title"] | "";
String persona = src["d"] | src["persona"]["displayName"] | "Lulu";
renderPet(state, agent, title, persona);
}Hardware is receive-only in the current protocol. Devices do not send prompts, approval decisions, tool input, or transcript content back to the bridge.