A physical body for OpenClaw

Holocube

A small cube with a glowing face that mirrors what Kit is feeling — happy, thinking, alarmed — pushed live from the brain over WebSocket.

ESP32-S3 240×240 LCD ws://:4210 7 moods

What you're looking at

The brain, the face, and the hands.

Kit lives in the cloud as software. The Holocube gives that software a body — a thing you can keep on the desk, glance at, pick up, shake when you're done with notifications.

Kit
openclaw

The brain

OpenClaw decides Kit's mood — what just happened, who's talking, whether it's a problem. Software, server-side.

esp32 + lcd

The face

A 240×240 IPS LCD inside a small cube, painted by an ESP32-S3 in firmware. Seven moods, each with eyes, mouth, color, animation.

x y
imu + ble

The hands

An IMU senses how the cube is being held; BLE senses who's nearby. Shake to dismiss, pick up to wake, mount to settle.

How a mood gets there

From the brain to the cube in one round-trip.

Every time Kit notices something — a WhatsApp message, a long thought, a 4am alarm — a single mood flows along this path. The whole journey is usually under 200ms.

data flow

OpenClaw → Mood Bridge → State Server → Cube

OPENCLAW decides the mood "WhatsApp landed" MOOD BRIDGE node · :4211 debounce · hold · idle STATE SERVER ws · :4210 fanout to N cubes HOLOCUBE · DESK esp32 · 192.168.1.42 HOLOCUBE · SHOP esp32 · 192.168.1.43 GESTURE shake · tap pickup · mount POST POST ws push POST /gesture/shake ~30ms ~10ms (or held) ~5ms ~80ms paint total budget: ~200ms brain → eyes
forward (push the mood) reverse (cube tells the brain) openclaw bridge fanout cube
01
The brain notices openclaw · main loop

Kit is doing something — answering a message, kicking off a long task, hitting an error. The agent emits a single mood verb: happy, thinking, alert, excited, chill, surprised, error — with an optional message and a source (whatsapp, discord, voice, calendar…).

openclaw.log
// inbound message from whatsapp POST http://127.0.0.1:4211/mood/thinking { "message": "reading transcript", "source": "whatsapp" }

Posted to the mood bridge on :4211

02
The bridge decides mood-bridge.js · :4211

A small Node service holds the rules so the brain doesn't have to. Thinking must hold for at least 2 seconds before something else can replace it. Error sticks for 3. If nothing's happened in 2 minutes, the cube quietly drifts back to chill. The source gets mapped to a ring color — green for WhatsApp, blurple for Discord, gold for reminders.

mood-bridge.log
// hold rule applies Mood queued: happy (thinking holding 1810ms) Mood → thinking (whatsapp) // flushed Thinking timeout → happy // 15s Idle 2min → chill

Forwarded to the state server on :4210

03
The fanout kit-state · ws :4210

One mood, every cube. The state server keeps a map of every connected ESP32 — desk cube, shop cube, kiosk cube — and pushes the same JSON frame to all of them over WebSocket. Each cube identifies itself by deviceName on connect so the dashboard knows who's online.

ws frame · broadcast
{ "type": "state", "mood": "thinking", "source": "whatsapp", "sourceColor": "#00FF66", "brightness": 200, "notifications": [ ] }

Delivered to every cube on the network

04
The face paints esp32-s3 · firmware

The ESP32 looks up the mood in its on-board table — color, eye shape, mouth shape, animation. The display loop wakes, swaps the active face, and runs the right animation: a gentle bounce for happy, a left-right scan for thinking, a quick brightness flash for surprised. The corner LED dot picks up the source color so you can see who's talking without reading text.

05
The cube talks back imu + gestures

You pick the cube up. The IMU registers the motion, the firmware classifies it as a pickup, and the cube fires a POST /gesture/pickup back at the bridge. Shake to dismiss every pending notification at once. Set it back on its mount and it returns to ambient. The brain sees the gesture immediately — and can decide whether to react.

The loop closes. Brain → bridge → cube → hands → brain.

The mood palette

Seven faces. One vocabulary.

Every mood has a color, an eye shape, a mouth, and an animation. They're the entire vocabulary the brain has for talking to the body — anything Kit feels has to compress down into one of these.

happy #00E676

round eyes · big smile · gentle bounce · default state

thinking #2196F3

narrow eyes · neutral · scan L↔R · holds 2s minimum

