A physical status companion for OpenClaw agents. A Raspberry Pi-powered display that lives on your desk and shows what your AI is doing — even when you're not looking at a screen. It senses your presence with a thermal camera, lets your agent send you notifications with a feedback loop, and can ask for your approval with physical buttons.
Inspired by Pwnagotchi, built for OpenClaw.
Try the interaction loop in your browser:
👉 https://snarflakes.github.io/snarling/demo/
Snarling is a tiny creature on a tiny screen. It reacts to your agent's state in real time — sleeping when idle, focused when processing, chatty when responding, and alert when it needs your approval on something. It can also nudge you with notifications — subtle at first, revealing more when you press A — and report back whether you actually read them, enabling notification attunement.
Instead of checking your phone or terminal to see if your agent is working, resting, or needs attention — just glance at Snarling.
| State | Face | When |
|---|---|---|
| Sleeping | (⇀‿‿↼) |
Agent is idle |
| Processing | (◕‿‿◕) |
Agent is using tools / thinking |
| Communicating | (ᵔ◡◡ᵔ) |
Agent is generating a response |
| Error | (╥☁╥ ) |
Something went wrong |
| Awaiting Approval | ( ⚆_⚆) |
Agent needs your yes/no decision |
| Notification | (◕‿‿◕) |
Agent has something to tell you |
| Proximity Aware | (≖◡◡≖) |
Someone is nearby while agent is sleeping (thermal sensor) |
| Leaving | (◡‿◡) |
Someone is walking away (thermal sensor, ~500ms) |
| Listening | (◕‿‿◕) teal |
Voice recording in progress (X button) |
Each state has its own color, LED pattern, and animation — breathing blue when sleeping, pulsing melon when processing, flashing red when approval is needed.
Snarling also handles notifications — informational alerts from your agent that don't require a decision. Unlike approvals, notifications are subtle:
- The creature's face changes to a notification expression (colored by priority)
- The 5 status boxes at the top fill based on priority (1 for low, 3 for normal, 5 for high) and change color
- The LED pulses in the priority's color (warm orange for high, yellow for normal, soft yellow for low)
- No text is shown until you press A — the notification stays as a subtle visual presence on the creature's face
| Button | Action |
|---|---|
| A | Reveal notification text (cycles through banners) |
| B | Dismiss without reading |
| (no press) | Auto-dismiss after timeout (low priority only) |
When you press A, the notification text appears below a separator line in 2–3 rotating banners (priority header + short preview, then full message across 2 content banners). The creature's expression shifts subtly to acknowledge you're reading.
| Priority | LED Color | Status Boxes | Timeout |
|---|---|---|---|
| high | Warm orange | 5/5 filled | None — stays until you interact |
| normal | Yellow | 3/5 filled | None — stays until you interact |
| low | Soft yellow | 1/5 filled | 28800s (8h) — auto-dismisses, sends timed_out feedback |
No urgent or moderate notification should ever just disappear. Only low-priority notifications auto-dismiss.
When you interact with a notification (reveal, dismiss, or let it time out), Snarling sends feedback back to the OpenClaw gateway:
{
"notification_id": "notify-1234567890-abc",
"revealed": true,
"time_to_reveal_sec": 42.5,
"dismissed": false,
"timed_out": false,
"secret": "uuid",
"sessionKey": "agent:main:main"
}time_to_reveal_sec measures the total time from when the notification was sent to when you interacted with it — including any time spent queued behind other notifications. This enables notification attunement: the agent learning what kinds of messages you respond to, and when.
If a notification is queued behind another one (e.g., a normal-priority notification is already on screen), it waits in a priority-sorted stack. When the first notification is resolved, the next one appears automatically.
Messages are word-wrapped across up to 3 banners that cycle every ~1.5 seconds:
- Banner 1: Priority header (e.g.,
!! HIGH,* MODERATE,~ LOW) + short preview - Banner 2: Full message (up to 3 lines of wrapped text)
- Banner 3: Continuation for longer messages (only if needed)
Short messages get 2 banners. Longer messages get all 3.
Snarling has a built-in microphone input flow via the X button. When pressed in normal state (not during an approval or notification), Snarling:
- Enters listening state (teal face, pulsing fill bars,
🎙 Listening...message) - Records 20 seconds of audio locally via
arecord(no gateway dependency for recording) - Switches to processing state (
⏱ Thinking...) while POSTing the WAV path to the plugin's/transcribe-and-replyendpoint - The plugin transcribes the audio and injects it as a voice system event into the agent's session
- On success, the plugin drives state transitions back to sleeping via the
/stateAPI - On failure, Snarling falls back to sleeping
The mic is checked at startup — if no audio input device is found (plughw:3,0), the X button shows No mic found instead of starting a recording. The WAV file is cleaned up after 60 seconds to give the plugin time to read it.
Snarling isn't just a display — it's an input device. When your agent needs approval for an action (deleting a file, sending a message, etc.), Snarling enters awaiting approval state and shows the request on screen.
Multiple approvals no longer overwrite each other. If an approval is already on screen when a new one arrives, the new one is queued behind it (FIFO). When the current approval is resolved (approved or rejected), the next one in the queue is automatically displayed. Each queued approval carries its own session_key so the A/B callback always routes to the correct agent session, even when two different agents queue approvals simultaneously.
This also means approvals take priority over notifications — if a notification is on screen when an approval arrives, the notification is bumped back to the notification stack. After the last approval is resolved, queued notifications reappear automatically.
Notifications are held in a priority-sorted stack (high → normal → low, newest-first within same priority). If a higher-priority notification arrives while a lower one is on screen and the user hasn't revealed it yet, the higher-priority one bumps the current notification back to the stack and takes over. Once resolved, the stack shows the next highest-priority notification.
| Button | Normal Mode | Approval Mode | Notification Mode |
|---|---|---|---|
| A | Show status summary | ✅ Approve | Reveal text (1st press) → Accept (2nd press) |
| B | (unused) | ❌ Reject | Dismiss without reading |
| X | 🎙 Record voice | — | — |
| Y | Toggle sleep mode | — | — |
When you press A or B, Snarling POSTs the decision or feedback back to the OpenClaw gateway's /approval-callback or /notification-callback routes, then sends a WebSocket RPC wake so the agent picks up the result immediately (~5 seconds total latency).
Snarling can detect human presence using an MLX90640 thermal camera mounted nearby. When connected, Snarling:
- Detects when someone arrives or leaves (binary presence)
- Tracks proximity zones (absent → approaching → present) for face expression changes
- Posts presence change events to the OpenClaw gateway so your agent knows you're there
- Gracefully degrades — if the camera isn't available, everything else works identically
The MLX90640 reads a 32×24 thermal grid at ~4 Hz. The ThermalSensor class (in thermal.py) runs as a daemon thread:
- Frame processing: Each frame computes ambient temperature, identifies warm blobs (≥3°C above ambient, ≥15 pixels, with aspect ratio filtering to reject oven heat plumes)
- Dual debounce: Fast path (2-frame) for face expressions and LED brightness, slow path (15-frame ≈ 3.75s) for presence callbacks and gateway events — avoids flicker on the display while keeping presence data stable
- Proximity: Calculated from blob size and warmth — larger, warmer blobs mean closer proximity
- Callbacks:
on_presence_change(was_absent, now_present, ambient_temp)andon_proximity_change(old_zone, new_zone, proximity, ambient_temp)fire on the slow debounce path,on_display_zone_change(old_zone, new_zone, proximity, ambient_temp)fires on the fast path for immediate face/LED reactivity
Each presence session (from arrival to departure) tracks:
- Dwell time: how long someone was present
- Proximity peak: highest proximity value during the session
- Zone flips: number of proximity zone transitions (approaching ↔ present)
- Approach time: seconds from first "approaching" zone to settled presence
On departure, this data is logged to /tmp/presence-log.jsonl as a compact JSON line with dwell_sec, prox_peak, and zone_flips fields. On presence_settled (60s of stable presence), the approach time and zone flips are included.
Snarling sends two types of presence events to the OpenClaw gateway's /environmental-event route:
Fired immediately when thermal detection confirms someone arrived or left:
{
"type": "presence_change",
"present": true,
"absent_duration": "3h20m",
"absent_duration_sec": 12000,
"timestamp": 1714588800
}present:truefor arrival,falsefor departureabsent_duration: human-readable string on return (e.g."45s","3m20s","2h15m"),nullon departureabsent_duration_sec: numeric seconds (only updated for absences ≥ 60s to avoid short-gap noise)timestamp: Unix epoch when the change was detected
Fired 60 seconds after confirmed arrival — means someone has been present long enough to be considered "settled":
{
"type": "presence_settled",
"absent_duration": "2h15m",
"absent_duration_sec": 8100,
"timestamp": 1714588860
}This is the signal the agent uses for "someone is home and staying" — useful for proactive check-ins, greeting messages, or adjusting notification behavior.
These events are routed to the agent via the OpenClaw Interaction Bridge plugin (see Configuration below). The plugin handles V1/V2 compatibility — presence_settled events without a trigger_reason field are treated as observation_report with trigger_reason: "presence_settled".
Proximity zone changes are not sent to the gateway — they're used internally by Snarling for face expressions and LED brightness only.
All presence events are logged to /tmp/presence-log.jsonl as compact JSON lines:
{"ts":1714588800,"type":"presence_change","p":1,"abs":12000}
{"ts":1714588860,"type":"presence_settled","abs":8100,"approach_sec":3.2,"prox_peak":0.85,"zone_flips":4}
{"ts":1714589200,"type":"presence_change","p":0,"dwell_sec":665.0,"prox_peak":0.92,"zone_flips":7}Fields: ts (epoch), type, p (1=present, 0=absent), abs (absent seconds), dwell_sec (departure only), approach_sec (settled only), prox_peak, zone_flips.
This log enables the environmental agent to build rolling statistics over time — average dwell times, nocturnal patterns, peak proximity trends — without needing to parse full event payloads.
| Setting | Where | Default | Description |
|---|---|---|---|
ENVIRONMENTAL_EVENTS_ENABLED |
snarling.py |
True |
Master switch — set to False to stop posting events |
ENVIRONMENTAL_SESSION_KEY |
Gateway env var | "" (empty) |
Routes presence events to a specific agent session. Empty = events acknowledged but dropped. Set to "agent:main:main" for the orchestrator, or "session:environmental" for a dedicated environmental agent |
- Oven heat plume: The double oven in the kitchen creates a rising heat column that the MLX90640 sees as a person-shaped warm blob (up to 70°C). Only happens when the oven is on. Temperature alone can't distinguish it from a person.
- Pi housing heat: The Raspberry Pi housing surface appears in the MLX90640's field of view (bottom-left of rotated frame, rows 12–17, cols 13–15) as a persistent warm zone (~30–40°C). Not a laptop base — it's the Pi case itself.
┌────────────┐ HTTP POST ┌────────────┐ button press ┌────────────┐
│ OpenClaw │ ───────────────── │ Snarling │ ────────────────┠ │ OpenClaw │
│ (plugin) │ /state (5000) │ Display │ webhook + WS │ Gateway │
│ │ ───────────────── │ + Buttons │ wake │ │
│ │ /approval/alert │ + Thermal │ │ │
│ │ ───────────────── │ + Mic │ ──────────────┠ │ │
│ │ /approval/alert │ │ /approval-cb │ │
│ │ (type: notify) │ │ ──────────────┠ │ │
│ │ │ │ /notification-cb │ │
│ │ │ │ │ │
│ │ │ ────────────────────────────────┠ │ │
│ │ │ /environmental-event │ │
│ │ │ (presence_change + settled) │ │
│ │ │ │ │
│ │ │ X button press: │ │
│ │ │ 🎙 arecord (local) ────────────┠ │ │
│ │ │ 20s WAV → plugin transcribes │ /transcribe │
└────────────┘ └───────────┘ └───────────┘
How it works:
- The OpenClaw Interaction Bridge plugin watches your agent's activity
- It POSTs state changes to Snarling's
/stateendpoint on port 5000 - It POSTs approval requests and notifications to Snarling's
/approval/alertendpoint (direct, no middleman) - Snarling updates the display, LED, and face expression in real time
- When you press A (approve/reveal) or B (reject/dismiss), Snarling POSTs the decision or feedback back to the OpenClaw gateway
- Snarling sends a WebSocket RPC wake to bypass the gateway's
requests-in-flightcheck - When the thermal sensor detects presence changes, Snarling POSTs to the gateway's
/environmental-eventroute - X button starts local audio recording (
arecord, 20s), then POSTs the WAV path to the plugin's/transcribe-and-replyendpoint for transcription and agent injection
| File | Purpose |
|---|---|
snarling.py |
Main creature — display rendering, face animations, button handling, Flask server, voice input, approval/notification queueing, thermal callbacks, environmental event posting, presence session tracking, data logging |
thermal.py |
ThermalSensor class — MLX90640 daemon thread, frame processing, blob detection, dual debounce (fast display / slow gateway), presence/proximity callbacks |
- Display: Pimoroni Display HAT Mini (320×240 IPS)
- Computer: Raspberry Pi 4 (recommended)
- Case: Argon One V2 (fits nicely, keeps it cool)
- Header Adapter: for getting the angle right on the display
- Thermal Camera: MLX plugs right into header
- StemmaQT Cable: need this cable to plug camera into the display for power and data
Buy a screen for your raspberry pi. Install the python display library specific to your screen. You will have to have openclaw adapt Snarling if you use a different screen.
git clone https://github.com/snarflakes/snarling.git
cd snarling
# Install dependencies
pip install flask pillow requests websocket-client mlx90640
# Copy the systemd service file to enable auto-start
sudo cp snarling.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable snarling.service
# To start now: sudo systemctl start snarling.service
# To run manually for testing (Ctrl+C to stop):
python snarling.pyThe service file handles auto-restart on crash/kill.
Snarling needs the openclaw-interaction-bridge plugin to receive state updates and send approval responses.
Add this to your agent's context (or system prompt):
You are now running with the OpenClaw Interaction Bridge plugin enabled.
- Bridge plugin installed at
~/.openclaw/extensions/openclaw-interaction-bridge- Snarling display hardware ready and polling for status
- State updates POST to
http://localhost:5000/state- Approval alerts POST to
http://localhost:5000/approval/alert
Check the display updates when you:
- Run a tool → shows processing
- Generate a response → shows communicating
- Wait 10 seconds → shows sleeping
- Request user approval → shows awaiting approval with the request on screen
Snarling runs a Flask server on port 5000:
| Endpoint | Method | Description |
|---|---|---|
/state |
POST | Set creature state (sleeping, processing, communicating, error) |
/approval/alert |
POST | Display an approval request or notification on screen |
/counts |
GET | Get lifetime approve/reject counts |
/health |
GET | Health check |
/status |
GET | Get current state, notification stack, and active notification details |
/presence |
GET | Get current thermal presence state (present, proximity, proximity_zone, ambient_temp, last_change) |
/environment |
POST | Update environmental state from external sources (rejected when thermal sensor is active) |
The approval server on port 5001 (
approval_server.py) has been removed. The plugin talks directly to Snarling on port 5000.
Push to development, merge through main.
git checkout development
git add .
git commit -m "feat: description"
git push origin development- Inspired by Pwnagotchi — the idea of a tiny creature that reacts to what's happening
- Born from Dustytext, a blockchain world-building experiment
- Built by Snar
