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.
Resources
15Install
npx skillscat add op7418/ai-desk-card Install via the SkillsCat registry.
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 $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:
{
"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:
- 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 |
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 |
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 |
wifi.provisioned == false (and user wants always-on or battery mode) |
Provision Wi-Fi | 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 |
device.alive == true + user said "push X" |
Build widget JSON, POST to daemon | flows/05_push.md |
device.alive == true + user said "schedule" / "每 N 分钟" / "auto" |
Set up scheduled push | flows/06_schedule.md |
device.alive == true + user said "sleep" / "息屏" |
Push business card + deep sleep | 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:
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
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:
- Confirm the cadence + which widgets they want
- Write/update
~/.ai-desk-card/interests.yaml(see flow 04) - Set up the schedule using your agent's native loop primitive:
- Claude Code:
/loop 30morScheduleWakeup - Codex / Gemini: equivalent scheduling
- Fallback: cron line via
plugin/skills/card-refresh/scripts/refresh_loop.sh
- Claude Code:
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. - 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 thisSKILL.mdis the agent-agnostic
entry point. - Not a cloud service. Everything runs on the user's machine (daemon at
127.0.0.1:9877by default) + the device's local Wi-Fi. - Not a generic e-ink renderer. The widgets, themes, and renderer all
target this specific device + grid.