From 515047090cd4471b59fc3e95667b539423fe137d Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 07:56:43 -0700 Subject: [PATCH 1/5] spike: hardware/board selection UX playground Four surfaces from one board-metadata schema: boards catalog (tiers/price/sort), provisioning picker (SoC match), device-card hardware pane (wired-pin callouts), and pin discovery (color x rhythm patterns on free pins). All board drawings are generated SVG from per-pin metadata - no hand-drawn images. Co-Authored-By: Claude Fable 5 --- spikes/hardware-boards/README.md | 27 + spikes/hardware-boards/index.html | 981 ++++++++++++++++++++++++++++++ 2 files changed, 1008 insertions(+) create mode 100644 spikes/hardware-boards/README.md create mode 100644 spikes/hardware-boards/index.html diff --git a/spikes/hardware-boards/README.md b/spikes/hardware-boards/README.md new file mode 100644 index 000000000..df35aff05 --- /dev/null +++ b/spikes/hardware-boards/README.md @@ -0,0 +1,27 @@ +# Hardware / board selection — UX spike + +Raw-HTML exploration (repo spike convention) for board selection as a first-class +feature. Feature notes: Dropbox `Planning/lp2025/_features/hardware - selection, pin discovery.md`. + +Open `index.html` (any static server). Four surfaces, one shared idea: a board is +**metadata**, and one SVG renderer draws it everywhere. + +1. **Boards catalog** — the public "what should I buy" page. Cards w/ SVG wireframe, + soc/flash/caps, price, purchase links, gold/silver/bronze support tier. Sort + filter. +2. **Provisioning picker** — we detected a SoC; show matching boards first, mismatches + collapsed, generic-board fallback for unknown hardware. +3. **Device card · hardware pane** — board wireframe with callout boxes on the wired + pins (LED outputs, buttons), driven by the project's output config. +4. **Pin discovery ("LED finder")** — drive a unique color/blink pattern on every free + data-capable pin; user clicks the pattern they see on their strip. Input-only and + USB-JTAG pins (C6 GPIO12/13!) are excluded. + +Decisions this spike proposes (gate questions in the handoff): +- Board metadata schema (see the collapsible JSON in the page) — name/mfr/urls/price, + soc, flash/psram, caps list, tier, and a `hw` drawing block (module, usb, buttons, + headers w/ per-pin role). Pin roles: pwr/gnd/io/input-only/strap/usb/ctl. +- Support tiers: gold = first-class + tested every release, silver = tested + occasionally, bronze = community / should work. +- Violet = pin bound to an output (studio convention: bound = violet, never green). +- Discovery patterns = color × blink-count (+chase), so patterns stay distinguishable + for colorblind users via rhythm, not hue alone. diff --git a/spikes/hardware-boards/index.html b/spikes/hardware-boards/index.html new file mode 100644 index 000000000..bc515c43b --- /dev/null +++ b/spikes/hardware-boards/index.html @@ -0,0 +1,981 @@ + + + + +Hardware & board selection — design spike + + + + + +

Hardware & board selection — design spike

+

+ One board-metadata schema drives four surfaces: the public boards catalog, the + provisioning picker, the device card's hardware pane, and pin discovery. + Everything below renders from the same BOARDS objects — no + hand-drawn images. +

+ +

1 Boards catalog

+

+ The "what should I buy" page — also our shop window. Cards are metadata-driven; + the SVG wireframe is generated from the same pin table used everywhere else. +

