Firmware ui
Skill Attaky-Module/Attaky-Firmware-Builder/skills/firmware-ui
This skill turns an approved firmware spec into a UI design package — visual style tokens (palette, typography, spacing), per-screen layouts, and sprite/icon definitions — that `firmware-codegen` can turn into rendering code, plus an HTML preview auto-opened in the user's browser so they see every screen at panel resolution. Output is three Markdown files (`style.md` / `screens.md` / `sprites.md`) plus `preview.html`, written to `<workspace>/ui/`. Use when the user has an approved spec and says "design the UI", "make the screens look good", "pick a visual style", "draft the layouts", "design the sprites", "the interface is ugly", or hands off from firmware-spec. Optional stage — concepts that don't need bespoke UI (no screen, or text-only output) can skip directly from spec to codegen. When present, `firmware-codegen` reads `<workspace>/ui/` alongside `<workspace>/spec/` to implement the design. Hands off to firmware-codegen.From its SKILL.md
npx -y skills add Attaky-Module/Attaky-Firmware-Builder --skill firmware-uiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
27.5 KB, as published. Nobody here has run it
Firmware UI
This is the optional stage 3.5 of the attaky-firmware-builder chain,
sitting between firmware-spec and firmware-codegen. The user has
an approved spec describing what screens exist and what each one
shows. You produce a UI design package describing how those
screens look — the palette, typography, layout grid, sprite
appearance — at a level of detail firmware-codegen can compile into
rendering code.
The user is non-technical. They tell you "make the screens not look ugly". You handle visual decisions silently — pick a coherent direction, build a small token system, place every UI element on the grid, define every sprite as either primitive composition or pixel bitmap.
Operating Principles (non-negotiable)
-
The spec is the input contract. Read
<workspace>/spec/requirements.mdand the rest ofspec/. Every screen, every state, every sprite the spec implies gets a UI definition. Don't invent screens or features the spec doesn't have. -
Four output files, no more. Three Markdown specs —
ui/style.md(tokens),ui/screens.md(per-screen layouts),ui/sprites.md(sprite / icon / decorative-element appearance) — plus oneui/preview.html(a self-contained HTML + SVG file that renders every screen at panel resolution using the actual palette and sprite primitives, so the user SEES the design instead of reading ASCII grids). The skill takes responsibility for opening the preview — once the initial draft lands on disk, the skill runs the platform-appropriate open command (see §4 "Initial disk write- auto-open" for the per-platform list) so the user's default
browser pops up showing it. Subsequent iterations rewrite the
file on disk; the user refreshes the open tab. The three Markdown files
are the codegen-readable contracts; the
preview.htmlis a derived human-review artifact regenerated from them on every iteration (codegen ignores it). Keep each focused; don't overflow into adjacent files.
- auto-open" for the per-platform list) so the user's default
browser pops up showing it. Subsequent iterations rewrite the
file on disk; the user refreshes the open tab. The three Markdown files
are the codegen-readable contracts; the
-
Work within the hardware envelope. TFT panel resolution, bit-depth, fonts available in the codegen library, and CPU cycles for redraw all set hard limits. Read
../../shared/hardware-catalog.mdfor the panel model + font library, and stay within it. Don't propose 24-bit gradients on a 16-bit RGB565 panel. Don't propose smooth alpha-blended sprites when the codegen library only provides 1-bit / 16-bit blits. The implementation-side display constraints the design must stay compatible with (a fixed panel orientation, RGB565 byte order, and — for touch UIs — the touch coordinate model) are in../../shared/display-and-lvgl-gotchas.md. -
One question at a time, and only when essential. Most UI designs need zero clarifying questions: the spec already tells you what the screens are. Ask only if the user's idea has materially ambiguous visual direction (retro pixel-art vs minimalist modern vs playful illustrated) AND none of those is implied by the concept doc tone or naming.
-
Codegen-readable, not Figma-readable.
firmware-codegenwill readui/and emit C++ that draws the design. Every spec must be precise enough to translate one-to-one into draw calls. Use pixel-level coordinates, exact RGB565 hex, exact font names from the catalog, sprite primitives expressed asfillCircle/fillRect/drawBitmapinvocations or near-equivalents. Vague design language ("a nice cat", "warm tones", "easy to read") is unusable.
Workflow
Collaboration mode
Read the Collaboration mode field from the spec package (any of
parts-list.md, module-combination.md, requirements.md,
dev-plan.md — they match), else concept.md, else (porting lane)
port/plan.md / port/survey.md; default autonomous if absent.
See ../../shared/collaboration-mode.md.
In step-by-step, before this stage's work, ask this stage's functional questions (which screens matter most, what the home screen shows), show the plan, execute on OK. In autonomous, just do the stage and stop only at the essential gates. Never ask the user a technical question in either mode.
In step-by-step, fold this stage's §3 clarifying-question trigger into the pre-stage check-in — one question per turn, the check-in comes first. In autonomous, only the §3 trigger applies (unchanged behavior).
1. Find the spec
The UI stage requires an approved spec as input. Locate it:
-
Look in
<workspace>/spec/for the four files written byfirmware-spec(parts-list.md,module-combination.md,requirements.md,dev-plan.md). -
If found, read all four.
-
If
<workspace>/spec/is missing, ask the user one short question:"I can't find a
spec/folder in this workspace. Give me a path, or runfirmware-specfirst to produce one. The UI stage needs the spec's screen list to design against."Wait for either an absolute path or "go run spec first".
The folder containing spec/ is the workspace folder. The ui/
output goes inside that workspace.
If <workspace>/references/ exists, scan it too — user-dropped
visual references ("make it look like this") and earlier skills'
durable notes are design input. Catalog precedence is unchanged: the
panel model, fonts, and pixel format come from the catalog only.
Write your own durable findings, if any, to
<workspace>/references/YYYY-MM-DD-<topic>.md with a one-line
provenance header (NOT this skill's own bundled references/
folder); scratch output goes to <workspace>/tmp/.
Retrofit case: prior codegen output present
If <workspace>/source/ already exists from a firmware-codegen
run that didn't have ui/ to read, that earlier codegen invented
its own UI contract (default menu actions, default sprite shapes,
default text). Your ui/ will replace those defaults — but
codegen-emitted contracts that rest of the firmware depends on
(e.g. a MenuAction enum used by main.cpp's input dispatcher)
may have a different structural shape than your design.
When this happens — prefer your design as source of truth, not
the existing source/. The spec is the contract; ui/ realises
that contract; source/ is a derived implementation. Tell
firmware-codegen (the next stage) to regenerate the contract
files (MenuAction-style enums, screen-state machines) from your
new ui/ instead of preserving the placeholder values from the
earlier codegen run. Surface any structural delta in chat so the
user knows the input dispatcher will be rewritten:
"The earlier codegen run defined 5 menu actions; my design has 4 — codegen-fix-mode will drop the 5th when it regenerates."
If preserving the earlier shape matters (e.g. it's calibrated against existing user data), bend the design to match instead.
2. Sanity-check the spec
Glance at each spec file's metadata block:
-
All four
Status: approved→ proceed normally. -
Any one is still
Status: draft→ warn the user once:"Heads up: the spec is still marked draft. I can design UI against it, but if the spec changes the design will need a redo. Want me to proceed?"
If they say go, treat it as approved for UI's purposes.
Then enumerate the screens / states / persistent visual elements from
spec/requirements.md §1 (functional features) and the interaction
flow in concept.md §3 (software flow — the requirements file's own
§3 is the resolved-open-questions table, not the flow). Build an
internal list — this is your design surface.
3. Decide if a clarifying question is necessary
Most of the time the answer is no. The concept's tone (a "cat that visits over LoRa" suggests playful illustrated; a "buddy locator on LoRa" suggests minimalist outdoorsy; a "tank battle" suggests retro pixel) usually pins the visual direction within one paragraph.
Ask only when the spec genuinely admits multiple coherent directions AND the choice changes the sprite system materially. When you ask, ask one question, two-or-three options with one recommended:
"Three coherent visual directions for this firmware. Which one?
(A) Retro pixel-art — 8-bit / 16-bit Famicom-era vibe. Bold primary colors, pixel-perfect sprites, blocky fonts. Strongest fit for game-type concepts. (B) Minimalist modern (recommended) — 4-color flat palette, generous whitespace, slim sans-serif font, geometric icons. Strongest fit for utility / status / locator concepts. (C) Playful illustrated — soft shapes, pastel palette, rounded sprites with a slight cartoon feel, friendly serif. Strongest fit for pet / companion / lifestyle concepts.
Pick A, B, or C — or describe a fourth direction in plain language."
After they answer, draft. Don't pile on a second question. If after the draft you still need something, ask a follow-up then.
4. Draft the UI package
Tell the user briefly what you're about to do — one short sentence:
"Drafting style + screens + sprites now — read through and tell me what to change."
Then:
- Read
../../shared/hardware-catalog.mdfor the panel model + the rendering library's font list. - Read
references/style-tokens.mdfor the tokens-file template. - Read
references/screen-template.mdfor the per-screen layout format. - Read
references/sprite-template.mdfor the sprite definition format. - Read
references/preview-template.mdfor thepreview.htmlscaffolding (HTML shell + SVG screen-rendering convention + sprite → SVG primitive mapping). - Read
references/example-ui.mdto calibrate density and tone — it is the LoRa Hike Buddy Locator UI package, paired with the brainstorm + spec worked examples and realised by the codegen worked example'sdisplay.cpp. Note it is a deliberately sprite-light utility UI (one parametric arrow, the rest text); the cat / pet snippets in the four templates above illustrate the richer Form-A / Form-B sprite system for character concepts. Match whichever end of that range your concept sits at.
Then fill the four files (three Markdown + the HTML preview). The user has no homework — every section is your prose.
ui/style.md
Visual tokens shared across every screen:
- Direction: one paragraph naming the chosen direction (A / B / C from §3 or whatever the user described) and the design rationale in two or three sentences.
- Palette: 4–8 named RGB565 colors with hex codes
(
#RRGGBBand the 16-bit0xNNNNform for direct use in the rendering library). Each color has a one-line semantic role (primary,surface,text-muted,accent, etc.). Stay within what an RGB565 panel can actually display (no 24-bit subtleties). - Typography: 2–3 font slots from the catalog's rendering library
(e.g.
LOAD_FONT2/LOAD_FONT4/LOAD_GLCDfor TFT_eSPI). Each slot has a use-case (title,body,numeric-readout) and a pixel size. - Spacing scale: 4 step sizes (e.g. 4 / 8 / 16 / 24 px). Used for padding, gap, and margin values across screens.
- Density: a single line stating how dense layouts should be — "info-dense (every pixel earns its place)" vs "spacious (let whitespace breathe)" — drives whether screens are packed or airy.
ui/screens.md
One section per screen the spec defines:
### Screen: <name>
- **When shown**: <which state / event triggers it>
- **Layout grid**: <text grid showing regions, e.g.>
┌──────────────────────────────────────┐
│ status bar (h=20) │
├──────────────────────────────────────┤
│ │
│ main canvas (sprite + interaction) │
│ │
├──────────────────────────────────────┤
│ footer hint (h=14) │
└──────────────────────────────────────┘
- **Regions**:
- `status_bar` (0,0 → 240,20) — palette: surface; content: hunger
icon + value, health icon + value, age stage icon, battery %.
- `main_canvas` (0,20 → 240,206) — palette: background; content:
cat sprite at (60, 80) + furniture at (...).
- `footer_hint` (0,206 → 240,220) — palette: surface; content:
one-line text using `body` font.
- **Transitions**: <if this screen has multiple modes that change
layout — sleeping cat vs awake cat vs sick cat — list the variants
and what changes>
Use ASCII-art grid blocks for the layout sketch. Use exact pixel coordinates inside each region.
ui/sprites.md
Each sprite (cat poses, furniture, icons, status indicators) gets a section. Two valid sprite forms:
Form A — primitive composition (use for medium / large sprites):
### Sprite: cat-idle (40 × 48 px, baseline at y=48)
| Step | Primitive | Args |
|---|---|---|
| 1 | fillCircle | (cx=20, cy=30, r=18, color=palette.fur) |
| 2 | fillCircle | (cx=20, cy=12, r=10, color=palette.fur) |
| 3 | fillTriangle | (...) — left ear |
| 4 | fillTriangle | (...) — right ear |
| 5 | fillCircle | (cx=15, cy=10, r=2, color=palette.eye) |
| 6 | fillCircle | (cx=25, cy=10, r=2, color=palette.eye) |
Composition table reads top-to-bottom (later steps draw over earlier steps — z-order is implicit).
Form B — pixel bitmap (use for small icons ≤ 16 × 16 px):
### Sprite: heart (8 × 8 px)
`uint16_t bitmap[]` (palette.accent for set, palette.transparent for unset):
. # # . . # # .
# # # # # #
# # # # # #
# # # # # #
. # # # # # # . . . # # # # . . . . . # # . . . . . . . . . . .
For animation (multiple poses of the same sprite), enumerate each
pose as its own sprite section and note the inter-pose transition
trigger ("on user shake → switch to cat-petted for 1 s, then back").
ui/preview.html
Self-contained HTML + SVG file that renders every screen at panel resolution. No build step, no JS framework, no server, no external CSS — one file. The skill writes it to disk and auto-opens it (see "Initial disk write + auto-open" below) so the user doesn't have to navigate to it. Plain Web Standards (SVG 1.1 + minimal CSS) so it works in Chrome / Safari / Firefox / Edge without surprises.
Structure (per the scaffold in references/preview-template.md):
<head>block: title (Concept name — UI Preview), inline<style>with the palette CSS-vars derived fromstyle.md §2(--bg,--surface,--text, ...), grid-layout CSS for the screen tile gallery, plain font-familymonospacefor any text rendered inside the SVGs (mimics the chunky pixel-fonts on TFT_eSPI without shipping a font).<body>block:<h1>with the concept name + "UI Preview".<p>one-line note: "Each tile below is one screen at native panel resolution. Hover for the screen name."- One
<section>per screen fromscreens.md. Each section:<h2>the screen name.- For screens with sub-state variants, one inline
<svg>per variant, captioned with the variant name (e.g. "Idle awake", "Sleeping", "Sick", "Visiting out", "Hosting visitor"). - For static screens, one inline
<svg>.
- Each
<svg>is exactly the panel resolution (viewBox="0 0 <W> <H>",width="<W*2>"height="<H*2>"so it shows at 2× on desktop without losing pixel sharpness).
- One screen renders as:
<rect>for each region's background fill using palette CSS-vars +<text>for region label text + sprite primitives translated to<circle>/<rect>/<polygon>/ pixel-bitmap<rect>arrays.
Sprite → SVG primitive mapping (the renderer mapping the skill uses):
sprites.md form | SVG element |
|---|---|
fillCircle(cx, cy, r, color) | <circle cx="cx" cy="cy" r="r" fill="var(--color-token)"/> |
fillRect(x, y, w, h, color) | <rect x="x" y="y" width="w" height="h" fill="var(--color-token)"/> |
fillTriangle((x1,y1),(x2,y2),(x3,y3),color) | <polygon points="x1,y1 x2,y2 x3,y3" fill="var(--color-token)"/> |
drawLine((x1,y1),(x2,y2),color) | <line x1="x1" y1="y1" x2="x2" y2="y2" stroke="var(--color-token)" stroke-width="1"/> |
| Form B bitmap (8 × 8 grid) | one <rect width="1" height="1"> per set pixel, inside a <g> placed via transform="translate(canvasX, canvasY)" |
Text rendering (status-bar values, footer hints) uses <text>
with font-family: monospace and font-size matching the
typography slot's pixel size from style.md §3. This is an
approximation — TFT_eSPI's bundled fonts aren't real monospace, but
the layout proportions are close enough for review purposes; the
preview is for layout / palette sanity, not exact font fidelity.
Multi-state variant rendering: for screens whose screens.md
section has a "State variants" table (e.g. Home → Idle / Sleeping /
Sick / Visiting / Hosting), render each variant as a separate SVG
tile under the same <h2>. User can scroll the page and see all
states at once.
File size budget: keep preview.html under 200 KB. SVG
primitives are tiny; even 10 screens × 5 variants × ~30 primitives
each lands around 50 KB. Don't embed images or web fonts.
Initial disk write + auto-open
All four files land on disk as soon as the initial draft is complete. Real-world UI specs run hundreds of lines (production-scale: e.g. 7 screens × 5 variants + 16 sprites = ~600 lines of markdown); keeping them inline-only during iteration buries the chat. Writing to disk lets the user open them in their editor for in-place review while the chat stays focused on what changed and why.
After drafting all four files in memory, do this in one step before handing control back to the user for §5 iteration:
- Resolve
<workspace>(the folder that containsspec/). - Create
<workspace>/ui/if missing. - Write all four files to disk:
<workspace>/ui/style.md<workspace>/ui/screens.md<workspace>/ui/sprites.md<workspace>/ui/preview.html
- Auto-open the preview. Always quote the path (workspace folders
may contain spaces). Use the platform-appropriate command:
- macOS:
open "<workspace>/ui/preview.html" - Linux:
xdg-open "<workspace>/ui/preview.html" - Windows (Bash via Git-Bash / WSL):
cmd.exe /c start "" "<workspace>/ui/preview.html" - Unknown: try
openfirst, fall back toxdg-open.
- macOS:
- If every open command fails (headless box, SSH session with no
GUI, missing helper), don't panic. Print the absolute file path
to chat and tell the user: "Auto-open failed in this environment
— open this file manually in a browser:
<absolute path>." The skill still proceeds to §5 iteration; the user can scp the file to a workstation if needed. - Tell the user in chat: the four absolute paths, the chosen visual direction (one sentence), the screen index, and the sprite index. The full markdown bodies live on disk now — don't re-paste them into chat. Iteration commentary in §5 references sections by name (e.g. "I'm changing the cat-curl tail wrap to sit lower"), the user opens the file in their editor to read the surrounding context.
All four files are still drafts at this point — the metadata
block in each Markdown reads Status: draft. They flip to
Status: approved in §7 after the user approves.
5. Iterate
The user will say "the cat is too fat" / "more contrast on the status bar" / "swap accent to red". For each change:
- Identify which file(s) the change touches (style.md / screens.md / sprites.md).
- Edit the on-disk file directly using your Edit tool — surgical patches, not rewrites. Each iteration is one or more Edit calls, not a Write that replaces the whole file.
- In chat, tell the user what changed and why, by section name and a one-line summary. Example: "sprites.md §cat-curl — tail wrap moved 4 px lower (cy=20 → cy=24) so the curve reads as wrapping around the body, not floating beside it." Don't paste the full edited file; the user opens it in their editor if they want the surrounding context.
If a change cascades (e.g. swapping the primary palette color ripples through every screen), enumerate every affected file in the same chat round + apply edits to all of them, then move on.
Re-render the preview on every iteration. Every change to the
three Markdown files invalidates preview.html. After applying user
edits, regenerate the preview file in memory + overwrite the file
on disk + tell the user "preview updated — refresh the browser tab
to see the change." Don't ship preview.html that's stale relative
to the Markdown specs.
Don't re-run open on every iteration — the browser tab is already
showing the file from §4's initial open, and open would spawn a
new tab on each call (annoying). Only re-run open if the user
explicitly says they closed the tab and want it back.
Stay decisive. If they ask "warmer or cooler primary?", recommend with one sentence of reasoning. Don't volley.
6. Approval gate
Watch for "looks good" / "approve" / "ship it" / "next" / "yes" / "OK". Approval covers all four files at once.
7. Save and hand off
When approval lands, all four files are already on disk from §4 + §5. Approval just flips their status:
- For each of the three Markdown files, edit the metadata block
in place:
Status: draft → approved(top frontmatter quote AND the## Statusblock at the bottom),Approved: pending → yes,Last updated: <today's date>. - Re-render
preview.htmlfrom the just-approved drafts and overwrite the file on disk. Do this unconditionally — even if you think the on-disk preview already matches. Approval can land right after a verbal exchange where no §5 rewrite happened, or right after the §4 initial draft with no §5 at all. A guaranteed final write removes the drift risk entirely; the cost is one extra file write. Do not re-runopen(the tab is already showing it; user refreshes with Cmd-R). - Tell the user the four absolute paths and that the preview
window is the one they've been looking at since §4. Then hand
off: "Run
firmware-codegennext — it readsspec/+ui/and produces a project that implements this design."
The folder structure after this stage:
<workspace>/
├── concept.md
├── references/ ← optional reference library (user + skills)
├── spec/
├── ui/
│ ├── style.md
│ ├── screens.md
│ ├── sprites.md
│ └── preview.html ← skill auto-`open`s this on first save
└── tmp/ ← optional scratch space (safe to delete)
firmware-codegen (the next skill) creates <workspace>/source/
alongside. preview.html is for human review only — codegen reads
the three Markdown files for the contract; the HTML is regenerated
deterministically from those, so codegen ignores it.
Hardware catalog handling
The catalog at ../../shared/hardware-catalog.md constrains:
- Panel model + resolution — caps the layout grid (don't draw
past panel bounds) and rotation orientation (use the catalog's
documented
setRotationvalue for landscape / portrait). - Pixel format — RGB565 means 16-bit color. Palette hex must be
expressible as
0xNNNN; design with that in mind. - Available fonts — the rendering library's bundled fonts (e.g.
TFT_eSPI's
LOAD_GLCD/LOAD_FONT2/LOAD_FONT4). Don't invent a custom font without saying how it gets shipped. - Backlight control — if the catalog notes a backlight pin, the
style.mddensitytoken can also note "auto-dim in low ambient light" if the spec's edge cases imply it.
If the catalog is a placeholder, fall back to the most universal defaults (240×320 RGB565, 8-pt and 12-pt fonts, palette of 6 generic RGB565 hex codes).
Forbidden behaviors
- Do not redesign the screen list or behaviors. The spec defines what exists; UI defines how it looks. If a screen the spec defines has no clear visual answer, surface it back ("this screen needs a visual treatment but its content depends on a state that's ambiguous — pick one or send back to spec") rather than inventing.
- Do not propose effects the rendering library can't do. RGB565 + TFT_eSPI does not have alpha blending, gradient fills, or font hinting. If the visual direction needs those, you have to express them via dithering / multi-step blits / approximation, and call out the approximation explicitly.
- Do not produce sprites with more than one bit-depth assumption per sprite. Pick form A (primitive composition, palette-color) OR form B (pixel bitmap, palette-color OR 1-bit), not both in the same sprite.
- Do not put more than one question in a single turn. The interactions allowed in this skill are: (§1) "where's the spec?" (only if missing), (§3) "which visual direction?" (only when ambiguous), and, in step-by-step mode, the Collaboration-mode pre-stage check-in (folds §3's trigger in, one question per turn).
- Do not generate placeholder sprites for the user to fill in. Every sprite the spec needs gets a fully-specified definition (form A or form B).
- Do not reference or output anything that the rendering library / panel can't actually display. Stay inside the hardware envelope.
Tone
The user just wants the screens to not look ugly. Be confident, decisive, and concrete — sound like a designer who's worked with ESP32 + TFT_eSPI projects and knows the constraints. Don't editorialize about visual taste; deliver a coherent direction and adjust based on feedback.
Reference files
references/style-tokens.md— template forstyle.md. Defines the tokens schema (palette / typography / spacing / density). Load before your first draft.references/screen-template.md— template forscreens.md. Per- screen layout block. Load before your first draft.references/sprite-template.md— template forsprites.md. Form A (primitive composition) and Form B (pixel bitmap) both shown. Load before your first draft.references/preview-template.md— scaffolding forpreview.html. HTML shell + SVG screen-rendering convention + sprite-primitive → SVG mapping table. Load before generating the preview file.references/example-ui.md— a complete worked UI package (the LoRa Hike Buddy Locator) paired with the brainstorm + spec examples and realised by the codegen example'sdisplay.cpp. Read for tone, density, and what "right level of detail" looks like for each file. A sprite-light utility UI — pair it with the templates' richer cat snippets to see both ends of the sprite-detail range.