---
slug: "ai-desk-card"
source_type: "skill_md"
source_url: "https://cdn.jsdelivr.net/gh/op7418/ai-desk-card@main/SKILL.md"
repo: "https://github.com/op7418/ai-desk-card"
source_file: "SKILL.md"
branch: "main"
---
---
name: ai-desk-card
description: |
  Drive a physical e-ink desk card (M5Paper 540×960) sitting next to the
  user's monitor. Use whenever the user wants to:
    - show / push / display ANYTHING on their card / 卡片 / 副屏 / 墨水屏 /
      e-ink display / desk card / glanceable display / secondary display
    - set up the device for the first time (flash firmware, pair, provision
      Wi-Fi) — phrases like "刚拿到 M5Paper", "怎么装卡片", "first-time
      setup", "刷固件", "卡片没反应"
    - schedule recurring pushes / "每小时刷一次" / "工作时间显示日历" /
      "auto-refresh every 30 min"
    - configure what the card shows (weather, todos, calendar, inbox,
      PR queue, AI status, focus, scratch, deadlines, messages, now-playing,
      git-status, system, next-meeting, break-reminder, ai-tasks)
    - put the device to sleep / show business card / "息屏" / "睡眠"
  Single Skill, agent-agnostic: probes current state, then routes to the
  right sub-flow. Never asks "is the daemon running" — it checks.
trigger_keywords:
  - card
  - desk card
  - 卡片
  - 副屏
  - 墨水屏
  - e-ink
  - M5Paper
  - paper card
  - glanceable display
  - secondary display
  - ai-desk-card
  - 桌面卡片
  - dashboard card
allowed-tools:
  - Bash
  - Read
  - Write
  - Edit
---

# ai-desk-card — single Skill entry point

A 540×960 e-ink panel sitting next to the user's monitor. AI agents push
widgets to it; the daemon renders frames server-side and ships pixels over
Wi-Fi / USB / BLE. This Skill is the **only** thing an agent needs to call
— it auto-detects state and routes to the right flow.

## Step 1 — ALWAYS probe state first

Before doing anything, run the state probe. Do not ask the user "have you
done X" — find out by checking.

```bash
bash $SKILL_DIR/scripts/state.sh
```

(`$SKILL_DIR` is wherever this Skill is installed. If your agent runtime
sets `$CLAUDE_PLUGIN_ROOT`, use that. Otherwise use the repo root.)

Output is JSON with this shape:

```jsonc
{
  "hardware":   { "pio_installed": bool, "m5paper_usb": str|null },
  "firmware":   { "flashed": bool, "ours": bool, "version": str|null },
  "daemon":     { "running": bool, "pid": int|null },
  "transport":  { "connected": bool, "type": "BLETransport|SerialTransport|WiFiTransport|null" },
  "device":     { "alive": bool, "last_seen_seconds": int|null,
                  "active_transport": "Wi-Fi|USB|BLE|null",
                  "battery_pct": int|null, "uptime": str|null },
  "wifi":       { "provisioned": bool, "ip": str|null },
  "interests":  { "configured": bool, "path": str|null }
}
```

The most important field is `device.alive`. It tells you whether the
device has sent a status report in the last ~90 s. `transport.connected`
only says "daemon picked a transport class"; `device.alive` says "we're
actually hearing back from the device right now."

## Step 1.5 — Identify the device profile

The Skill supports two devices with different panels + daemons:

- **M5Paper V1.1** (540×960 grayscale, GT911 touch, BLE pair) — original
- **M5Paper Color** (600×400 Spectra 6 color, 3 physical buttons, audio + SHT40) — new in v0.10

If GET `/heartbeat` returns `device_status.device == "M5PaperColor"`
(via `color_daemon.py` running), use the **Color path** described in
[flows/08_paper_color.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/08_paper_color.md):

- env: `pio run -e paper-color`
- daemon: `python3 daemon/color_daemon.py --device-ip <IP>`
- Wi-Fi provision: Serial JSON `cmd:wifi_set` (no BLE pairing UX)
- slot names: `top-left / top-right / bottom-left / bottom-right`
- extra widgets: `ambient` (SHT40 temp+humid)
- physical buttons: 顶=sleep / 下左=refresh / 下中=settings

Otherwise (default) use the **V1.1 path** through the routing table below.

The two daemons can't run on the same port simultaneously — pick one
based on which device is in front of the user. The Skill flows below
work for V1.1 unless explicitly noted.

## Step 2 — Route based on state

Walk the decision tree in this order. First mismatch wins; fix it, then
re-probe.