+
+ Gold first-class, tested every release + Silver supported, tested occasionally + Bronze community-verified, should work +
+
+ Sort + + + SoC + + + + + + + +
+
+ +
+ proposed board metadata (one entry, verbatim from this page's data) +

+
+ +

2 Provisioning — pick your board

+

+ During device setup we already know the SoC from the flasher. Matching boards + lead; everything else collapses under a mismatch row. A dashed "generic" card + is the escape hatch for unlisted hardware. +

+
+ Detected SoC + + + + + + + +
+
+ +

3 Device card — hardware pane

+

+ The wiring view: the project's outputs and controls, drawn as callout boxes next + to the pins they occupy. Violet = bound (studio convention). Hover a callout or + a pin to link them. +

+
+
+ porch-lights + running · 57.7 fps + USB +
+
+ + + + +
+
+
+
+
hover a pin or a box — the pair lights up
+
+ ESP32-S3-DevKitC-1 + flash 8 MB · image 1.6 MB + psram 8 MB + outputs 2 · 450 px +
+
+ +

4 Pin discovery — "which pin are my LEDs on?"

+

+ We drive a distinct color × rhythm pattern on every free, output-capable pin. + The user looks at their strip and clicks the pattern they see. Rhythm (solid / + 1-blink / 2-blink / chase) disambiguates for colorblind users; input-only and + USB pins are skipped automatically. +

+
+
+
+
What are your LEDs doing right now?
+
+ Every free pin is playing a different pattern. Find the one that matches + your strip — that's your data pin. +
+
+
+ Found it — your LEDs are on . + +
+
+
+
+ + + + From b31d933a86dde55eda4daf5f299d74aa9df89b8d Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 09:12:53 -0700 Subject: [PATCH 2/5] spike rev 2: pin-row layout engine + solid-color discovery codes Gate feedback: the pin row is now the layout unit (u = pitch drives pad/cell/font sizes), so every pin holds cells simultaneously - name cell + capability/connection/swatch cells inline, like classic pinout diagrams. Top-edge terminals get band rows with leader lines (QuinLED). New board-pinout section documents the design language (row/cell/rail/ band/leader). Discovery drops motion patterns for solid color codes: color-order pretest (K-R-G-B-K-B-G-R-K, pick what you see) then combinatoric pixel-count x color-set computed from free-pin count. Co-Authored-By: Claude Fable 5 --- spikes/hardware-boards/README.md | 56 +- spikes/hardware-boards/index.html | 987 +++++++++++++++++++----------- 2 files changed, 652 insertions(+), 391 deletions(-) diff --git a/spikes/hardware-boards/README.md b/spikes/hardware-boards/README.md index df35aff05..dac4c73c5 100644 --- a/spikes/hardware-boards/README.md +++ b/spikes/hardware-boards/README.md @@ -3,25 +3,37 @@ Raw-HTML exploration (repo spike convention) for board selection as a first-class feature. Feature notes: Dropbox `Planning/lp2025/_features/hardware - selection, pin discovery.md`. -Open `index.html` (any static server). Four surfaces, one shared idea: a board is -**metadata**, and one SVG renderer draws it everywhere. - -1. **Boards catalog** — the public "what should I buy" page. Cards w/ SVG wireframe, - soc/flash/caps, price, purchase links, gold/silver/bronze support tier. Sort + filter. -2. **Provisioning picker** — we detected a SoC; show matching boards first, mismatches - collapsed, generic-board fallback for unknown hardware. -3. **Device card · hardware pane** — board wireframe with callout boxes on the wired - pins (LED outputs, buttons), driven by the project's output config. -4. **Pin discovery ("LED finder")** — drive a unique color/blink pattern on every free - data-capable pin; user clicks the pattern they see on their strip. Input-only and - USB-JTAG pins (C6 GPIO12/13!) are excluded. - -Decisions this spike proposes (gate questions in the handoff): -- Board metadata schema (see the collapsible JSON in the page) — name/mfr/urls/price, - soc, flash/psram, caps list, tier, and a `hw` drawing block (module, usb, buttons, - headers w/ per-pin role). Pin roles: pwr/gnd/io/input-only/strap/usb/ctl. -- Support tiers: gold = first-class + tested every release, silver = tested - occasionally, bronze = community / should work. -- Violet = pin bound to an output (studio convention: bound = violet, never green). -- Discovery patterns = color × blink-count (+chase), so patterns stay distinguishable - for colorblind users via rhythm, not hue alone. +Open `index.html` (any static server). **Rev 2** after Yona's gate feedback. + +## The row layout engine (rev 2 core) + +The pin row is the layout unit: `u` = pin pitch = row height, and everything +derives from it (pad `0.6u`, cell `0.78u`, font `0.5u`). Concepts: + +- **Row** — one pin's horizontal band; cells never leave it, so *every* pin can + hold cells simultaneously (rev 1's staggered callouts could not). +- **Cell** — typed chip in a row: name, capability (`adc`/`touch`/`spi`/`i2c`/ + `uart`/`usb`/`strap`/`warn`), connection (violet = bound), or color swatch. +- **Rail** — the stack of rows on a board edge (left/right extend outward). +- **Band + Leader** — top/bottom-edge pins (e.g. QuinLED screw terminals) get + rows in a band above/below the rails, tied to the pad by an elbow leader line. + +One renderer, four modes: `plain` (catalog thumbnails), `caps` (what's +supported — the classic pinout-diagram view), `wired` (what's connected, device +card), `swatch` (pin discovery). Board defs are plain JSON — agent-buildable +(this page's were), user-editable later. + +## Surfaces + +1. **Boards catalog** — cards w/ SVG wireframe, gold/silver/bronze tier, price, + purchase links, sort + SoC filter. +2. **Board pinout** — full capability view per board + the design-language doc. +3. **Provisioning picker** — flasher-detected SoC leads; mismatches collapse; + dashed generic fallback. +4. **Device card · hardware pane** — wired connections inline in pin rows; + QuinLED demos the top-edge terminal band. +5. **Pin discovery** — solid color codes (no motion). Step 1: color-order test + (K-R-G-B-K-B-G-R-K on every pin; user picks the row they see → RGB/GRB/…). + Step 2: combinatoric codes — smallest pixels×colors from RGBCMYW covering the + free-pin count, computed in code (e.g. 20 pins → 2 px × 5 colors). Input-only, + USB-JTAG (C6 IO12/13), and in-use pins are auto-skipped. diff --git a/spikes/hardware-boards/index.html b/spikes/hardware-boards/index.html index bc515c43b..426525dd9 100644 --- a/spikes/hardware-boards/index.html +++ b/spikes/hardware-boards/index.html @@ -29,11 +29,9 @@ --silver: #b9c0cc; --silver-bg: rgba(185,192,204,.09); --bronze: #c98d5c; --bronze-bg: rgba(201,141,92,.10); - /* board drawing */ --pcb: #101418; --pcb-edge: #2c3440; --silk: #8b93a0; - --pad: #4a5260; --module: #1a2027; } * { box-sizing: border-box; } @@ -56,15 +54,13 @@ padding: 10px 14px; border: 1px dashed var(--border-strong); border-radius: 8px; color: var(--muted); font-size: 12px; margin-bottom: 16px; } - .controls label { display: inline-flex; gap: 5px; align-items: center; cursor: pointer; } .controls .group { display: inline-flex; gap: 10px; align-items: center; } .controls .group b { color: var(--text); font-weight: 600; } .controls select { background: var(--terminal); color: var(--text); border: 1px solid var(--border-strong); border-radius: 6px; font-size: 12px; padding: 3px 6px; } - - .chiprow { display: inline-flex; gap: 4px; } + .chiprow { display: inline-flex; gap: 4px; flex-wrap: wrap; } .fchip { appearance: none; border: 1px solid var(--border-strong); background: transparent; color: var(--muted); font-size: 11.5px; font-weight: 600; border-radius: 999px; @@ -84,7 +80,6 @@ .tier.gold { color: var(--gold); background: var(--gold-bg); border-color: rgba(228,192,101,.35); } .tier.silver { color: var(--silver); background: var(--silver-bg); border-color: rgba(185,192,204,.3); } .tier.bronze { color: var(--bronze); background: var(--bronze-bg); border-color: rgba(201,141,92,.35); } - .tier-legend { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; font-size: 11.5px; color: var(--dim); margin: 0 0 16px; } .tier-legend span.def { display: inline-flex; gap: 7px; align-items: center; } @@ -95,7 +90,7 @@ .bcard { background: var(--card); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; display: flex; flex-direction: column; - transition: border-color .2s, transform .2s; + transition: border-color .2s; } .bcard:hover { border-color: var(--border-strong); } .bcard .figure { @@ -124,7 +119,7 @@ } .spec.soc { color: var(--name); border-color: rgba(228,192,101,.3); } .bcard .blurb { color: var(--muted); font-size: 12px; flex: 1; } - .bcard .links { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } + .bcard .links { display: flex; gap: 6px; flex-wrap: wrap; } .buy { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: var(--muted); @@ -134,6 +129,36 @@ .buy:hover { color: var(--strong); border-color: var(--bound); } .buy svg { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2; } + /* ---- pinout detail / design language ----------------------------- */ + .pinout-wrap { display: flex; gap: 22px; align-items: flex-start; flex-wrap: wrap; } + .pinout-fig { + background: var(--card); border: 1px solid var(--border); border-radius: 12px; + padding: 18px; overflow-x: auto; max-width: 100%; + } + .dlang { + flex: 1; min-width: 300px; max-width: 420px; + background: var(--terminal); border: 1px solid var(--border); border-radius: 12px; + padding: 14px 16px; font-size: 12px; color: var(--muted); + } + .dlang h3 { margin: 0 0 8px; font-size: 11px; letter-spacing: .09em; + text-transform: uppercase; color: var(--dim); } + .dlang dl { display: grid; grid-template-columns: 64px 1fr; gap: 6px 10px; margin: 0 0 12px; } + .dlang dt { color: var(--strong); font-weight: 600; font-size: 12px; } + .dlang dd { margin: 0; } + .dlang code { font-family: var(--mono); font-size: 10.5px; color: var(--text); } + + /* svg cell type styles (shared by all board svgs) */ + svg .cellt { font: 600 6.4px var(--mono); } + svg .rowg:hover .cell-rect { stroke-width: 1.2; filter: brightness(1.35); } + svg .leader { stroke: var(--border-strong); stroke-width: 1; fill: none; } + svg .pinlabel { font: 600 6.5px var(--mono); fill: var(--silk); } + + .cell-legend { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; } + .cell-legend .lg { + font-family: var(--mono); font-size: 10px; font-weight: 700; + border-radius: 5px; padding: 2px 8px; border: 1px solid; + } + /* ---- provisioning ----------------------------------------------- */ .provision { max-width: 860px; background: var(--card); border: 1px solid var(--border); @@ -158,7 +183,6 @@ .detected.unknown .dot { background: var(--attn); box-shadow: 0 0 8px var(--attn); } .detected code { font-family: var(--mono); color: var(--good); font-size: 12px; } .detected.unknown code { color: var(--attn); } - .pick-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px; padding: 14px 16px 8px; } .pick { @@ -177,13 +201,9 @@ .pick .pl span { font-size: 10.5px; color: var(--dim); font-family: var(--mono); } .pick .tier { position: absolute; top: 6px; right: 6px; transform: scale(.9); transform-origin: top right; } .pick.generic { border-style: dashed; } - .pick.generic .thumb { color: var(--dim); } .pick .match { font-size: 10px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--good); } - - .mismatch { - margin: 2px 16px 14px; border-top: 1px dashed var(--border); padding-top: 10px; - } + .mismatch { margin: 2px 16px 14px; border-top: 1px dashed var(--border); padding-top: 10px; } .mismatch > button { appearance: none; background: none; border: 0; color: var(--dim); font-size: 11.5px; cursor: pointer; padding: 2px 0; display: inline-flex; gap: 6px; align-items: center; @@ -204,8 +224,9 @@ .action:disabled { opacity: .4; cursor: default; } /* ---- device card hardware pane ----------------------------------- */ + .hw-demos { display: flex; gap: 22px; flex-wrap: wrap; align-items: flex-start; } .dcard { - width: 560px; border-radius: 10px; overflow: hidden; + width: fit-content; max-width: 100%; border-radius: 10px; overflow: hidden; background: var(--card); border: 1px solid var(--border); border-left: 3px solid var(--good); } @@ -227,60 +248,73 @@ font-size: 11px; font-weight: 700; letter-spacing: .05em; } .tab[aria-selected=true] { color: var(--good); background: var(--good-bg); } - .hwpane { display: flex; gap: 0; } - .hwpane .boardside { - flex: 1; padding: 12px; display: flex; align-items: center; justify-content: center; - min-height: 300px; - } - .hwpane .boardside svg { max-width: 100%; } + .boardside { padding: 14px; display: flex; justify-content: center; overflow-x: auto; } .hwmeta { border-top: 1px solid var(--border); padding: 8px 12px; display: flex; gap: 14px; font-size: 11px; color: var(--dim); font-family: var(--mono); flex-wrap: wrap; } .hwmeta b { color: var(--muted); font-weight: 600; } - /* board svg shared bits */ - .pinlabel { font: 600 6.5px var(--mono); fill: var(--silk); } - .pinlabel.gpio { fill: #aab3c0; } - svg .callout-box { fill: #171b21; stroke: var(--border-strong); } - svg .callout-box.bound { stroke: var(--bound); } - svg .callout-title { font: 700 8px -apple-system, sans-serif; fill: var(--strong); } - svg .callout-sub { font: 500 7px var(--mono); fill: var(--dim); } - svg .callout-gpio { font: 700 7px var(--mono); fill: var(--bound); } - svg .wire { stroke: var(--bound); stroke-width: 1.2; fill: none; opacity: .8; } - svg .wire.dim { stroke: var(--border-strong); } - svg .pad-ring { fill: none; stroke: var(--bound); stroke-width: 1.6; } - svg .hl .callout-box { stroke: var(--strong); } - .hover-hint { font-size: 11px; color: var(--dim); padding: 0 12px 10px; } - /* ---- discovery ---------------------------------------------------- */ + .step-card { + max-width: 980px; background: var(--card); border: 1px solid var(--border); + border-radius: 12px; padding: 16px 18px; margin-bottom: 18px; + } + .step-card.locked { opacity: .45; pointer-events: none; } + .step-card .stitle { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; } + .step-card .stitle .sn { font-size: 10.5px; font-weight: 800; letter-spacing: .1em; + text-transform: uppercase; color: var(--dim); } + .step-card .stitle b { font-size: 14px; color: var(--strong); } + .step-card .sexp { font-size: 12px; color: var(--muted); margin-bottom: 12px; max-width: 640px; } + .seq { display: inline-flex; gap: 4px; padding: 8px 10px; background: var(--terminal); + border: 1px solid var(--border); border-radius: 8px; } + .seq .px { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(255,255,255,.08); } + .order-opts { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 8px; margin-top: 12px; } + .order-opt { + display: flex; align-items: center; gap: 10px; appearance: none; cursor: pointer; + background: var(--terminal); border: 1px solid var(--border); border-radius: 9px; + padding: 8px 11px; color: var(--text); + } + .order-opt:hover { border-color: var(--border-strong); } + .order-opt[aria-pressed=true] { border-color: var(--good); background: var(--good-bg); + box-shadow: 0 0 0 1px var(--good); } + .order-opt .mini { display: inline-flex; gap: 2px; flex: none; } + .order-opt .mini i { width: 9px; height: 12px; border-radius: 2px; display: block; + border: 1px solid rgba(255,255,255,.07); } + .order-opt .ol { display: flex; flex-direction: column; } + .order-opt .ol b { font-family: var(--mono); font-size: 12px; } + .order-opt .ol span { font-size: 10px; color: var(--dim); } + .order-ok { display: none; align-items: center; gap: 8px; margin-top: 12px; + font-size: 12.5px; color: var(--text); } + .order-ok.on { display: flex; } + .order-ok b { color: var(--good); font-family: var(--mono); } + .disco-wrap { display: flex; gap: 22px; align-items: flex-start; flex-wrap: wrap; } .disco-board { - background: var(--card); border: 1px solid var(--border); border-radius: 12px; - padding: 16px; display: flex; align-items: center; justify-content: center; + background: var(--terminal); border: 1px solid var(--border); border-radius: 12px; + padding: 14px; overflow-x: auto; max-width: 100%; } - .disco-side { flex: 1; min-width: 320px; max-width: 460px; display: flex; + .disco-side { flex: 1; min-width: 300px; max-width: 430px; display: flex; flex-direction: column; gap: 10px; } - .disco-side .prompt { font-size: 14px; font-weight: 700; color: var(--strong); } - .disco-side .explain { font-size: 12px; color: var(--muted); } - .patlist { display: flex; flex-direction: column; gap: 6px; } + .disco-side .prompt { font-size: 13.5px; font-weight: 700; color: var(--strong); } + .mathline { font-size: 11.5px; color: var(--dim); } + .mathline code { font-family: var(--mono); color: var(--muted); font-size: 11px; } + .patgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 6px; } .patbtn { - display: flex; align-items: center; gap: 12px; - appearance: none; text-align: left; cursor: pointer; - background: var(--terminal); border: 1px solid var(--border); border-radius: 9px; - padding: 9px 12px; color: var(--text); + display: flex; align-items: center; gap: 7px; justify-content: space-between; + appearance: none; cursor: pointer; + background: var(--terminal); border: 1px solid var(--border); border-radius: 8px; + padding: 6px 9px; color: var(--text); } .patbtn:hover { border-color: var(--border-strong); } .patbtn[aria-pressed=true] { border-color: var(--bound); background: var(--bound-bg); box-shadow: 0 0 0 1px var(--bound); } - .patbtn .dots { display: inline-flex; gap: 4px; flex: none; width: 46px; } - .patbtn .plabel { font-size: 12.5px; font-weight: 600; flex: 1; } - .patbtn .pgpio { font-family: var(--mono); font-size: 11px; color: var(--dim); } + .patbtn .dots { display: inline-flex; gap: 3px; } + .patbtn .dots i { width: 11px; height: 11px; border-radius: 3px; display: block; + border: 1px solid rgba(255,255,255,.08); } + .patbtn .pgpio { font-family: var(--mono); font-size: 10px; color: var(--dim); } .patbtn[aria-pressed=true] .pgpio { color: var(--bound); } - .led { width: 10px; height: 10px; border-radius: 50%; background: #22262d; } - .skipnote { font-size: 11px; color: var(--dim); border-top: 1px dashed var(--border); - padding-top: 9px; } - .skipnote code { font-family: var(--mono); color: var(--muted); font-size: 10.5px; } .disco-result { display: none; align-items: center; gap: 10px; padding: 10px 12px; background: var(--good-bg); border: 1px solid rgba(63,214,142,.35); border-radius: 9px; @@ -288,39 +322,9 @@ } .disco-result.on { display: flex; } .disco-result b { color: var(--good); font-family: var(--mono); } - - /* pattern animations: color set via --pc; rhythm via keyframes. - every pattern = color + rhythm so it survives colorblindness. */ - @keyframes blink1 { 0%,55% { background: var(--pc); box-shadow: 0 0 7px var(--pc); } - 60%,100% { background: #22262d; box-shadow: none; } } - @keyframes blink2 { 0%,20%,40%,60% { background: #22262d; box-shadow: none; } - 25%,35% { background: var(--pc); box-shadow: 0 0 7px var(--pc); } - 45%,55% { background: var(--pc); box-shadow: 0 0 7px var(--pc); } - 65%,100% { background: #22262d; box-shadow: none; } } - @keyframes solidp { 0%,100% { background: var(--pc); box-shadow: 0 0 7px var(--pc); } } - @keyframes chase { 0%,15% { background: var(--pc); box-shadow: 0 0 7px var(--pc); } - 25%,100% { background: #22262d; box-shadow: none; } } - .led.m-solid { animation: solidp 1.6s infinite; } - .led.m-blink { animation: blink1 1.6s infinite; } - .led.m-blink2 { animation: blink2 1.6s infinite; } - .led.m-chase { animation: chase 1.2s infinite; } - .led.d1 { animation-delay: .12s; } .led.d2 { animation-delay: .24s; } - .led.m-blink.d1, .led.m-blink.d2, .led.m-blink2.d1, .led.m-blink2.d2 { animation-delay: 0s; } - - /* svg pattern swatches (same keyframes apply to svg rects via fill? no — - svg circles animate via CSS background not allowed; use opacity instead) */ - @keyframes sblink1 { 0%,55% { opacity: 1; } 60%,100% { opacity: .12; } } - @keyframes sblink2 { 0%,20%,40%,60% { opacity: .12; } 25%,35% { opacity: 1; } - 45%,55% { opacity: 1; } 65%,100% { opacity: .12; } } - @keyframes ssolid { 0%,100% { opacity: 1; } } - @keyframes schase { 0%,15% { opacity: 1; } 25%,100% { opacity: .12; } } - svg .sled.m-solid { animation: ssolid 1.6s infinite; } - svg .sled.m-blink { animation: sblink1 1.6s infinite; } - svg .sled.m-blink2 { animation: sblink2 1.6s infinite; } - svg .sled.m-chase { animation: schase 1.2s infinite; } - svg .sled.d1 { animation-delay: .12s; } svg .sled.d2 { animation-delay: .24s; } - svg .sled.m-blink.d1, svg .sled.m-blink.d2, - svg .sled.m-blink2.d1, svg .sled.m-blink2.d2 { animation-delay: 0s; } + .skipnote { font-size: 11px; color: var(--dim); border-top: 1px dashed var(--border); + padding-top: 9px; } + .skipnote code { font-family: var(--mono); color: var(--muted); font-size: 10.5px; } details.schema { max-width: 860px; margin-top: 14px; } details.schema summary { font-size: 11.5px; color: var(--dim); cursor: pointer; } @@ -332,19 +336,16 @@ -

Hardware & board selection — design spike

+

Hardware & board selection — design spike · rev 2

- One board-metadata schema drives four surfaces: the public boards catalog, the - provisioning picker, the device card's hardware pane, and pin discovery. - Everything below renders from the same BOARDS objects — no - hand-drawn images. + Rev 2 after gate feedback: the pin row is the layout unit — one row height + drives pads, cells, and type, and every pin can hold cells simultaneously. Pin + discovery switched from motion patterns to solid color codes with a + color-order pretest. All boards still render from one metadata schema.

1 Boards catalog

-

- The "what should I buy" page — also our shop window. Cards are metadata-driven; - the SVG wireframe is generated from the same pin table used everywhere else. -

+

The "what should I buy" page. Thumbnails are the same renderer in plain mode.

Gold first-class, tested every release Silver supported, tested occasionally @@ -368,17 +369,51 @@

1 Boards catalog

-
proposed board metadata (one entry, verbatim from this page's data)

 
-

2 Provisioning — pick your board

+

2 Board pinout — the row layout engine

+

+ The full "what's supported" view (compare: the classic pinout diagrams). Every pin + owns a row; capability cells sit inside it. Boards pick their own row height via + scale — nothing ever overlaps because the row is the unit. +

+
+ Board + + +
+
+
+
+

Design language (proposed)

+
+
Unit u
pin pitch = row height. Everything scales from it: + pad 0.6u, cell 0.78u, font 0.5u.
+
Row
one pin's horizontal band. Cells never leave their row, so + every pin can be annotated at once.
+
Cell
typed chip in a row: name, capability + (adc, touch, spi…), connection, + or swatch. Width fits content; height is fixed.
+
Rail
the stack of rows on a board edge. Left/right rails extend + outward from the pads.
+
Band
top/bottom-edge pins can't have vertical rows — their rows + go in a band above/below the rails, tied to the pad by a leader line.
+
Leader
elbow line from a pad to its band row.
+
+
Board defs are plain JSON — buildable by agents (this page's were), editable + by users later. Same renderer, four modes: plain · + caps · wired · swatch.
+
+
+
+ +

3 Provisioning — pick your board

- During device setup we already know the SoC from the flasher. Matching boards - lead; everything else collapses under a mismatch row. A dashed "generic" card - is the escape hatch for unlisted hardware. + We know the SoC from the flasher. Matching boards lead; mismatches collapse; dashed + generic card is the escape hatch.

Detected SoC @@ -392,67 +427,95 @@

2 Provisioning — pick your board

-

3 Device card — hardware pane

+

4 Device card — hardware pane

- The wiring view: the project's outputs and controls, drawn as callout boxes next - to the pins they occupy. Violet = bound (studio convention). Hover a callout or - a pin to link them. + "What's connected" — same rows, connection cells (violet = bound). Every pin keeps + its name cell, so the wiring reads in context. Second card: screw terminals on the + top edge land in a band with leader lines.

-
-
- porch-lights - running · 57.7 fps - USB -
-
- - - - -
-
+
+
+
+ porch-lights + running · 57.7 fps + USB +
+
+ + + + +
+
+ ESP32-S3-DevKitC-1 + flash 8 MB · image 1.6 MB + outputs 2 · 450 px +
-
hover a pin or a box — the pair lights up
-
- ESP32-S3-DevKitC-1 - flash 8 MB · image 1.6 MB - psram 8 MB - outputs 2 · 450 px +
+
+ garden-quinled + offline + WIFI +
+
+ + + + +
+
+
+ QuinLED-Dig-Uno + top-edge terminals → band + leaders +
-

4 Pin discovery — "which pin are my LEDs on?"

+

5 Pin discovery — solid color codes

- We drive a distinct color × rhythm pattern on every free, output-capable pin. - The user looks at their strip and clicks the pattern they see. Rhythm (solid / - 1-blink / 2-blink / chase) disambiguates for colorblind users; input-only and - USB pins are skipped automatically. + No motion: each free pin shows a steady color code on the first pixels of whatever + strip is attached. Step 1 nails down the strip's color order (otherwise color codes + lie); step 2 assigns combinatoric codes — pixel count × color set computed from how + many free pins we need to cover.

-
-
-
-
What are your LEDs doing right now?
-
- Every free pin is playing a different pattern. Find the one that matches - your strip — that's your data pin. -
-
-
- Found it — your LEDs are on . - + +
+
Step 1 of 2What colors do you see?
+
+ Every pin is now playing this 9-pixel sequence (K = off). If your strip's color + order isn't RGB, you'll see swapped colors — pick the row that matches your strip. +
+
+
+
✓ Color order — codes below are corrected for it.
+
+ +
+
Step 2 of 2Find your code
+
+
+
+
+
Click the code your strip is showing
+
+
+ Found it — your LEDs are on . + +
+
-
From 0ab7f1f8b2eb37c35523ba7f374f0252d758d269 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 09:24:37 -0700 Subject: [PATCH 3/5] spike rev 3: aligned pin labels, palindrome order test, design-language cheat sheet Pin names move inside the board (silkscreen-style) so outside cells share one start edge and align into columns. Order-test sequence is now the palindrome K-R-G-B-W-B-G-R-K: K bounds repeats, W marks center, reads the same either direction, and K/W survive any color-order permutation. DESIGN-LANGUAGE.md added as the vocab cheat sheet. Co-Authored-By: Claude Fable 5 --- spikes/hardware-boards/DESIGN-LANGUAGE.md | 66 +++++++++++++++++++++++ spikes/hardware-boards/index.html | 52 +++++++++++------- 2 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 spikes/hardware-boards/DESIGN-LANGUAGE.md diff --git a/spikes/hardware-boards/DESIGN-LANGUAGE.md b/spikes/hardware-boards/DESIGN-LANGUAGE.md new file mode 100644 index 000000000..239f628d3 --- /dev/null +++ b/spikes/hardware-boards/DESIGN-LANGUAGE.md @@ -0,0 +1,66 @@ +# Board diagram design language — cheat sheet + +Vocabulary for talking about LightPlayer board diagrams. Everything renders from +board metadata (plain JSON) through one SVG renderer. + +## Layout vocabulary + +| Term | Meaning | +|---|---| +| **Unit** `u` | Pin pitch = row height. THE scaling factor — every other dimension derives from it. | +| **Row** | One pin's horizontal band, `u` tall. All annotation for a pin lives in its row, so every pin can be annotated simultaneously. | +| **Cell** | A typed chip inside a row. Width fits content; height is fixed (`0.78u`). | +| **Rail** | The stack of rows on a left/right board edge. Rails extend outward from the pads. | +| **Band** | Horizontal strip above/below the rails holding rows for top/bottom-edge pins (screw terminals etc.), which can't have vertical rows. | +| **Leader** | Elbow line connecting a top/bottom pad to its band row. | +| **Pad** | The pin's physical marker on the board edge, colored by role. | +| **Label** | The pin's name, silkscreen-style *inside* the board edge, colored by role. Keeps outside cells left-aligned into columns. | + +## Derived geometry (all from `u`) + +| Element | Size | +|---|---| +| Row height / pin pitch | `1.00u` | +| Cell height | `0.78u` | +| Pad | `0.62u × 0.45u` | +| Cell font | `0.50u` | +| Cell gap | `0.22u` | + +## Cell types + +| Type | Color family | Used for | +|---|---|---| +| `name` | slate | Pin name (band rows only; rails put names inside the board) | +| `pwr` / `gnd` | red / dim | Power and ground | +| `adc` `dac` `touch` | green / blue / orange | Analog capabilities | +| `spi` `i2c` `uart` `usb` | violet / cyan / brown / amber | Buses | +| `strap` | amber | Boot-strap pins — usable, with care | +| `warn` | dim orange | Caution: input-only, XTAL, PSRAM-reserved, JTAG | +| `conn` | **violet** | A bound connection (studio convention: bound = violet) | +| swatch | — | Discovery color code (n solid squares) | + +## Pin roles (pad + label color, drives eligibility) + +`pwr5` `pwr3` `gnd` `io` `ioin` (input-only) `strap` `usb` `ctl` (EN/RST) `nc` + +Output-eligible for LEDs: `io`, `strap`. Never: `ioin`, `usb` (e.g. C6 IO12/13 +USB-JTAG), `nc`, power/ctl, and pins already bound or reserved (onboard RGB). + +## Renderer modes + +| Mode | Question it answers | Where | +|---|---|---| +| `plain` | what does the board look like? | catalog / picker thumbnails | +| `caps` | what's *supported* on each pin? | boards page detail | +| `wired` | what's *connected*? | device card hardware pane | +| `swatch` | which pin are my LEDs on? | pin discovery | + +## Discovery language + +- **Order test**: the 9-pixel palindrome `K-R-G-B-W-B-G-R-K`, repeated on every + free pin. K bounds each repeat, W marks the center; palindrome ⇒ direction + doesn't matter; K/W are permutation-invariant ⇒ the perceived row uniquely + identifies the strip's color order (RGB/GRB/…). +- **Code**: a pin's steady color sequence on the first `n` pixels. +- **Code plan**: smallest `n px × c colors` with `c^n ≥ free pins`, colors drawn + in order from the palette `R G B C M Y W`. diff --git a/spikes/hardware-boards/index.html b/spikes/hardware-boards/index.html index 426525dd9..c727659b1 100644 --- a/spikes/hardware-boards/index.html +++ b/spikes/hardware-boards/index.html @@ -403,9 +403,15 @@

Design language (proposed)

go in a band above/below the rails, tied to the pad by a leader line.
Leader
elbow line from a pad to its band row.
+
+
Pad
the pin's physical marker on the edge, colored by role.
+
Label
pin name, silkscreen-style inside the board — so + outside cells share one start edge and align into columns.
+
Board defs are plain JSON — buildable by agents (this page's were), editable by users later. Same renderer, four modes: plain · - caps · wired · swatch.
+ caps · wired · swatch. + Full vocab: DESIGN-LANGUAGE.md next to this file.
@@ -484,8 +490,9 @@

5 Pin discovery — solid color codes

Step 1 of 2What colors do you see?
- Every pin is now playing this 9-pixel sequence (K = off). If your strip's color - order isn't RGB, you'll see swapped colors — pick the row that matches your strip. + Every pin repeats this 9-pixel palindrome (K = off, W = center marker) — it reads + the same from either end, so it doesn't matter which way your strip runs. If the + color order isn't RGB you'll see swapped colors — pick the row that matches.
@@ -826,21 +833,24 @@

5 Pin discovery — solid color codes

// ---- build cell lists per pin ------------------------------------- // cell: {text,type} or {swatch:[...colors]} const cellW = c => c.swatch ? c.swatch.length * (cellH * .74) + 8 : Math.max(18, c.text.length * font * .66 + 9); - const cellsFor = (p) => { + // pin names live INSIDE the board (silkscreen-style) so outside cells share a + // common start x and align into columns. Band rows (terminals) have no inside + // space — they keep a leading name cell. + const cellsFor = (p, withName = false) => { if (mode === "plain") return []; - const name = { text: nameCellText(p), type: nameCellType(p) }; + const base = withName ? [{ text: nameCellText(p), type: nameCellType(p) }] : []; if (mode === "caps") - return [name, ...(p.caps || []).map(([text, type]) => ({ text, type }))]; + return [...base, ...(p.caps || []).map(([text, type]) => ({ text, type }))]; if (mode === "wired") { const w = wired?.find(x => x.gpio === p.gpio && p.gpio != null); - return w ? [name, { text: w.title, type: "conn" }, ...(w.extra ? [{ text: w.extra, type: "conn" }] : [])] - : [name]; + return w ? [...base, { text: w.title, type: "conn" }, ...(w.extra ? [{ text: w.extra, type: "conn" }] : [])] + : base; } if (mode === "swatch") { const s = swatches?.find(x => x.gpio === p.gpio && p.gpio != null); - return s ? [name, { swatch: s.colors, sel: s.sel }] : [name]; + return s ? [...base, { swatch: s.colors, sel: s.sel }] : base; } - return [name]; + return base; }; // ---- vertical geometry -------------------------------------------- @@ -865,7 +875,7 @@

5 Pin discovery — solid color codes

const terms = (hw.terminals || []).map((t, i) => ({ ...t, i })); const tw = 20, t0 = (W - terms.length * tw) / 2; const bandRows = mode === "plain" ? [] : terms.map((t, i) => { - const cells = cellsFor(t); + const cells = cellsFor(t, true); return { t, cells, padX: t0 + t.i * tw + (tw - 2) / 2, y: -(1.6 * u) - (terms.length - 1 - i) * u, w: cells.reduce((a, c) => a + cellW(c), 0) + gap * Math.max(0, cells.length - 1) }; @@ -953,14 +963,14 @@

5 Pin discovery — solid color codes

rails[side].forEach(r => { const px = side === "left" ? 1 : W - 1 - padW; A(``); - if (mode === "plain") { - if (labels) { - const lx = side === "left" ? px + padW + 3 : px - 3; - A(`${esc(r.p.label)}`); - } - } else if (r.cells.length) { - A(drawCells(r.cells, r.y, side === "left" ? -4 : W + 4, side === "left" ? -1 : 1, r.p.gpio)); + if (labels) { + const lx = side === "left" ? px + padW + 3 : px - 3; + const txt = mode === "plain" ? r.p.label : nameCellText(r.p); + const fg = mode === "plain" ? "var(--silk)" : (CELL_STYLE[nameCellType(r.p)] || CELL_STYLE.name).fg; + A(`${esc(txt)}`); } + if (mode !== "plain" && r.cells.length) + A(drawCells(r.cells, r.y, side === "left" ? -4 : W + 4, side === "left" ? -1 : 1, r.p.gpio)); }); } @@ -1131,7 +1141,9 @@

5 Pin discovery — solid color codes

const COLOR_NAME = { R: "red", G: "green", B: "blue", C: "cyan", M: "magenta", Y: "yellow", W: "white" }; /* --- step 1: color order --- */ -const SEQ = ["K","R","G","B","K","B","G","R","K"]; +// palindrome: reads the same from either end (art may hide data direction), +// K bounds each repeat, W marks the center — start/end always obvious. +const SEQ = ["K","R","G","B","W","B","G","R","K"]; const ORDERS = [ { o: "RGB", note: "colors as sent" }, { o: "GRB", note: "most WS281x strips" }, @@ -1140,7 +1152,7 @@

5 Pin discovery — solid color codes

]; // we emit bytes as R,G,B; a strip wired for order O reads byte i as channel O[i] function perceived(sent, order) { - if (sent === "K") return "K"; + if (sent === "K" || sent === "W") return sent; // permutation-invariant const v = { R: sent === "R" ? 1 : 0, G: sent === "G" ? 1 : 0, B: sent === "B" ? 1 : 0 }; const d = { R: 0, G: 0, B: 0 }; [...order].forEach((ch, i) => d[ch] = v["RGB"[i]]); From 8db816d0ec9732f5bf84b3ac55cf9d05663188bd Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 09:42:19 -0700 Subject: [PATCH 4/5] spike rev 4: annotated anatomy figure + palindromic pin codes Anatomy card renders the QuinLED and pins every design-language term (Unit/Row/Cell/Rail/Band/Leader/Pad/Label) to the element it names - measured from the live SVG, not hand-positioned. Pin-identifying codes are now palindromes for the same reason as the order test: data direction is unknown, so a code and its reverse are indistinguishable. Coverage becomes c^ceil(n/2): 20 free pins -> 3 px x 5 colors, X-Y-X. Co-Authored-By: Claude Fable 5 --- spikes/hardware-boards/DESIGN-LANGUAGE.md | 10 +- spikes/hardware-boards/index.html | 123 ++++++++++++++++++++-- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/spikes/hardware-boards/DESIGN-LANGUAGE.md b/spikes/hardware-boards/DESIGN-LANGUAGE.md index 239f628d3..e02dc1d90 100644 --- a/spikes/hardware-boards/DESIGN-LANGUAGE.md +++ b/spikes/hardware-boards/DESIGN-LANGUAGE.md @@ -61,6 +61,10 @@ USB-JTAG), `nc`, power/ctl, and pins already bound or reserved (onboard RGB). free pin. K bounds each repeat, W marks the center; palindrome ⇒ direction doesn't matter; K/W are permutation-invariant ⇒ the perceived row uniquely identifies the strip's color order (RGB/GRB/…). -- **Code**: a pin's steady color sequence on the first `n` pixels. -- **Code plan**: smallest `n px × c colors` with `c^n ≥ free pins`, colors drawn - in order from the palette `R G B C M Y W`. +- **Code**: a pin's steady color sequence on the first `n` pixels. Codes are + **palindromes** — data direction is unknown, so a code and its reverse are + indistinguishable; a palindrome reads the same from either end. +- **Code plan**: smallest `n px × c colors` with `c^ceil(n/2) ≥ free pins` + (a length-`n` palindrome has `ceil(n/2)` free digits), colors drawn in order + from the palette `R G B C M Y W`. E.g. 20 pins → `3 px × 5 colors` = 25 + `X-Y-X` codes. diff --git a/spikes/hardware-boards/index.html b/spikes/hardware-boards/index.html index c727659b1..f8a5dc0af 100644 --- a/spikes/hardware-boards/index.html +++ b/spikes/hardware-boards/index.html @@ -153,6 +153,16 @@ svg .leader { stroke: var(--border-strong); stroke-width: 1; fill: none; } svg .pinlabel { font: 600 6.5px var(--mono); fill: var(--silk); } + .anat-title { font-size: 11px; letter-spacing: .09em; text-transform: uppercase; + color: var(--dim); margin-bottom: 8px; } + svg .anat-line { stroke: var(--name); stroke-width: .9; stroke-dasharray: 2 2.5; + opacity: .65; fill: none; } + svg .anat-dot { fill: var(--name); } + svg .anat-label { font: 700 8px -apple-system, "Segoe UI", sans-serif; + fill: var(--name); } + svg .anat-shape { stroke: var(--name); fill: none; stroke-width: 1; + stroke-dasharray: 3 2.5; opacity: .8; } + .cell-legend { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; } .cell-legend .lg { font-family: var(--mono); font-size: 10px; font-weight: 700; @@ -387,6 +397,10 @@

2 Board pinout — the row layout engine

+
+
Anatomy — the vocab in place
+
+

Design language (proposed)

@@ -962,7 +976,7 @@

5 Pin discovery — solid color codes

for (const side of ["left", "right"]) { rails[side].forEach(r => { const px = side === "left" ? 1 : W - 1 - padW; - A(``); + A(``); if (labels) { const lx = side === "left" ? px + padW + 3 : px - 3; const txt = mode === "plain" ? r.p.label : nameCellText(r.p); @@ -1135,6 +1149,87 @@

5 Pin discovery — solid color codes

], }); +/* ===================== anatomy figure ===================== */ +// The design language drawn on a real board: render the QuinLED (rails + band + +// leaders + terminals), then measure the live SVG and pin each vocab term to +// the element it names. Measured, not hand-positioned, so it tracks the engine. +function renderAnatomy() { + const host = document.getElementById("anatomyFig"); + const U = 13, SC = 1.35; + host.innerHTML = renderBoard(quin, { mode: "caps", u: U, scale: SC }); + const svg = host.querySelector("svg"); + const NS = "http://www.w3.org/2000/svg"; + const add = (tag, attrs, text) => { + const el = document.createElementNS(NS, tag); + for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v); + if (text != null) el.textContent = text; + svg.appendChild(el); return el; + }; + const mid = b => ({ x: b.x + b.width / 2, y: b.y + b.height / 2 }); + const note = (lx, ly, px, py, text, anchor = "start") => { + add("path", { d: `M ${lx} ${ly} L ${px} ${py}`, class: "anat-line" }); + add("circle", { cx: px, cy: py, r: 1.6, class: "anat-dot" }); + add("text", { x: lx, y: ly + (ly < py ? -3 : 8), "text-anchor": anchor, class: "anat-label" }, text); + }; + const bracket = (x, y1, y2, dir) => + add("path", { d: `M ${x + 4 * dir} ${y1} L ${x} ${y1} L ${x} ${y2} L ${x + 4 * dir} ${y2}`, class: "anat-shape", "stroke-dasharray": "none" }); + + const pads = [...svg.querySelectorAll(".pad")].map(e => e.getBBox()); + const rowgs = [...svg.querySelectorAll(".rowg")]; + const leaders = [...svg.querySelectorAll(".leader")].map(e => e.getBBox()); + const plabels = [...svg.querySelectorAll(".pinlabel")].map(e => e.getBBox()); + const nL = quin.hw.left.length; // pads: left rail first, then right + + // Pad — right rail, last pin (free space beyond: no cells on that row) + const padT = mid(pads[nL + quin.hw.right.length - 1]); + note(padT.x + 26, padT.y + 14, padT.x + 3, padT.y + 2, "Pad"); + + // Label — inside name of right pin "IO15" (pinlabels: [USB caption, left…, right…]) + const lblT = mid(plabels[1 + nL + 1]); + note(lblT.x - 4, lblT.y + 30, lblT.x, lblT.y + 4, "Label", "middle"); + + // Row + Cell — right rail row with cells (IO15 → ADC2_3): outline pad→cells + const rowRight = rowgs.find(g => g.dataset.gpio === "15"); + const rb = rowRight.getBBox(); + const padRow = pads[nL + 1]; // IO15 is 2nd right pin + const rx0 = Math.min(rb.x, padRow.x) - 2, rx1 = Math.max(rb.x + rb.width, padRow.x + padRow.width) + 2; + add("rect", { x: rx0, y: padRow.y + padRow.height / 2 - U / 2, width: rx1 - rx0, height: U, rx: 4, class: "anat-shape" }); + note(rx1 + 26, padRow.y - 12, rx1 + 1, padRow.y + padRow.height / 2 - U / 2 + 2, "Row (1u tall)"); + const cellT = mid(rowRight.querySelector(".cell-rect").getBBox()); + note(cellT.x + 10, cellT.y + 26, cellT.x, cellT.y + 4, "Cell"); + + // Rail — bracket right of the right rail's pads + const railPads = pads.slice(nL); + const railX = rx1 + 44; + bracket(railX, railPads[0].y - 2, railPads.at(-1).y + railPads.at(-1).height + 2, -1); + note(railX + 20, (railPads[0].y + railPads.at(-1).y) / 2 + 4, railX + 1, + (railPads[0].y + railPads.at(-1).y) / 2 + 4, "Rail"); + + // Band — bracket right of the terminal band rows (they sit above the board) + const bandRows = rowgs.filter(g => g.parentNode === svg && g.getBBox().y < 0); + const bandBBs = bandRows.map(g => g.getBBox()); + const bx = Math.max(...bandBBs.map(b => b.x + b.width)) + 6; + const by0 = Math.min(...bandBBs.map(b => b.y)) - 2, by1 = Math.max(...bandBBs.map(b => b.y + b.height)) + 2; + bracket(bx, by0, by1, -1); + note(bx + 18, (by0 + by1) / 2 + 3, bx + 1, (by0 + by1) / 2 + 3, "Band"); + + // Leader — the elbow line from the leftmost terminal pad + const ldT = leaders[0]; + note(ldT.x - 28, ldT.y + ldT.height + 16, ldT.x + 1, ldT.y + ldT.height - 4, "Leader"); + + // Unit u — dimension between the first two left pads + const p0 = mid(pads[0]), p1 = mid(pads[1]); + const ux = p0.x - 12; + add("path", { d: `M ${ux - 3} ${p0.y} L ${ux + 3} ${p0.y} M ${ux} ${p0.y} L ${ux} ${p1.y} M ${ux - 3} ${p1.y} L ${ux + 3} ${p1.y}`, class: "anat-shape", "stroke-dasharray": "none" }); + note(ux - 26, p0.y + U / 2 + 3, ux, (p0.y + p1.y) / 2, "u = pitch", "end"); + + // expand the viewBox to fit annotations + const bb = svg.getBBox(); + svg.setAttribute("viewBox", `${bb.x - 6} ${bb.y - 6} ${bb.width + 12} ${bb.height + 12}`); + svg.setAttribute("width", Math.round((bb.width + 12) * SC)); + svg.setAttribute("height", Math.round((bb.height + 12) * SC)); +} + /* ===================== 5. discovery ===================== */ const COLOR = { K: "#15181d", R: "#ef5350", G: "#3fd68e", B: "#4f9cf0", C: "#2fd4c9", M: "#d06bd6", Y: "#f0e04b", W: "#f4f2ec" }; @@ -1187,19 +1282,23 @@

5 Pin discovery — solid color codes

const freePins = allPins.filter(p => p.gpio != null && (p.role === "io" || p.role === "strap") && !RESERVED.has(p.gpio)); -// smallest (pixels n, colors c) covering P pins: prefer fewer pixels, then fewer colors +// codes must be palindromes: the strip's data direction is unknown, so a code +// and its reverse are indistinguishable. A length-n palindrome has ceil(n/2) +// free digits -> coverage c^ceil(n/2). Prefer fewer pixels, then fewer colors. const PALETTE = ["R","G","B","C","M","Y","W"]; function codePlan(P) { - for (let n = 1; n <= 3; n++) + for (let n = 1; n <= 5; n++) { + const half = Math.ceil(n / 2); for (let c = 2; c <= PALETTE.length; c++) - if (Math.pow(c, n) >= P) return { n, c }; - return { n: 3, c: PALETTE.length }; + if (Math.pow(c, half) >= P) return { n, c, half }; + } + return { n: 5, c: PALETTE.length, half: 3 }; } const plan = codePlan(freePins.length); const codeFor = i => { - const digits = []; - for (let d = 0, x = i; d < plan.n; d++) { digits.push(PALETTE[x % plan.c]); x = Math.floor(x / plan.c); } - return digits; + const half = []; + for (let d = 0, x = i; d < plan.half; d++) { half.push(PALETTE[x % plan.c]); x = Math.floor(x / plan.c); } + return Array.from({ length: plan.n }, (_, k) => half[Math.min(k, plan.n - 1 - k)]); }; const candidates = freePins.map((p, i) => ({ gpio: p.gpio, code: codeFor(i) })); @@ -1212,10 +1311,11 @@

5 Pin discovery — solid color codes

renderBoard(c6, { mode: "swatch", u: 12, scale: 1.15, swatches: sw }); document.getElementById("mathLine").innerHTML = `${freePins.length} free output-capable pins → - smallest covering code = ${plan.n} px × ${plan.c} colors + smallest covering palindromic code = ${plan.n} px × ${plan.c} colors (${PALETTE.slice(0, plan.c).map(k => COLOR_NAME[k]).join(", ")}) = - ${Math.pow(plan.c, plan.n)} codes. Shown on the first - ${plan.n === 1 ? "pixel" : plan.n + " pixels"} of the strip, steady.`; + ${Math.pow(plan.c, plan.half)} codes. Palindromes because the strip's + data direction is unknown — a code must read the same from either end. Shown steady on the first + ${plan.n === 1 ? "pixel" : plan.n + " pixels"}.`; document.getElementById("patList").innerHTML = candidates.map(c => ` `).join(""); document.querySelectorAll(".patbtn").forEach(b => b.addEventListener("click", () => {