Skip to main content

trmnl-dev

Build, update, and operate TRMNL e-ink private plugins end to end — webhook data updates and merge variables, Liquid markup with Framework 3.1, the per-plugin MCP server (stateless JSON-RPC), an offline preview/screenshot harness, and battery-aware refresh cadence. Use when working with TRMNL devices (OG or TRMNL X), trmnl.com private plugins, webhook payloads, e-ink dashboard markup, the TRMNL MCP API, or debugging clipped, empty, or stale TRMNL renders.

ソース情報

リポジトリ
0juano/agent-skills
ソースの最終更新活動
2026年7月2日 14:44
検出された SKILL.md の言語
英語
スター
0
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

ファイルエクスプローラー
4 ファイル

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
trmnl-dev
description
Build, update, and operate TRMNL e-ink private plugins end to end — webhook data updates and merge variables, Liquid markup with Framework 3.1, the per-plugin MCP server (stateless JSON-RPC), an offline preview/screenshot harness, and battery-aware refresh cadence. Use when working with TRMNL devices (OG or TRMNL X), trmnl.com private plugins, webhook payloads, e-ink dashboard markup, the TRMNL MCP API, or debugging clipped, empty, or stale TRMNL renders.
license
MIT
metadata
{"author":"0juano","version":"1.0.0"}
# TRMNL Dev Field-tested knowledge for building and operating TRMNL private plugins: what the docs say, plus the gotchas the docs don't mention (rendering geometry that clips, MCP quirks, CSS cascade layers, payload byte limits). ## Mental model TRMNL is server-rendered e-ink. The device polls; nothing is pushed to it. ```text data source ──► webhook POST ──► merge variables ─┐ ├─► TRMNL renders PNG ──► device wakes, markup editor / MCP ──► Liquid template ──────────┘ downloads image, sleeps ``` There are **two independent update paths** — never confuse them: 1. **Change what the screen says** → generate JSON → POST to the plugin webhook. The webhook updates merge variables only; it never changes markup. 2. **Change how the screen looks** → edit Liquid markup → save → preview → force refresh. The markup editor never fetches new data by itself. A webhook post triggers a server-side re-render in ~2 s, but the physical device only shows it on its next wake (default every 15 min). Server-side posts do **not** wake the device and cost no battery. ## Keys and identifiers (don't mix them up) | Credential | Looks like | Where found | Used for | |---|---|---|---| | Device API key | opaque token | Devices → Edit | `access-token:` header on `/api/display`, `/api/current_screen` | | Account API key | `user_…` | Account settings | `Authorization: Bearer` on account API (devices, playlists) | | Plugin Settings UUID | UUID in webhook URL | generated on first plugin **Save** | webhook POSTs to `/api/custom_plugins/<uuid>` | | Plugin MCP key | `ps_mcp_…` | plugin settings page, right side after Save | `https://trmnl.com/mcp?api_key=…` | The MCP key is **per-plugin**: it can read/write that one plugin's markup, settings, and merge variables — no device, playlist, battery, screenshot, or preview control, and it will not reveal the plugin UUID or webhook URL. For device/playlist work use the account API instead. Keep all real values in environment variables or an OS keychain, never in committed files. Treat the webhook URL itself as a secret — anyone holding it can write to your screen. ## Webhook data updates Payload shape — everything lives under a top-level `merge_variables`: ```bash curl "https://trmnl.com/api/custom_plugins/${TRMNL_PLUGIN_UUID}" \ -H "Content-Type: application/json" \ -X POST \ -d '{"merge_variables":{"headline":"Hello TRMNL","items":[{"time":"09:00","title":"Standup"}]}}' ``` Hard limits (standard tier): **2 KB body, 12 posts/hour** (TRMNL+: 5 KB, 30/hour). Faster sends return `429`. Plugin "Debug Logs" mode temporarily raises the limit during development. Rules that keep you out of trouble: - Send only reduced display text (`{"time","title"}`), never raw upstream API output — you'll blow the byte limit and leak data you didn't mean to send. - Enforce a local guardrail *below* the real limit (e.g. 1900 bytes for a 2 KB tier) and make the generator **degrade instead of fail**: shed rows and render an honest `+ N more` label rather than throwing or silently cropping. - Keep template loop limits ≥ generator caps — a template `limit:` lower than what the generator sends silently drops items. - Stateful updates: `merge_strategy: "deep_merge"` patches nested keys; `merge_strategy: "stream"` + `stream_limit` appends to top-level arrays with bounded growth. Stored data must still fit the size limit. - `GET` the same webhook URL to fetch currently stored merge variables. ## Markup essentials Full geometry/CSS details with the bugs they cause: [references/framework.md](references/framework.md). The non-negotiables: - **Author at 800x480 always.** TRMNL X renders 1040x780 at 4-bit (16 grays) but the framework scales your 800x480 markup automatically (`scale_factor: 1.8`). Never hardcode the native resolution. - One `<div class="layout">…</div>` per view, optional sibling `<div class="title_bar">`. Never nest `.layout` inside `.layout`. Do **not** paste `.screen`/`.view` wrappers into the TRMNL editor — those are for offline preview only. - The framework sizes `.layout` to `screen-h − 2×gap` (460 px inside the 10 px bezel; less with a `title_bar`). If you use a custom root element instead of `.layout`, it must copy that height: `height: calc(var(--screen-h, 480px) - var(--gap, 10px) * 2)`. `height: 100%` renders 480 px inside a 460 px box and **clips the bottom ~20 px**. - Omitting the `title_bar` is legal and often right (it eats ~35 px). Saving markup without one triggers a "Missing title_bar include" warning — expected; ignore it. - `plugins.css` uses CSS cascade layers: a layered `!important` beats your unlayered `!important` and inline styles. You cannot out-specify the framework's sizing — adopt its variables (`--screen-h`, `--gap`, `--full-w`) or its classes instead of fighting it. - Custom `<style>` blocks are allowed in private-plugin markup, but TRMNL's in-app AI agent enforces "no custom styles" and will flatten your design if asked to "improve" it. Keep a local file as the source of truth. - E-ink legibility: trust the physical device over any browser preview. When in doubt — bigger type, 2 px rules (1 px hairlines vanish at distance), `font-feature-settings: "tnum" 1` for aligned times, and for screens read at a distance don't shrink body type below ~19 px — use two columns instead. ## Per-plugin MCP Full tool table, curl recipes, and every gotcha: [references/mcp.md](references/mcp.md). The essentials: - Endpoint `https://trmnl.com/mcp?api_key=…` is **stateless JSON-RPC over HTTP** — call `tools/list` / `tools/call` directly, no initialize handshake. - Tool names are **PascalCase with a `Tool` suffix** (`MarkupsWriteTool`), not the in-app agent's snake_case names. snake_case returns `Tool not found`. - `MarkupsWriteTool` takes `{size, content}` — passing `markup` instead of `content` returns an unhelpful `-32603 Internal error`. - MCP **writes markup blind** (no screenshot/preview tool exists). Safe write flow: read live → back up → write → read back → unescape → compare with your local source. Verify visually in the editor previewer or offline harness. ## Offline preview (no account round-trips) Render your Liquid template locally with sample data inside TRMNL's official CSS/JS wrapper, then screenshot the `.screen` element (exactly 800x480). Complete recipe and pitfalls: [references/preview.md](references/preview.md). Iterate offline until the layout is right; only then push markup. Include worst-case sample payloads (max rows, longest titles) — busy days are where layouts break, not calm ones. ## Battery and refresh cadence - Plugin refresh (server) and device wake (battery) are **separate**. Webhook posts are free; device wakes cost battery (WiFi + fetch + e-paper refresh). - Sleep Mode is the biggest easy win: sleeping 22:00–06:00 at a 15-min cadence avoids ~32 wakes/day. - Match cadence to content: dashboards fed by daily/hourly data rarely need 15-min refresh all day. A sane profile: 15–30 min during the morning rush, 60 min midday, 30–60 min in the evening, sleep overnight. - `/api/display` (device key) **advances the playlist** — don't poll it for monitoring; use `/api/current_screen` sparingly instead. ## Troubleshooting | Symptom | Fix | |---|---| | Preview renders empty | POST a tiny payload first; confirm strategy = Webhook; payload under `merge_variables`; plugin saved once (UUID exists) | | Bottom of screen clipped | custom root missing the `calc(var(--screen-h) - var(--gap)*2)` height; or a `title_bar` you forgot to budget for | | Physical device looks worse than preview | trust the device: bigger type, fewer display fonts, explicit grid columns, thicker rules | | Webhook `429` | over posts/hour cap — batch updates; enable Debug Logs during dev | | Items missing from render | template `limit:` below what the payload contains; or payload exceeded the byte cap and the generator shed rows | | MCP `Tool not found` | you used snake_case; use PascalCase + `Tool` suffix | | MCP `-32603 Internal error` on write | you passed `markup`; the argument is `content` | | MCP readback never matches local file | the envelope escapes `\n`/`\"`/`\\` — unescape before diffing | | Editor save "Missing title_bar include" warning | expected when intentionally omitting the title bar; ignore | ## Doc links - Docs index: https://docs.trmnl.com/go/llms.txt - Webhooks: https://docs.trmnl.com/go/private-plugins/webhooks.md - Templates: https://docs.trmnl.com/go/private-plugins/templates.md - Framework 3.1: https://trmnl.com/framework/docs/3.1/v3_overview.md - TRMNL X guide: https://trmnl.com/framework/docs/3.1/trmnl_x_guide - Native plugin markup examples: https://github.com/usetrmnl/plugins/
GitHubで見る