| Condition | Next action | Detail flow |
|---|---|---|
| `firmware.flashed == false` AND no `device.alive` | First-time hardware setup | [flows/01_install.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/01_install.md) |
| `daemon.running == false` | Start the daemon | `bash $SKILL_DIR/plugin/scripts/start.sh` |
| `device.alive == false` AND `transport.connected == false` | Device unreachable — could be asleep, off, BLE not paired. Tell user, suggest physical wake (tap rotary / plug USB) | [flows/02_transport.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/02_transport.md) |
| `device.alive == false` AND `transport.connected == true` (daemon connected something but no status_report in 90s) | Device transport up but not responding — restart daemon, then probe | [flows/02_transport.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/02_transport.md) |
| `wifi.provisioned == false` (and user wants always-on or battery mode) | Provision Wi-Fi | [flows/03_wifi.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/03_wifi.md) |
| `interests.configured == false` AND user just asked for "auto-refresh" or "定时推送" | Ask about interests, write `~/.ai-desk-card/interests.yaml` | [flows/04_interests.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/04_interests.md) |
| `device.alive == true` + user said "push X" | Build widget JSON, POST to daemon | [flows/05_push.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/05_push.md) |
| `device.alive == true` + user said "schedule" / "每 N 分钟" / "auto" | Set up scheduled push | [flows/06_schedule.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/06_schedule.md) |
| `device.alive == true` + user said "sleep" / "息屏" | Push business card + deep sleep | [flows/07_sleep.md](https://github.com/op7418/ai-desk-card/blob/HEAD/flows/07_sleep.md) |

Always tell the user which step you're on. Don't operate silently.

## Step 3 — Push a widget (the hot path)

When state is OK and user asks to show something:

```bash
curl -sf -X POST "${CARD_DAEMON_URL:-http://127.0.0.1:9877}/widget" \
  -H 'Content-Type: application/json' \
  -d @- <<'JSON'
{
  "slot": "top-left",
  "type": "weather",
  "data": { "city": "Beijing", "temp_c": 22, "icon": "sun", "summary": "晴" }
}
JSON
```

- **slot**: string. Layout is 2-1-1 not 2x2 —
  `top-left` (270×280) · `top-right` (270×280) · `middle` (540×340) ·
  `bottom` (540×280) · `full` (540×960, takes over the whole screen).
- **type**: one of 16 — see `plugin/skills/card-widget/schemas/` for full
  JSON schemas with examples.
- Wi-Fi: response in ~0.2 s. USB: 1–32 s. BLE frame-data: broken — small
  commands only.

Full per-widget schema + theme reference:
[plugin/skills/card-widget/SKILL.md](https://github.com/op7418/ai-desk-card/blob/HEAD/plugin/skills/card-widget/SKILL.md)

## Step 4 — When to suggest scheduled pushes

If the user's request implies recurrence ("keep my calendar updated",
"check email every hour", "show me today's todos throughout the day"),
**don't** just push once. Instead:

1. Confirm the cadence + which widgets they want
2. Write/update `~/.ai-desk-card/interests.yaml` (see flow 04)
3. Set up the schedule using your agent's native loop primitive:
   - Claude Code: `/loop 30m` or `ScheduleWakeup`
   - Codex / Gemini: equivalent scheduling
   - Fallback: cron line via `plugin/skills/card-refresh/scripts/refresh_loop.sh`

The Skill provides the *what* (interests + push) — your agent provides the
*when* (loop primitive).

## Constraints to never violate

- **No silent operations.** Every sub-step gets a one-line update to the user.
- **No retry loops.** If something fails, surface the diagnostic and stop.
- **No assuming state.** Always re-probe after fixing something.
- **No font escape hatches.** The CJK TTF doesn't include ▢ ▶ ✎ ♪ ↑ ↓ ● ○
  — … °. Use the safe glyph set documented in
  [plugin/skills/card-widget/SKILL.md](https://github.com/op7418/ai-desk-card/blob/HEAD/plugin/skills/card-widget/SKILL.md).
- **Wi-Fi preferred over USB / BLE.** 0.2 s vs 1-32 s vs broken.

## Hardware: what the user needs

- M5Paper V1.1 (~¥600 / $90) — primary target
- USB-C data cable (one time, for flashing)
- Optional: USB-C charger for always-on Wi-Fi mode

## What this Skill is NOT

- Not a Claude-Code-only plugin. The `plugin/` directory is provided for
  CLIs that consume slash commands, but this `SKILL.md` is the agent-agnostic
  entry point.
- Not a cloud service. Everything runs on the user's machine (daemon at
  `127.0.0.1:9877` by default) + the device's local Wi-Fi.
- Not a generic e-ink renderer. The widgets, themes, and renderer all
  target this specific device + grid.