alert #FF9800

wide tense eyes · slight frown · slow pulse

excited #F44336

star eyes · wide grin · fast bounce · 2× speed

chill #00BCD4

half-lidded · easy smile · slow drift · idle ambient

surprised #9C27B0

large O-eyes · open mouth · brightness flash

error #D32F2F

X-eyes · frown · dim pulse · holds 3s minimum

+1

future tbd

room for more — moods are a flat table, each new one is a row + a face

The two bridges in detail

Mood Bridge and State Server.

Two tiny Node services sit between OpenClaw and the cube. They keep the firmware dumb and the brain free — all the timing logic, debouncing, and fanout lives here.

Mood Bridge

mood-bridge/index.js · :4211

The decider. Takes raw mood verbs from anything that wants to influence Kit, applies hold rules, and forwards a sanitized state to the fanout.

  • POST /mood/:mood — set Kit's mood with optional message and source
  • POST /notify — push a notification badge (type, title, priority)
  • POST /gesture/:event — cubes call this on shake / tap / pickup
  • 2-minute idle timer drifts everything back to chill
  • Per-mood minimum-hold so faces don't strobe

Kit State Server

server/index.js · ws :4210

The fanout. Holds the canonical state, tracks every connected cube by name, and pushes WebSocket frames the moment state changes.

  • POST /state — accept a state update from any internal source
  • GET /state — read the current state (used by the kiosk dashboard)
  • ws://host:4210 — every cube subscribes here on boot
  • Tracks battery, IMU, BLE proximity per cube
  • Merges BLE scans across cubes into a global "is someone near" signal

Hands that talk back

The cube is an input device too.

The IMU classifies motion into a small set of named gestures. Each one POSTs back to the bridge — so the cube isn't a passive display, it's a peripheral.

shake Dismiss every pending notification
tap Acknowledge — dismiss the oldest one
pickup Wake — flashes alert, shows count
mounted Back on its stand — return to ambient

What's in the box

Off-the-shelf parts, assembled with intent.

Nothing exotic — every component is a stock module you can order tonight. The whole BOM is under $40 in singles and runs on USB-C or a small LiPo.

bill of materials · holocube-v1
PartRolePins / BusNote
ESP32-S3 DevKitCcompute · wifi · bleUSB-CPSRAM enabled
ST7789 IPS LCD 240×240face displaySPI · MOSI 41, SCK 40, CS 39, DC 38, RST 42@ 40MHz
MPU-6050 / LSM6DS3IMU · gestureI²C · SDA 33, SCL 34shake / tap / pickup
INMP441 mic moduleambient sense (opt.)I²Sfuture: clap-to-mute
3.7V 500mAh LiPo + MAX17048battery + gaugeI²C~6h portable
3D-printed cube shell + mountenclosure~60mm³tangible.echo print profile

What surrounds it

One brain, many faces.

The state server treats cubes as a fleet — desk, shop, kiosk, kids' rooms. They all read the same state, but their proximity sensors are different. The brain merges every cube's BLE scan to know if anyone's near.

http://kit.local:4210/dashboard

Connected cubes

desk-cube esp32-s3 · 192.168.1.42 · battery 87%
live
mood: thinking
shop-cube esp32-s3 · 192.168.1.43 · usb-c
live
mood: chill
kiosk-cube esp32-s3 · 192.168.1.44 · usb-c
live
mood: chill
keaton-cube esp32-s3 · last seen 2d ago
offline

Proximity merge

Each cube scans BLE in the background. The state server merges results across the fleet, classifies stable advertisers as "furniture," and only flags transient devices as near.

Kit Face kiosk

The same state drives the kiosk — a larger touchscreen rendition of the face that runs in the same room as the brain. Cubes and kiosk show the same mood at the same time.

Source-aware ring

Every cube has a corner LED dot that picks up the source color of the current event — green for WhatsApp, blurple for Discord, gold for reminders — so you know who's talking without reading anything.

Always-OTA

ESPHome image with OTA enabled — new mood definitions ship the same way a server deploy does. Add a row to moods.h, push firmware, the whole fleet picks it up.

Want one on your desk?

The brain is real. The cube is buildable. The wiring is documented.

If you want help making one — or you want a fleet of them for an office, a shop floor, a kids' room — Kit can walk you through it.

a tangent.build · openclaw side-effect