diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
similarity index 94%
rename from CODE_OF_CONDUCT.md
rename to .github/CODE_OF_CONDUCT.md
index b95315876..d07895ba7 100644
--- a/CODE_OF_CONDUCT.md
+++ b/.github/CODE_OF_CONDUCT.md
@@ -4,7 +4,7 @@ This applies everywhere Patternflow people meet: this repository (issues, pull r
## How we treat each other
-The rule was written before there was anything to build, as a short poem, and it is the sixth section of the [manifesto](docs/manifesto.md#6-how-we-treat-each-other). In full:
+The rule was written before there was anything to build, as a short poem, and it is the sixth section of the [manifesto](../docs/manifesto.md#6-how-we-treat-each-other). In full:
> 아무것도 몰라도, 하고자 한다면 만들 수 있습니다.
> 도와주는 게 당연합니다. 고마운 게 당연합니다.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index e67ad6201..cdcde0f89 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -13,7 +13,7 @@ contact_links:
url: https://discord.gg/Vr9QtsxeTk
about: New here? Join the server, then jump into the channels above.
- name: Getting help — every venue in one table
- url: https://github.com/engmung/Patternflow/blob/main/SUPPORT.md
+ url: https://github.com/engmung/Patternflow/blob/main/.github/SUPPORT.md
about: Where to ask what, including the venue that works where Discord is blocked.
- name: Contributing guide
url: https://github.com/engmung/Patternflow/blob/main/CONTRIBUTING.md
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 776794203..24ab53dcb 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -9,5 +9,10 @@
## Tested
+## Which row
+
+
---
- [ ] Touches `web/` → it builds (`npm run build`)
+- [ ] Touches `firmware/` → every composition builds (`./firmware/bundles/build.sh all`), and the core still names no feature (`python firmware/toolchain/check_boundaries.py`)
+- [ ] Touches a markdown file → its relative links resolve (`python3 .github/scripts/check_links.py`)
diff --git a/SECURITY.md b/.github/SECURITY.md
similarity index 57%
rename from SECURITY.md
rename to .github/SECURITY.md
index 98915fe88..772748efa 100644
--- a/SECURITY.md
+++ b/.github/SECURITY.md
@@ -7,13 +7,13 @@ Please report privately through [GitHub's report form](https://github.com/engmun
## What is in scope
- **The community site** ([community.patternflow.work](https://community.patternflow.work)): accounts and sessions, uploads, the sandboxed iframe that runs published patterns, moderation.
-- **The browser flasher and update handoff** ([patternflow.work/flash](https://patternflow.work/flash), `/update`): the firmware images it serves and where they come from.
+- **The browser flasher and update handoff** (the **Flash Patternflow** button on [patternflow.work/pattern](https://patternflow.work/pattern) and in the Play guide, which installs the images under `/flash/`; and [`/update`](https://patternflow.work/update)): the firmware images they serve and where they come from.
- **The build worker** that turns a pattern into a `.pfm` module on the community host.
- **Published firmware images.** They must not carry credentials. Three early releases did carry the maintainer's Wi-Fi credentials, which is why the release workflow now refuses an image built with `patternflow_secrets.h`. If you find one that slipped through, that is a report.
## What is a documented trust model, not a vulnerability
-The device's HTTP server on the LAN has **no authentication**: anyone on the same network can call any `/api/*` route, including `POST /update`. This is deliberate and documented in [docs/rest-api.md](docs/rest-api.md#transport) — the panel is an instrument on a home or venue network, with the same posture as ArduinoOTA's default. `PF_WEBUPDATE_ALWAYS_ARMED 0` is the one lever that narrows it. Reports that amount to "the LAN API is open" will be closed with a pointer here; reports of a way to reach that API from *outside* the LAN, or from a web page on another origin, are in scope.
+The device's HTTP server on the LAN has **no authentication**: anyone on the same network can call any `/api/*` route, including `POST /update`. The same holds on the panel's own hotspot (`patternflow-xxxx`, raised when it finds no network it knows), whose password is the documented default `patternflow` until it is changed on the Wi-Fi page. This is deliberate and documented in [docs/rest-api.md](../docs/rest-api.md#transport) — the panel is an instrument on a home or venue network, with the same posture as ArduinoOTA's default. `PF_WEBUPDATE_ALWAYS_ARMED 0` is the one lever that narrows `/update`; the hotspot takes its own password, or is switched off, on the Wi-Fi page. Reports that amount to "the LAN API is open" will be closed with a pointer here; reports of a way to reach that API from *outside* the LAN, or from a web page on another origin, are in scope.
## Supported versions
diff --git a/SUPPORT.md b/.github/SUPPORT.md
similarity index 51%
rename from SUPPORT.md
rename to .github/SUPPORT.md
index b5f6e1121..e9dc5ab2b 100644
--- a/SUPPORT.md
+++ b/.github/SUPPORT.md
@@ -6,12 +6,22 @@ One table for "where do I ask". Issues are for things that change this repositor
| :--- | :--- |
| Learn to build, play, make patterns for one or give it sound and MIDI | [The guide](https://patternflow.work/guide) — each step on a 3D Patternflow in the browser. A step that didn't get you through has a **Stuck here?** link, which opens an issue with that step filled in |
| Get unstuck building, soldering or flashing | [Discord → hardware-help](https://discord.com/channels/1497757947827327067/1499907910707187833) — photos and quick back-and-forth |
+| Flash the DevKit for the first time, or get the panel onto Wi-Fi | [The Play guide's Flash chapter](https://patternflow.work/guide/play#flash), or [`BUILD_GUIDE.md` §8](../BUILD_GUIDE.md#8-firmware) in writing. The short version is below the table |
| Show a pattern I made, or get feedback on one | Publish it from the [Pattern Lab](https://patternflow.work/pattern-lab) to the [Community](https://community.patternflow.work/community), then [Discord → patterns](https://discord.com/channels/1497757947827327067/1499908302962819236) |
| Put my finished build on the [build map](https://patternflow.work/inside) | [Share your build](https://github.com/engmung/Patternflow/issues/new?template=share_build.yml) — or post it in Discord |
| Talk about where the project should go, or find people working on the same direction | [Discord](https://discord.gg/Vr9QtsxeTk) — the dev-log and the project channels |
| Report a bug, a wrong or missing doc, or propose a feature | [New issue](https://github.com/engmung/Patternflow/issues/new/choose) — pick the template |
| Report a security problem | [SECURITY.md](SECURITY.md) — privately, not as an issue |
-| Contribute code, hardware files or docs | [CONTRIBUTING.md](CONTRIBUTING.md) |
+| Contribute code, hardware files or docs | [CONTRIBUTING.md](../CONTRIBUTING.md) |
+
+**The first flash, in short.**
+
+1. The DevKit out of the board, on its **left** USB-C port (labelled `USB`), with a cable that carries data — a charge-only cable is the most common reason nothing shows up — in desktop Chrome or Edge. That port is for flashing and data only; the panel is always powered through `J4`, the screw terminal.
+2. **Download mode, every first flash:** hold **BOOT**, tap **RST**, let go of BOOT. Then press **Flash Patternflow** (on the [Pattern tab](https://patternflow.work/pattern) under *Got the hardware?*, or in the Play guide) and pick the port (`USB JTAG/serial debug unit`).
+3. **The Wi-Fi step usually does not appear** after *Installation complete!* That is expected: press **RST** once, press **Flash Patternflow** again, pick the port, choose **Connect to Wi-Fi**, and after about ten seconds of scanning type your network's name exactly (capitals count; 2.4 GHz only) and its password.
+4. **If that will not take,** put the DevKit back in the board and power it: about 15 seconds after it finds no network it knows, the panel raises its own, `patternflow-xxxx` (password `patternflow`; hold **K2** to see the exact name). Not in your phone's list yet? Wait, and refresh the list. Join it (the phone will say the network has no internet, which is true; stay on it), open `http://192.168.4.1/`, and pick your network on the Wi-Fi page.
+
+A board that already runs Patternflow is updated over Wi-Fi from its console's Update page instead — download mode erases the patterns and saved networks.
**If Discord is blocked where you live**, the community site still works for patterns and decks without it, and a bug or a documentation issue here reaches the maintainer the same day. A discussion venue that works everywhere is planned; until it exists, an issue is the fallback for a question too.
diff --git a/.github/workflows/drift.yml b/.github/workflows/drift.yml
new file mode 100644
index 000000000..c41a676e9
--- /dev/null
+++ b/.github/workflows/drift.yml
@@ -0,0 +1,70 @@
+name: Drift checks
+
+# The tree keeps some facts in two places by hand: the BOM and the build
+# guide's parts table, a card on /editions and a folder under
+# firmware/bundles/. Each check in web/scripts/drift/ compares one such pair
+# and prints both sides when they part (docs/rfc-cleanup.md §2.4). check:ci
+# runs them too, but Web CI starts only on web/ and hardware/case/. A pull
+# request that edits only the BOM, a bundle or the changelog runs no web job,
+# and that is the pull request a drift check is for. So this one runs on every
+# pull request.
+#
+# PF_DRIFT_STRICT=1 turns a skip into a failure. At a desk a check skips a side
+# it cannot read: a tag the clone lacks, no pdftotext, no gh login. Here every
+# side can be read, so a skip means this file is missing something.
+# fetch-depth: 0 brings the tags the absolute links point at, and the base
+# commit check:human-lock compares with.
+on:
+ pull_request:
+ push:
+ branches: [main]
+
+concurrency:
+ group: drift-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ issues: read
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: web
+ env:
+ PF_DRIFT_STRICT: "1"
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ cache-dependency-path: web/package-lock.json
+
+ - name: Install dependencies
+ id: deps
+ run: npm ci
+
+ # check:bom-schematic reads the schematic's text layer with pdftotext,
+ # which the runner image does not carry.
+ - name: Install pdftotext
+ run: command -v pdftotext || { sudo apt-get update -q && sudo apt-get install -y -q poppler-utils; }
+
+ # check:roadmap asks gh for the state of each issue a roadmap node names.
+ - name: Drift checks
+ run: npm run check:drift
+ env:
+ GH_TOKEN: ${{ github.token }}
+
+ # Not a word of the maintainer's own writing changes (§2.9). It runs
+ # after a red drift step too, so one push shows both.
+ - name: Locked text
+ if: ${{ !cancelled() && steps.deps.outcome == 'success' && github.event_name == 'pull_request' && hashFiles('web/scripts/human-lock.ts') != '' }}
+ run: npm run check:human-lock
+ env:
+ PF_LOCK_BASE: ${{ github.event.pull_request.base.sha }}
diff --git a/.github/workflows/web-ci.yml b/.github/workflows/web-ci.yml
index 87717e892..3253a76fc 100644
--- a/.github/workflows/web-ci.yml
+++ b/.github/workflows/web-ci.yml
@@ -33,16 +33,17 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 22
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install dependencies
run: npm ci
- # Lint is a hard gate: lint errors can't reach main (warnings still pass).
+ # Lint is a hard gate, and a warning fails it like an error. The cleanup's
+ # step 3 took the site from twelve warnings to none; one more fails here.
- name: Lint
- run: npm run lint
+ run: npm run lint -- --max-warnings=0
# `next build` type-checks only what it bundles; scripts/ and the smoke
# suites are outside that, and a type error there used to surface as a
diff --git a/AGENTS.md b/AGENTS.md
index a66463721..6e8973332 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,11 +8,12 @@ Patternflow is an open-source hardware instrument: four rotary encoders controll
It is standalone (not Eurorack), powered from a 5 V power bank through a screw terminal, and it is not an analog video synthesizer. Sound arrives through the Audio edition (on-board microphone, OSC, MIDI), not the default firmware.
## Repository map
-- `docs/` — contracts and long-form docs: `EDITIONS.md` (the firmware seam — read it before touching firmware), `rest-api.md` / `osc-spec.md` / `midi-spec.md` / `audio-ws-spec.md` (wire contracts integrations are built against), `pfst-v2-spec.md` (show files), `panel-compatibility.md`, `RELEASING.md`, `SERVICES.md` (production server ops), the assembly map (`docs/assembly/`), the manifesto, investigations, and media (build-guide images in `docs/build-guide/`). Root-level guides: `BUILD_GUIDE.md` (the **v3.0 board**), `BUILD_GUIDE_v2.md` (legacy v2.x board), `PATTERN_GUIDE.md` (community → deck → Pattern Lab → device), `FEATURE_GUIDE.md` (writing a firmware feature or edition), `AUDIO_GUIDE.md` (sound into the panel). The live roadmap is patternflow.work/journal and the `/roadmap` page.
-- `firmware/` — PlatformIO/Arduino code for the ESP32-S3. Main sketch folder: `firmware/patternflow/` with `patternflow.ino`, `config.h` (pin mappings, brightness, limits, LED calibration), `net_config.h` (Wi-Fi / Improv / OTA / self-update defaults and the lane scale — core only; a feature's own tunables live in `features//_config.h`, and settings TUNE a feature, they never add one; per-device secrets in gitignored `patternflow_secrets.h`), and `pattern_registry.h` (function-pointer pattern table; compiles in Origin alone — the rest of the curated set ships as the Basics pack). Shared engine code lives in `firmware/patternflow/src/` (`core_display.h` HUB75 driver, `core_encoders.h`, canvas/color/math/noise helpers, Wi-Fi/OTA/web-console modules, the hotspot in `core_hotspot.h`) — core only: OSC, audio, MQTT, weather, shows, MIDI and the clock all live in `features/`. `firmware/patternflow/abi/` is the frozen host ⇄ module contract. Curated patterns live in `firmware/patternflow/presets/` (`preset_origin.h`, `preset_wave_saw.h`, dated presets), each using its own pattern namespace; user patterns are `.pfm` modules uploaded to the device over Wi-Fi, built from `firmware/modules/` via `firmware/toolchain/build_module.py` (the old `custom1.h`–`custom3.h` root slots are gone). Features attach through `firmware/patternflow/features/` (named `addons/` until 2026-08-30; legacy spellings are shimmed for out-of-tree bundles — see docs/EDITIONS.md) — a directory and a descriptor of function pointers, with the core naming none of them (see its README). `firmware/bundles/` names firmwares built from the same tree (the shelf editions `audio` and `performance`, plus `clock` and `midi`, compositions the tree keeps buildable but does not publish; the default build has no bundle): two files each, saying which features compile in and what the build calls itself; `firmware/bundles/build.sh` builds the default or a named one, `shelf.sh` stages a publishable image. Device console pages are plain HTML in `firmware/patternflow/console/`, spliced into `*_index.h` by `firmware/toolchain/console_pages.py`. Tooling lives in `firmware/toolchain/` (repo-level scripts) and `firmware/patternflow/toolchain/` (PlatformIO `extra_scripts`, which must stay sketch-relative). Also: `firmware/encoder_test/` (standalone encoder diagnostic) and `firmware/CUSTOM_PATTERNS.md` (pattern authoring guide).
-- `hardware/` — Hardware designs. `case/` (Blender source in `source/`, STLs by printer bed size, `legacy_v2/` for the v2.x board), `pcb/` (KiCad 10.0 source, Gerbers per revision, schematic PDF), `bom/` (`bom_v3.9.csv` — **the BOM source of truth**; the guide's table is derived from it. `bom_v3.0.csv` is kept for the previous board).
-- `web/` — Next.js site at patternflow.work: landing page (`/`, `/pattern`, `/build`, `/inside` are tabs of one view), the interactive guide (`/guide` is a hub over `/guide/build`, `/guide/play`, `/guide/make` and `/guide/audio`, each with a `/ko` twin; one 3D stage in `components/guide/`, the firmware simulator in `lib/guide/`), Pattern Lab (`/pattern-lab`), community (`/community/**`, SQLite + Drizzle + Better Auth, only on the Pi deployment), browser flasher (`/flash`), the edition shelf (`/editions`; `/variants` redirects there), the feature catalogue (`/features`, reels per feature), device update handoff (`/update`), journal, roadmap. `lib/pattern/` is the pattern runtime and annotations, `lib/lab/` the Pattern Lab, `lib/community/` the community, `lib/ai/` the Gemini client. Architecture doc: `web/ARCHITECTURE.md`. The JS presets in `web/src/lib/presets/` are the source of truth for firmware preset headers.
-- `tools/` — desktop-side helpers: `patternflow-audio-extension` (the browser audio-react extension — also the authoring source the device's `/audio-in` console page is assembled from), `patternflow-audio-android` (phone capture app), `rtpmidi-probe`, and `case-models` (builds the /build preview's per-case 3D models, `web/public/cases//model.glb`, from the case files in the repo; run by hand, not by the site build).
+- `docs/` — contracts and long-form docs: `EDITIONS.md` (the firmware seam — read it before touching firmware), `rest-api.md` / `osc-spec.md` / `midi-spec.md` / `mqtt-spec.md` / `audio-ws-spec.md` (wire contracts integrations are built against, each with a folder of vectors beside it), `contract-vectors.md` (the vectors' format), `pfst-v2-spec.md` (show files, and `pfst-v2-vectors/`, the first vectors), `panel-compatibility.md`, `RELEASING.md`, `SERVICES.md` (production server ops), the assembly map (`docs/assembly/`), the legacy v2.x build guide (`docs/legacy/BUILD_GUIDE_v2.md`), `AUDIO_GUIDE.md` (sound into the panel), the manifesto, investigations, and media (build-guide images in `docs/build-guide/`). The vector folders are `rest-api-vectors/`, `osc-vectors/`, `midi-vectors/`, `mqtt-vectors/` and `audio-ws-vectors/`. Each vector is a JSON file that quotes one sentence of its spec in `says` and gives the steps that show a panel doing it. It follows the spec, not the firmware: where the firmware disagrees, the vector keeps the spec and the disagreement goes to an issue. An edit that rewords a quoted sentence fails its vector until the vector is brought along. Root-level guides: `BUILD_GUIDE.md` (the **v3 board**: v3.9, and v3.0 the same way), `PATTERN_GUIDE.md` (community → deck → Pattern Lab → device), `FEATURE_GUIDE.md` (writing a firmware feature or edition). The live roadmap is patternflow.work/journal and the `/roadmap` page.
+- `firmware/` — PlatformIO/Arduino code for the ESP32-S3. Main sketch folder: `firmware/patternflow/` with `patternflow.ino`, `config.h` (pin mappings, brightness, limits, LED calibration), `net_config.h` (Wi-Fi / Improv / OTA / self-update defaults and the lane scale — core only; a feature's own tunables live in `features//_config.h`, and settings TUNE a feature, they never add one; per-device secrets in gitignored `patternflow_secrets.h`), and `pattern_registry.h` (function-pointer pattern table; compiles in Origin alone — the rest of the curated set ships as the Basics pack). Shared engine code lives in `firmware/patternflow/src/` (`core_display.h` HUB75 driver, `core_encoders.h`, canvas/color/math/noise helpers, Wi-Fi/OTA/web-console modules, the hotspot in `core_hotspot.h`) — core only: OSC, audio, MQTT, weather, shows, MIDI and the clock all live in `features/`. `core_display.h` drives the panel through `src/hub75/`, ESP32-HUB75-MatrixPanel-DMA vendored at tag 3.0.13. Its `UPSTREAM.md` names the upstream commit and each commit taken from upstream since, and `patternflow.patch` holds every line Patternflow changed (`VENDORED.md` says why). An edit under `src/hub75/` rebuilds the patch with the command in `UPSTREAM.md`, never by hand: upstream ends some lines in spaces, and an editor that trims them breaks the hunks. `firmware/patternflow/abi/` is the frozen host ⇄ module contract. Curated patterns live in `firmware/patternflow/presets/` (`preset_origin.h`, `preset_wave_saw.h`, dated presets), each using its own pattern namespace; user patterns are `.pfm` modules uploaded to the device over Wi-Fi, built from `firmware/modules/` via `firmware/toolchain/build_module.py` (the old `custom1.h`–`custom3.h` root slots are gone). Features attach through `firmware/patternflow/features/` (named `addons/` until 2026-08-30; legacy spellings are shimmed for out-of-tree bundles — see docs/EDITIONS.md) — a directory and a descriptor of function pointers, with the core naming none of them (see its README). `firmware/bundles/` names firmwares built from the same tree (the shelf editions `audio` and `performance`, plus `clock` and `midi`, compositions the tree keeps buildable but does not publish; the default build has no bundle): two files each, saying which features compile in and what the build calls itself; `firmware/bundles/build.sh` builds the default or a named one, `shelf.sh` stages a publishable image. Device console pages are plain HTML in `firmware/patternflow/console/`, spliced into `*_index.h` by `firmware/toolchain/console_pages.py`. Tooling lives in `firmware/toolchain/` (repo-level scripts) and `firmware/patternflow/toolchain/` (PlatformIO `extra_scripts`, which must stay sketch-relative). Also: `firmware/encoder_test/` (standalone encoder diagnostic) and `firmware/CUSTOM_PATTERNS.md` (pattern authoring guide).
+- `hardware/` — Hardware designs. `case/` (Blender source in `source/`, STLs by printer bed size, `knobs/`, `legacy_v2/` for the v2.x board, `legacy_lasercut/` for the v1/v2 acrylic drawings, and `remixes/` — community enclosures, one folder each, whose README is its license header), `pcb/` (KiCad 10.0 source, Gerbers per revision, schematic PDF), `bom/` (`bom_v3.9.csv` — **the BOM source of truth**; the guide's table is derived from it. `bom_v3.0.csv` is kept for the previous board).
+- `web/` — Next.js site at patternflow.work: landing page (`/`, `/pattern`, `/build`, `/inside` are tabs of one view), the interactive guide (`/guide` is a hub over `/guide/build`, `/guide/play`, `/guide/make` and `/guide/audio`, each with a `/ko` twin; one 3D stage in `components/guide/`, the firmware simulator in `lib/guide/`), Pattern Lab (`/pattern-lab`), community (`/community/**`, SQLite + Drizzle + Better Auth, only on the Pi deployment), browser flasher (no page of its own: the **Flash Patternflow** button on `/pattern` and in the Play guide, installing `public/flash/manifest.json` and the images in `public/flash/bin/`; the bare `/flash` redirects to `/guide/play#flash`), the edition shelf (`/editions`; `/variants` redirects there), the feature catalogue (`/features`, reels per feature), device update handoff (`/update`), journal, roadmap. `lib/pattern/` is the pattern runtime and annotations, `lib/lab/` the Pattern Lab, `lib/community/` the community, `lib/ai/` the prompt Pattern Lab's Copy prompt puts on the clipboard (the site calls no AI service). Architecture doc: `web/ARCHITECTURE.md`. The JS presets in `web/src/lib/presets/` are the source of truth for firmware preset headers.
+- `shared/` — `constants.json`, the values the firmware and the site both carry: the panel geometries `PANEL_GEOMETRY` picks from, the knob count, the encoder's 24 detents a turn, the bus maximum and the button timings. Two files are generated from it, `firmware/patternflow/src/core_shared.h` (each key with `PF_SHARED_` in front) and `web/src/lib/shared/constants.ts`, and neither is edited by hand. It exists because the lab counted 20 detents a turn, against the encoder's 24, until 31 July 2026.
+- `tools/` — desktop-side helpers: `patternflow-audio-extension` (the browser audio-react extension — also the authoring source the device's `/audio-in` console page is assembled from), `patternflow-audio-android` (phone capture app), `rtpmidi-probe`, `case-models` (builds the /build preview's per-case 3D models, `web/public/cases//model.glb`, from the case files in the repo; run by hand, not by the site build), and `virtual-panel`. `virtual-panel` is the one that is not a client: a Patternflow with no board, the guide's `DeviceSim` (`web/src/lib/guide/deviceSim.ts`) behind the ports the firmware listens on, with the five contracts in `transports/`. It runs each pattern's JS twin, never a `.pfm` module. The vector suites replay against it, so a change to `DeviceSim` is a change to the panel they test. It is a test fixture, and Wi-Fi and firmware updates are not part of it.
- `integrations/` — host-software integrations, each with its own README. `integrations/ableton/` is the Max for Live bridge (knobs → Live parameters over OSC). The Home Assistant integration left this repository on 2026-09-03; its author maintains it separately. Integrations are built against the contracts in `docs/` (`rest-api.md`, `osc-spec.md`, `midi-spec.md`), not against the firmware source. `docs/rest-api.md` also has the table for choosing between HTTP, OSC and MQTT, and the rules that make the device's single-connection web server easy to knock over.
## Hard rules (do not violate)
@@ -20,13 +21,21 @@ It is standalone (not Eurorack), powered from a 5 V power bank through a screw t
2. `hardware/bom/bom_v3.9.csv` is the BOM source of truth. The parts table in `BUILD_GUIDE.md` and the schematic in `hardware/pcb/schematic.pdf` must match it. If you change one, check the other two. (`bom_v3.0.csv` stays for people holding a v3.0 board; it is not the source of truth.)
3. License split is strict: firmware and web code = MIT; hardware designs (PCB, case STLs, Blender source) = CC-BY-SA 4.0. Two separate license files at root: `LICENSE-MIT` and `LICENSE-CC-BY-SA`. Do not merge them; the root `LICENSE` is a pointer to both so GitHub's detector sees both, not a third license.
4. Brand naming: body text = "Patternflow", physical engravings (PCB silkscreen, future case engravings) = "PATTERNFLOW", filenames and URLs = lowercase "patternflow". Never mix these in a single context.
-5. Known issues of the current board are documented in `BUILD_GUIDE.md` section 10; the v2.0 fixes and leftovers are in `BUILD_GUIDE_v2.md` section 10. Reference those sections instead of restating the issues.
+5. Known issues of the current board are documented in `BUILD_GUIDE.md` section 10; the v2.0 fixes and leftovers are in `docs/legacy/BUILD_GUIDE_v2.md` section 10. Reference those sections instead of restating the issues.
6. **The board has exactly one power input: `J4`, the screw terminal. Never describe USB-C as a power option.** v3.9 removed the `USB1` footprint and its `R1`/`R2` CC pull-downs outright, after a USB-C-powered v3.0 board ran for 20–30 minutes and then smoked at a connector pin ([#221](https://github.com/engmung/Patternflow/issues/221)) — the failure is delayed, so "it seems to work" is not evidence. On v3.9 the footprint does not exist; on a v3.0 board already in someone's hands, `USB1`/`R1`/`R2` stay unpopulated. Release notes and changelog entries that record the hold as it stood are history — update current-state docs, leave the record alone.
7. **The firmware core is not the place to put a feature.** `firmware/patternflow/patternflow.ino` and `firmware/patternflow/src/` are the device itself — panel, encoders, pattern loader, Wi-Fi, sleep, console — and every published firmware compiles them **unchanged**. They change for a bug or a real improvement to the device, and nothing else; a feature bends to fit the core, never the reverse. Features attach through the hooks in `firmware/patternflow/features/pf_feature.h`, and the core names none of them — no `#include`, no `#if PF__ENABLED`, not even a feature's name in a string on the screen or in a console page. Before editing the core, ask whether the edit would still be correct on a build that does not contain the feature you are working on; if not, it belongs in `features/`. The full reasoning is [`docs/EDITIONS.md`](docs/EDITIONS.md), and the header comment of `patternflow.ino` lists the regressions this rule exists because of.
+8. During a cleanup window (see [`docs/rfc-cleanup.md`](docs/rfc-cleanup.md)) no PR changes behaviour; moves and renames carry a redirect or an updated link map.
## Common commands
- Web dev server: `npm run dev` (inside the `web/` directory)
-- Web production build: `npm run build` (inside the `web/` directory); the smoke suites are `npm run check:*` and CI runs all of them.
+- Web production build: `npm run build` (inside the `web/` directory); the smoke suites are `npm run check:*`, and CI runs the ones `npm run check:ci` chains together.
+- Web lint and typecheck: `npm run lint` and `npm run typecheck` (inside `web/`). CI runs lint with `--max-warnings=0`, so one warning fails a pull request the way an error does. The rules in `eslint.config.mjs` stay as they are. An exception is an `eslint-disable-next-line` comment for one rule, with its reason after `--`. `typecheck` also reads `tools/virtual-panel/`, and lint does not.
+- Shared constants: change `shared/constants.json`, then run `npm run gen:shared` (inside `web/`) and commit the two files it writes. `npm run check:shared` fails while either is stale. It also reads the two copies the generator cannot write, the 1000 in the frozen `abi/pf_params.h` and the 128 and 64 in `firmware/toolchain/build_module.py`, and fails when they no longer match the JSON.
+- Drift checks: `npm run check:drift` (inside `web/`) runs each file in `web/scripts/drift/`, and each compares two things the tree keeps by hand, such as `hardware/bom/bom_v3.9.csv` and the build guide's parts table. `npm run check:` runs one: `links-absolute`, `bom`, `bom-schematic`, `features`, `editions`, `presets`, `roadmap`, `changelog` or `shared`. `check:ci` ends with it, and `.github/workflows/drift.yml` runs it on every pull request with `PF_DRIFT_STRICT=1`. At a desk a check skips a side it cannot read (a tag the clone lacks, no `pdftotext`, no `gh` login); in that workflow the skip fails. One check has no script of its own: `markers` fails on a `TODO` or `FIXME` in first-party code, and runs alone as `npx tsx scripts/drift.ts markers`.
+- Contract vectors: `npm run check:rest-vectors` (inside `web/`) gives each file in `docs/rest-api-vectors/` a fresh virtual panel on free ports and prints what the vector expects beside what the panel did. `check:osc-vectors`, `check:midi-vectors`, `check:mqtt-vectors` and `check:audio-vectors` do the same for their folders. The panel's clock stands still between steps, so a 500 ms release costs no real time. `check:virtual-panel` runs each preset with a JS twin on a panel of its own for two seconds of panel time. A thrown error fails it. The six are files in `web/scripts/drift/`, so `check:drift` and `drift.yml` run them with the drift checks.
+- Virtual panel: `npm run virtual-panel -- --port http=8080` (inside `web/`) starts one to talk to by hand. HTTP is on 80 as on the device, and Linux keeps ports below 1024 from a process that is not root, so the flag moves it; `--port '*=0'` takes a free port for each and prints them. `--features mqtt` composes a feature in by its directory name in `firmware/patternflow/features/`. `GET /frame.png` is the frame as the LEDs would show it, and `/vp/` is a hand and the clock. The firmware has neither. `tools/virtual-panel/README.md` has the rest.
+- Locked text: `npm run check:human-lock` (inside `web/`) fails when a word changes in the maintainer's own writing, the regions `docs/rfc-cleanup.md` §2.9 lists plus each path in `docs/.human-written`. Markup and link paths in them may change. Text moved word for word passes, and the move needs the maintainer's approval on the PR. A typo is reported in the PR, not fixed. `drift.yml` runs it on pull requests against the base commit; at a desk the base is the merge-base with `origin/dev`, or with `origin/main` in a clone that has no `dev`.
+- Voice lint: `npm run check:voice` (inside `web/`) reads the lines a branch adds since that same base and prints `file:line` for each banned word from `docs/voice-banned.txt`, list of three, opener that says what the text is about, or closing summary. Run it by hand before opening a PR that adds prose; CI does not run it. Files given as arguments are read whole, and `-` reads a PR description from stdin.
- Firmware compilation: `./firmware/bundles/build.sh` (PlatformIO, bundled toolchain — not the Arduino IDE; the IDE path cannot build the editions' library set). That builds the default: the device with no features. Named editions are `./firmware/bundles/build.sh audio`, `performance`, `clock` and `midi` (the last builds in its own PlatformIO env, named in `bundles/midi/env`, because its USB port must be the OTG controller — a build flag, not a setting). `./firmware/bundles/build.sh all` builds every composition and scans each binary for per-feature marker strings, proving each image carries exactly its features — run it before pushing anything that touches firmware.
- **Changing anything in the firmware core means building every composition** — `build.sh all` is that rule as one command. A hook change that compiles against the default is not tested; the default has no features to break. The boundary rule itself is enforced by `firmware/toolchain/check_boundaries.py` in CI: core referencing a feature namespace, including from `features/`, branching on a feature flag, or naming a feature in a core console page fails the build. `check_presets.py` (also CI) keeps `web/src/lib/presets/` and `firmware/patternflow/presets/` in step.
- Console pages: edit `firmware/patternflow/console/*.html`, preview with `python firmware/toolchain/console_serve.py`, then `python firmware/toolchain/console_pages.py build` (CI checks the headers stayed in sync).
@@ -42,3 +51,30 @@ It is standalone (not Eurorack), powered from a 5 V power bank through a screw t
- New users: `README.md` → `BUILD_GUIDE.md`
- Contributors: `CONTRIBUTING.md` → `docs/REPOSITORY.md` → `docs/EDITIONS.md` (firmware) · `web/ARCHITECTURE.md` (web)
- Version history: `CHANGELOG.md`
+
+## Writing voice (hard rule)
+
+Everything an agent writes that a person will read — docs, README sections, release notes, console page copy, PR descriptions, comments — must read as if the maintainer wrote it. Not "polished". Written.
+
+Voice reference: `docs/voice.md` describes the project voice. Read it before writing anything longer than a sentence. Match its register (Korean: 해요체 or 합니다체 as the file already uses; English: plain, first person where the project already uses it), its sentence length, and its habit of naming the specific thing — the part number, the detent count, the date — instead of the category.
+
+Do not:
+- Open with what the document is about ("This document describes…", "이 문서는 …을 설명합니다"). Start with the thing.
+- Close with a summary that repeats the body, or with "In conclusion" / "결론적으로".
+- Turn prose into bullet lists, or bold phrases for emphasis. Bullets are for lists of things.
+- Use these words or their Korean equivalents: seamless, robust, leverage, comprehensive, streamline, empower, cutting-edge, ensure, utilize, delve, "it's important to note", "here's", 효율적으로, 원활하게, 강력한, 최적화된, 혁신적인, 다양한, 전반적으로, 참고로, 살펴보겠습니다.
+- Write in threes ("fast, simple, and reliable"). One reason is a reason; three is a pattern.
+- Join clauses with em dashes (—). End the sentence and start another.
+- Hedge every claim. Say it, or leave it out.
+- Add emoji, decorative headers, "Note:" callouts, or a "Generated by" line.
+- Mention that an AI, an agent or a tool wrote or helped write the text. Ever.
+- Reformat an existing human-written paragraph into another structure.
+
+Do:
+- Prefer one specific example over a general statement.
+- Vary sentence length. A short one can stand alone.
+- Keep the project's own terms (core, feature, edition, bundle, preset, module) exactly as the existing docs use them.
+- For PR descriptions and commits: imperative first line, one line of why, nothing else. No "This PR introduces…".
+- When unsure of tone, write less.
+
+Any new or rewritten text over 20 lines goes through `web/scripts/voice-lint.ts` (banned phrases, triplets, opener and closer patterns) before the PR is opened, and a person reads it before merge.
diff --git a/BUILD_GUIDE.md b/BUILD_GUIDE.md
index dfb0b5fef..f6fe29b44 100644
--- a/BUILD_GUIDE.md
+++ b/BUILD_GUIDE.md
@@ -1,6 +1,6 @@
# Patternflow v3 -- Build Guide
-> **Building a v2.x board?** Use the [v2 build guide](BUILD_GUIDE_v2.md) instead — v2 and v3 parts are **not** interchangeable.
+> **Building a v2.x board?** Use the [v2 build guide](docs/legacy/BUILD_GUIDE_v2.md) instead — v2 and v3 parts are **not** interchangeable.
This guide walks you through building a Patternflow on the v3 board from scratch. v3.9 is the current revision; a v3.0 board follows the same steps (its USB-C footprint stays unpopulated, see §2). No prior soldering experience needed — every joint is big, forgiving through-hole, and the board was deliberately kept that simple so a first-time solderer can finish it.
@@ -23,7 +23,7 @@ This guide walks you through building a Patternflow on the v3 board from scratch
4. [3D Printing](#4-3d-printing)
5. [PCB Assembly](#5-pcb-assembly)
6. [Case Assembly](#6-case-assembly)
-7. [Wiring & First Power-Up](#7-wiring--first-power-up)
+7. [Wiring](#7-wiring)
8. [Firmware](#8-firmware)
9. [Final Checks](#9-final-checks)
10. [Known Issues & Design Notes](#10-known-issues--design-notes)
@@ -217,7 +217,7 @@ By now the enclosure halves you bonded in Section 4 have cured and the board is
- The power bank slides into the internal compartment behind the board bay.
- The four black knobs press-fit onto the encoder shafts — save them for **last**, after the Section 9 checks pass.
-## 7. Wiring & First Power-Up
+## 7. Wiring
1. Connect the HUB75 ribbon from `J1` to the panel's `IN` connector.
2. Wire `J3` to the panel's power cable (+5V / GND — double-check polarity). The wiring run is shown at **06:57** in the [assembly video](https://youtu.be/J9C9bZgkNKs).
@@ -233,44 +233,38 @@ By now the enclosure halves you bonded in Section 4 have cured and the board is
*Cable already through the enclosure hole, wires clamped into `J4`, board about to go in.*
-4. With power off, seat the ESP32 DevKit in its sockets (orientation per silkscreen), then power on.
+4. **Leave the ESP32 DevKit out of its sockets for now.** It gets its firmware on its own, out of the board (§8.1), and goes in only after that, with the power bank unplugged. The first power-up is §8.1, step 5.
## 8. Firmware
-The ESP32-S3 module is flashed **separately, outside the PCB**. Already seated it? Just pop it gently out of the sockets, flash it, plug it back in, and connect power — done. **One firmware image serves every board generation** — the pin map is identical on v2.x and v3.0, so there is no board selection to get wrong.
+The ESP32-S3 module is flashed **on its own, out of the board**, once; after that, patterns and firmware updates arrive over Wi-Fi. Already seated it? Lift it gently straight up out of the sockets first. **One firmware image serves every board generation** — the pin map is identical on v2.x and v3.0, so there is no board selection to get wrong.
### 8.1 Browser flash (recommended)
-No installation required — desktop **Chrome or Edge** only (Web Serial; Firefox/Safari won't work).
+No installation required — desktop **Chrome or Edge** only (Web Serial; Firefox/Safari won't work). Steps 1–6 follow the Play guide's [Flash chapter](https://patternflow.work/guide/play#flash) in the same order, and step 7 is its [Patterns chapter](https://patternflow.work/guide/play#patterns).
> 🎬 **See it before you do it.** [patternflow.work/guide/play](https://patternflow.work/guide/play) walks through this section — the port, BOOT and RST, the Wi-Fi step, the Basics pack — on a Patternflow you can turn, and every step has a link for telling us where you got stuck.
> 🔌 **Use the LEFT USB-C port** — the one on your left when the two ports face you. On the ESP32-S3 DevKit that's the board's **native USB** port (labeled `USB`); the browser flasher (Web Serial + Improv) talks to it directly. The right-hand port goes through a separate USB-to-UART bridge chip and is the one Arduino IDE uses (§8.2) — the flasher may still write the firmware through it, but the Wi-Fi step never appears there, because the firmware only listens for it on the left port.
-1. Visit **[patternflow.work](https://patternflow.work)** on a desktop browser.
-2. Connect the ESP32-S3 to your computer with a USB-C **data cable**, using the **left port** (see above).
-3. Open the **Pattern** tab, find **Got the hardware?**, click **"Flash Patternflow"**, pick the serial port, and follow the on-screen steps. A new install erases the module first; a module already running Patternflow is updated in place and keeps its patterns and Wi-Fi.
-4. **Wi-Fi.** When the install finishes, press **Next** and the flasher asks for your network (Improv-Serial). Type the name exactly — there is no list to pick from, and it is case-sensitive — and use a **2.4 GHz** network; the ESP32-S3 cannot see 5 GHz. If the install ends without asking, press **RST** on the module once, click **Flash Patternflow** again, pick the port, and choose **Connect to Wi-Fi**. Some modules do not restart into Patternflow on their own after flashing; the button does it.
-5. Disconnect, seat the module back into the board sockets (orientation per silkscreen), and connect power.
-6. **Load the patterns.** The image ships with **Origin only** — the rest live on the device's filesystem instead of inside the firmware, which is what freed the memory for everything else. Open **[the decks shelf](https://community.patternflow.work/community/decks)** and press **Install to my board** on the **Basics** pack: 33 patterns, one click, no account. Your browser fetches the pack and hands it to the board over your Wi-Fi, so the board is never talking to the internet itself. On a freshly flashed board the device's page first says **Storage needs formatting**: press **Format storage**, confirm, and the pack installs by itself as soon as the format is done. On Android, `patternflow.local` does not resolve — type the board's IP address (hold **K2**) into the **Device address** field on the Basics card instead.
+1. **Connect the module on its own.** Hold it antenna up, ports toward you, and connect its **left port** (see above) to your computer with a USB-C **data cable**. A charge-only cable is the usual reason nothing shows up.
+2. **Download mode first — every first flash, before you click anything in the browser.** Press and **hold BOOT**, **tap RST** (labelled EN on some modules), then **let go of BOOT**. The module now waits for its firmware.
+
+ In download mode the flasher can't tell the module already runs Patternflow, so it **erases it** — installed patterns and saved Wi-Fi included. Fine on a new build. A board already running Patternflow updates over Wi-Fi instead, from its console's **Update** page, and keeps everything.
+3. **Flash.** On **[patternflow.work](https://patternflow.work)**, open the **Pattern** tab, find **Got the hardware?** and click **Flash Patternflow** (the Play guide's Flash chapter has the same button). Pick the line like `USB JTAG/serial debug unit (COM4)` — the number depends on which USB port you used — and install. It takes about two minutes. Not in the list? Hold BOOT, tap RST again, then click the button again.
+4. **Wi-Fi — expect it to take a second go.** Usually the install ends without asking for your network. Nothing failed: after download mode the module just hasn't restarted into Patternflow yet. Close the dialog, press **RST** once, click **Flash Patternflow** again and pick the port: this time it opens on Patternflow's own menu instead of installing. Choose **Connect to Wi-Fi**, let the short scan finish (it lists nothing, by design), and type your network's name exactly — it is case-sensitive — and its password. Use a **2.4 GHz** network; the ESP32-S3 cannot see 5 GHz. (Sometimes the same form comes straight after the install instead; fill it in there the same way.) Still won't join? Carry on: step 6 does it from a phone.
+5. **Back in, power on.** Unplug the USB cable. With the power bank unplugged, seat the module in the board's sockets the way the silkscreen shows: its USB-C ports toward the board's `USB` mark, all 44 pins in, neither row shifted by one. Then plug the `J4` lead into the power bank — never the module's USB ports (Section 2). Within a second or two the panel lights with **Origin**, a red, squarish pattern.
+6. **Still no Wi-Fi? The panel is a network itself.** Give it about 15 seconds after power-on, then hold **K2**, the top-left knob, for a second. `CONNECTED` and an address: it's on your Wi-Fi, carry on. `HOTSPOT` and a name like `patternflow-a1b2`: it found no network it knows, so it raised its own. Join that network from a phone or laptop — the password is `patternflow` — and open `http://192.168.4.1/`. The phone will say the network has no internet; that is true, and it stays connected. On the **Wi-Fi** page, pick your network, add its password and save: the panel shows `JOINED` and its new address, or why it couldn't. Neither on K2 yet? Wait a few seconds and hold it again. Not in your phone's list yet? Refresh it (or switch Wi-Fi off and on), and look for the exact name K2 shows.
+7. **Load the patterns.** The image ships with **Origin only** — the rest live on the device's filesystem instead of inside the firmware, which is what freed the memory for everything else. Back on your own Wi-Fi, open **[the decks shelf](https://community.patternflow.work/community/decks)** and press **Install to my board** on the **Basics** pack: 33 patterns, one click, no account. Your browser fetches the pack and hands it to the board over your Wi-Fi, so the board is never talking to the internet itself. On a freshly flashed board the device's page first says **Storage needs formatting**: press **Format storage**, confirm, and the pack installs by itself as soon as the format is done. On Android, `patternflow.local` does not resolve — type the board's IP address (hold **K2**) into the **Device address** field on the Basics card instead.
> 🎛️ **One pattern after flashing is correct, not a failed install.** It used to be 34 baked into the image. They moved out so that patterns can be added and removed without reflashing, which is also how anything you make yourself reaches the panel.
-> 🔍 **Nothing in the port list?** A module that is running its own firmware does not always announce itself, so put it into download mode by hand before you look again:
->
-> 1. Press and **hold BOOT**
-> 2. **Tap and release EN / RST** (the reset button)
-> 3. **Release BOOT**
->
-> The picker should now offer a line like `USB JTAG/serial debug unit (COM4) – Paired`. The number depends on which USB port you used.
->
-> In download mode the firmware cannot answer the flasher, so it treats the module as new and **erases it** — installed patterns and saved Wi-Fi included. Press **RST** alone to bring a module back to normal first if you only meant to update it.
+> 🔍 **Still nothing in the port list?** Put the module into download mode again (step 2) and look again. Still nothing? It is almost always the cable — a charge-only USB-C one enumerates nothing at all. **There is no driver to install on this port**: the ESP32-S3 handles USB itself, so the CP2102 / CH34x links on the flasher's troubleshooting screen do not apply here.
>
-> Still nothing? It is almost always the cable — a charge-only USB-C one enumerates nothing at all. **There is no driver to install on this port**: the ESP32-S3 handles USB itself, so the CP2102 / CH34x links on the flasher's troubleshooting screen do not apply here.
+> Only meant to update a module that already runs Patternflow, over USB? Skip download mode: plugged in as it is (or after a press of **RST** alone), the flasher recognises Patternflow and updates it in place, keeping its patterns and Wi-Fi. The console's **Update** page does the same over Wi-Fi.
-> 📶 **Changing Wi-Fi later.** The network you set during flashing is **saved on the device and reused on every boot**. The panel remembers up to five networks — home, studio, a venue — and tries the most recent first, so moving it does not mean reflashing. Add or forget networks on the device's **Wi-Fi** page (the console at `patternflow.local`, or the board's IP address). A plain Arduino IDE re-upload does **not** clear the stored networks; **Tools → *Erase All Flash Before Sketch Upload*** does.
+> 📶 **Changing Wi-Fi later.** The network you set during flashing is **saved on the device and reused on every boot**. The panel remembers up to five networks — home, studio, a venue — and tries the most recent first, so moving it does not mean reflashing. Add or forget networks on the device's **Wi-Fi** page (the console at `patternflow.local`, or the board's IP address). Somewhere with none of them, it raises its own `patternflow-xxxx` network again, as in step 6 (every image the flasher and the editions page install does; the older clock and MIDI try-out images from the features page don't). The hotspot's mode (`auto`, `always`, `off`) and password are on the same page. A plain Arduino IDE re-upload does **not** clear the stored networks; **Tools → *Erase All Flash Before Sketch Upload*** does.
-> 📡 **No Wi-Fi where you are? The panel is one.** *(Firmware after v3.10.4; the Performance edition v0.4.0 has it already.)* About fifteen seconds after it finds no known network, the panel raises its own hotspot: `patternflow-xxxx` (the name on its NETWORK screen - hold K2), password `patternflow`. Join it from a phone or a laptop and open `http://192.168.4.1/` - the whole console, including the Wi-Fi page, so you can add the network for wherever you are next and the panel joins it at once. The phone will say the network has no internet; that is true, and it stays connected. Mode (`auto`, `always`, `off`) and the password are on the console's Wi-Fi page.
*Photos from the v2 guide — the flashing flow is identical on v3.*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d19b7130d..4ef9e5b18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,18 +4,210 @@ All notable changes to Patternflow will be documented in this file, newest first
## [Unreleased]
+### Firmware
+
+- **The MIDI page counts 24 detents a turn, as the encoder has.** The Sensitivity readout worked out its per-turn figure from 20, so 1 : 1 read 20/turn and 6.4 turns end to end, where a turn of the PEC11R-4220F-S0024 sends 24 steps and the sweep takes 5.3. What the panel sends did not change.
+- **The vendored HUB75 driver names the upstream commit it came from.** `firmware/patternflow/src/hub75/UPSTREAM.md` gives ESP32-HUB75-MatrixPanel-DMA at tag 3.0.13 (`a622186`), copied on 31 July 2026, and `patternflow.patch` beside it is every change since: `diff -ruN` of upstream's `src/` against the folder, which applied to a clean checkout of the tag gives the folder back. `VENDORED.md` and the comments in `core_display.h` and `core_canvas.h` point at the patch instead of counting the additions. The firmware is unchanged.
+- **The vendored HUB75 driver takes upstream's fix for its config constructor.** `HUB75_I2S_CFG` now sets `line_decoder` from its argument (upstream `7bf3b6c`), so the field is `TYPE138` instead of whatever was on the stack where `initDisplay()` builds the config. `src/hub75/UPSTREAM.md` lists the commit and its rebuild cherry-picks it, so `patternflow.patch` still holds only Patternflow's changes and comes out byte for byte the same.
+- **The firmware reads the values it shares with the site from `src/core_shared.h`.** `config.h` takes the panel geometries and their sizes from it, `net_config.h` writes the lane scale as 24 detents times two turns, and the encoders, the bus and the sketch take the debounce, the long press and the bus scale. All come from `shared/constants.json`. No value changed, and all five images build to the same size.
+
### Hardware
+- **`check:bom` and `check:bom-schematic` hold the BOM to the build guide and the schematic.** Hard rule 2 as two checks. A designator, an MPN or a quantity that `hardware/bom/bom_v3.9.csv` and the parts tables in section 1 of `BUILD_GUIDE.md` do not share turns `check:ci` red, and so does a designator that the BOM and `hardware/pcb/schematic.pdf` do not share. The schematic's designators come out of its text layer through `pdftotext` (poppler); where it is not installed the check skips, and under `PF_DRIFT_STRICT=1` the skip fails. Part names on the board are not compared, since the guide words them its own way. Off the board, where there is no MPN, the rows pair in order and each guide row keeps every word of its BOM row's part.
- **Besoiobiy's printed case is the second enclosure remix** (`hardware/case/remixes/besoiobiy-printed/`). Two long pieces glued side by side, a frame round the panel and a box for the board and the power bank, closed at the back by four screwed-on covers, with the panel bolted through the case into its own M3 sockets. Its README takes the place of the build guide's sections 4 and 6 and sets the order of 7 to 9; check your panel against it first, because the BOM's panel takes M4 screws on another hole pattern. The build guide's printing section and the assembly map point at it.
+- **Mykyta Bilous' horizontal desktop case is the third enclosure remix** (`hardware/case/remixes/mbchars-horizontal-desktop-printed/`, [#488](https://github.com/engmung/Patternflow/pull/488) by [@mbchars](https://github.com/mbchars)). The panel lies the long way across on two removable stands, with the four controls on the right. Three front sections print face down, two white frames and an orange control section, and four joint bars on brass inserts hold them together without glue; two rear covers open onto the electronics and carry wall-mount openings. There is no power-bank bay: a separate PDSink 303PDSink01 trigger module in the control section feeds J4, the screw terminal, at 5 V. One Bambu Studio project holds all nine plates, with illustrated assembly instructions and the BOM changes beside it.
### Web
+- **Pattern Lab no longer has the AI gallery or a Gemini key.** The Gallery panel, its batch generation and the key field are gone, and so is the `@google/genai` dependency: the site calls no AI itself. Copy prompt stays, with the v-field/rgb choice beside it in the Code tab now. Opening the Lab removes the gallery's saved patterns and the key from the browser, and a saved dock layout opens without the Gallery tab instead of falling back to the default.
+- **Pattern Lab has text layers.** + Text adds a layer you type into, set in the site's pixel font, a generic system family, or a .ttf, .otf, .woff or .woff2 file from your own device. The file is read in the browser and never uploaded, and the browser keeps it only when you tick remember. Size, X and Y, alignment, one color or the layer's Color Ramp across or down, sharp or soft edges; a drag on Preview moves the text, by touch too. The text is drawn into a bitmap, so it blends, masks, renders in Graphic Export and travels in both exports as pixel art.
+- **Pattern Lab's text layer copies out as code, and its firmware header is checked against the preview.** Copy as code in the Text panel hands the drawn letters to an AI chat as a pattern that needs no font, with a marked place to warp them and another to change their light. To hardware says a stack of text and pixel layers is a finished header with nothing to translate. A new test compiles that header with the host's C++ compiler and finds it lights the same pixels as the Lab preview and the flattened JS, apart from exact half steps at 50% and 25% opacity, where the browser's doubles and the board's floats round one level apart.
+- **Pattern Lab's text layers scroll, twinkle or run their colour ramp, at the speed of a knob.** The Text panel's Effect picks one, and the knob it names sets the speed; turned all the way down, the letters stop. A scroll carries the whole line, so words longer than the panel pass through it. The flattened JS and the To hardware header move the same LEDs the preview does, frame for frame, which a test checks by compiling the header on the host.
+- **Tapping a tab in Pattern Lab on a phone no longer closes another panel.** Touch screens showed a × on every tab, and a tab half out of the strip scrolled a × under the finger before the click landed. The tab overflow menu also opened under the panel below it.
+- **A new code layer in Pattern Lab follows the knobs and lets the pattern beneath show.** The starter "+ Code" adds read no knob and drew every pixel opaque, so it hid the layer under it and turning a knob changed nothing. It now scrolls by knob 2 and spaces its bands by knob 3, and over another layer its ramp starts with the low end transparent.
+- **Pattern Lab's Panels menu brings a hidden panel to the front instead of closing it.** On a phone every panel but Preview is a tab in one group, so picking Knobs while Code was in front closed Knobs. A panel behind another tab now comes forward and the menu closes. Picking a panel that is already in front still closes it.
+- **Pattern Lab's To hardware waits for a translated layer before Next.** With two code layers, Next opened on "0 done" and called a header of stubs ready, and the board showed black. Next now opens once one layer is translated, and the next step names each layer that is still a stub.
+- **Pattern Lab's To hardware keeps only the header from a pasted chat answer.** With one code layer, an answer pasted whole went into the .h with the model's words and the ``` fence, and Copy .h on an empty box copied nothing and said Copied. The .h now starts at `#pragma once` (with the comment lines above it) and stops at the closing fence, and Copy .h waits for a header.
+- **Pattern Lab's To hardware translates the layer that shows.** With one code layer showing and a hidden or 0% code layer picked in Layers, the conversion prompt carried the hidden layer's code, so the board got a pattern the preview and the community upload did not show. To hardware and the upload now pick the layer the same way.
+- **Pattern Lab's stacked C++ header compiles whatever the knobs and names.** A knob that only became a whole number once rounded (a 0..360 knob at Director bus value 175) came out as `63f`, and a backslash at the end of the pattern or knob name escaped its closing quote. g++ stopped on both.
+- **Pattern Lab sends one code layer out at its preview brightness.** A single code layer under 100% opacity or on multiply showed dimmed or black in the preview, but Share published its bare code and To hardware ported it the same way, so the community and the board lit it at full brightness. It now goes out as a stack of one that keeps its opacity and blend.
+- **A Pattern Lab code layer written as `const draw = …` still runs once stacked.** The flattened export pasted each layer's code under its own `var setup, update, draw;`, so a layer that declared them with `const`, `let` or `class` failed to load with "Identifier 'draw' has already been declared" and the published stack never ran. Each layer's code now runs in a scope of its own.
+- **A stacked Pattern Lab code layer goes out with the pixels the preview shows.** The flattened export cut coordinates with `| 0` and let `Uint8ClampedArray` round colours, so a point at y −0.5 lit row 0, a red of 0.5 went out as 0 and `1 / 0` as 255. It now floors and rounds half up as the preview does.
+- **Typing over a code layer's `@matrix` line no longer crops the pixel and text layers.** The Lab took the line up at every keystroke, so typing 96 over the 128 of `// @matrix 128x64` passed through 12x64 and 9x64 and left a full pixel layer with 576 of its 8,192 cells. The frame now changes when focus leaves the editor, a line out of range or gone leaves it as it is, and Undo in the Pixel panel brings back what a smaller frame cut off.
+- **Pattern Lab's To hardware keeps the pasted header.** Esc, a press outside the window or a reload emptied the box, and the way back was to ask the model again. The paste is now kept for the tab against the prompt it answers, so it comes back on the next open, and an edit to the code, the ramp or the matrix drops it.
+- **Pattern Lab's dialogs stay open when a drag that began inside them ends outside.** Selecting text in To hardware, Code guide or Import image and letting go on the backdrop closed the window; only a press that starts on the backdrop closes it now.
+- **Pattern Lab's Text panel turns a text effect's speed knob in place.** A slider beside Speed knob moves the same knob the Knobs panel does, so on a phone an effect's speed no longer takes a trip to the Knobs tab and back.
+- **Pattern Lab's paste box asks in English, and the Code and Pixel panels name a selected text layer.** When the browser refused the clipboard, Paste opened a box that asked in Korean on an English page. With a text layer selected, the Code panel called it pixel art and the Pixel panel called it a code pattern; each now names the layer's kind and the panel that edits it.
+- **Pattern Lab's knob sliders turn one encoder detent per arrow key.** An arrow key moved a knob by the slider's 0.001 step, so Freq's 10..120 took 110,000 presses. It now moves one detent, 48 presses end to end as on the device, in the Knobs panel and beside a text effect's speed picker.
+- **Pattern Lab's Layers and Knobs controls are fingertip-sized on touch screens.** On a 390px phone Del was 37×28px, the layer eye 20×17px and a knob's Push 40×22px. Where there is no hover, the layer bar buttons, the eye, the mask, opacity and blend rows and Push are now 44px tall, as the Text panel's fields already were.
+- **Pattern Lab's Pixel panel opens a 128×64 picture at 2× on a 390px phone.** The status bar's alt-click, right-drag and middle-drag hint wrapped to a second line on touch screens, which have none of those gestures, and left the picture a 151px viewport that fitted at 1×. Touch screens no longer show it.
+- **Pattern Lab's Pixel zoom buttons keep the middle of the view still.** − and + zoomed from the viewport's top-left corner, so one + at 10× moved the middle of the 649px desktop panel from column 64 to 58. They now hold the cell in the middle the way the wheel holds the one under the cursor.
+- **Pattern Lab's Share window closes on Esc.** It closed only on a press outside, and its title field took the focus before anything noted the Share button, so the focus had nowhere to go back to. One Esc now closes it and puts the focus back on Share, and inside To hardware it still closes Share alone and keeps the header.
+- **Add firmware header names the Lab's To hardware button.** Its note, and the one in Port this pattern, sent people to Copy C++ prompt, a button Pattern Lab no longer has.
+- **A community card reads a fine knob the way the Lab does.** A knob declared `0.0001..0.0004` started below its range at 0 and reached the pattern as -0.1 where the Lab gives 0.5. One declared `0.001..0.002` started at its top, and the card's wheel rounded both to three decimals. Every place that reads a knob as 0..1, and the card's starting middle, now go through `lib/pattern/controls.ts`.
+- **A downloaded community header carries one licence line.** A Pattern Lab header published under CC BY 4.0 downloaded with the scaffold's own `CC-BY-SA-4.0` SPDX line on line 1 above the author's `CC-BY-4.0`; the download now drops SPDX lines from the comments above the code before it adds the licence block.
+- **Pattern Lab's stylesheets drop 13 classes no component used.** The 220 lines of rules that styled them are gone, and the Lab CSS test now fails when a Lab module defines a class nothing reads.
+- **The Pattern and Build tabs' small notes keep their own size.** The global `.panel-body p` rule outranked each note's single module class, so the advanced note and both To hardware notes ran at 20px instead of 14px, and the Build tab's other-paths line at 20px instead of 15px, each with a 28px bottom margin. At 1440 the To hardware block is 363px instead of 572px.
+- **Pattern Lab's modules export only what another file reads.** knip found 64 exports in `lib/lab` and `app/pattern-lab` with no importer; two names with no reader at all are gone and the other 62 are file-local.
+- **Pattern Lab's twice-written helpers live in one place each.** The FNV-1a hash, the file download, `commentSafe`, `roundRangeValue` and `sortedLane` had two or three bodies. The `.h`, `.pfs` and `.mid` downloads now take the Graphic Export path, and a test pins the keys a To hardware paste and a shader are filed under.
+- **The Layers panel has its own stylesheet.** The 24 rules only `LayersPanel.tsx` read moved from the dock's `LabPanels.module.css` to `panels/LayersPanel.module.css`, and computed styles in 29 Lab states at 390px and 1440px match `dev`.
+- **Pattern Lab's header rules live with the dock.** The 22 rules only `PatternLabClient.tsx` read moved from the shared `PatternLab.module.css` to `LabPanels.module.css`, To hardware's dark button went to `HardwareModal.module.css`, and a test fails when a class in the shared file has one reader. Computed styles in 28 Lab states at 390px and 1440px match `dev`.
+- **Pattern Lab's preview shows add layers the way the device does.** The preview clamped each layer to 255 before blending the next, so two (200, 0, 0) add layers under a (128, 128, 128) multiply showed R 128 in the Lab and 201 in the exported pattern and on the panel.
+- **The Live Editor says why pasted code does not run.** The error, and the line it failed on when the stack gives one, shows under the editor. It used to reach the browser console only, once a frame for a `draw()` that throws.
+- **Community pages open when the browser blocks site data.** Reading the deck from `localStorage` threw a SecurityError that took every community page down to "This page couldn't load", and the atlas did the same reading its language. Both reads are guarded now, and a deck or language the browser refuses to store holds for the rest of the visit.
+- **The home page waits for the Pattern tab before fetching the code editor and the flasher.** The Pattern panel is mounted on `/`, `/build` and `/inside` too, folded away, so every visit asked jsdelivr for Monaco's loader and unpkg for esp-web-tools. Both now load the first time the tab opens, and stay.
+- **The guide's tests give the same answer on a busy machine and a fast one.** The hub hand test read Make's redraw off the wall clock and, on a fast machine, passed without the hand turning a knob; it now runs on its own clock. That turned up a simulator bug: nine clicks of K1 out and nine back left Origin's hue at 0.9999999999999993, read as "hue 1.00" with a full bar, and `wrap` now calls that the start. The NETWORK timeout test covers its 15 seconds in 150 frames instead of 900.
+- **Pattern Lab's "Copied" labels clear their timer when the panel closes.** The seven labels that show after a copy, paste or clear no longer leave a `setTimeout` behind, which could fail the Lab tests after every test had passed, and a second click keeps its label for the full 1.2 s. The image import modal's wheel listener now attaches in the same commit that enables Apply.
+- **posthog-js loads after the page does.** The analytics library was the largest script on every page (318 KB, 103 KB gzipped); it is now fetched with `import()` after the load event, and events captured before then are sent with their original timestamps.
+- **Closing Send or Publish from its backdrop keeps To hardware open.** In Pattern Lab's To hardware dialog, a press outside the nested Send or Publish modal closed To hardware as well, and the C++ header pasted into it was lost.
+- **Korean pages fetch 318 KB of Pretendard Hangul instead of 1.2 MB.** The Hangul part of each weight is now the 2,350 syllables of KS X 1001 and the jamo; the other 8,822 syllables are a second file that a page fetches only when its text has one of them.
+- **Editing a deck no longer runs out after four changes.** Sharing a deck and editing it counted against the same five requests a minute, so the fifth edit within a minute of sharing (each visibility change is one) came back 429. Edits now have their own twenty a minute, the same as a pattern's or a post's.
+- **The hero video has a stop button.** The YouTube embed on the home page played for 89 seconds by itself and took no clicks, so nothing could stop it (WCAG 2.2.2). The button in its corner takes the video away and leaves the photo under it. Pressed again, or after the video ends, it plays from the start.
+- **Pattern Lab's dialogs close on Esc, and its header menus close on a tap outside.** To hardware, Import image and Code guide take the keyboard focus when they open and give it back to their button when they close; an Esc over Send or Publish closes only that window, so the pasted header stays. Recent and Panels close on a press outside them, on Esc and on Tab past the last item, and the Panels list tells a screen reader which panels are open.
+- **The breadboard build guide fits the window from 1280px up, and loads its photos as you reach them.** At 1440px the page scrolled 146px sideways and the photos on the left were cut off. The step photos now sit in the space beside the text, up to 360px wide. On a phone the eight photos (1.9 MB) no longer download when the page opens, and the page ships 59 KB less JavaScript.
+- **The deck panel and the Send window stop waiting when the build server goes quiet.** With the build worker down, both asked the server every second until the window was closed, with a spinner beside "usually a second or two". They now give up after the wait the deck bar already used (6 s, plus 1.5 s a pattern, plus 5 s), say the server has not answered, and leave a button to start the build again. The bar, the panel and the window share that rule in one hook, `lib/community/useModuleBuild.ts`.
+- **The Live Editor fits a phone screen.** On a 390px phone the preset bar squeezed the pattern name to nothing and showed only "01"; the name now takes its own row above the buttons. The editor shrinks to the height left under the pinned 3D preview (330px on a 390×844 phone, 480px on desktop as before), so some of the page always stays in view to scroll by.
+- **`npm run build:enqueue` queues a module build.** Without `--pfm` it wrote a whole-image row, which the worker built as modules anyway, so `build:status` reported an image it never made.
+- **Pattern Lab's Pixel zoom follows the wheel's travel and holds the cell under the cursor.** Each wheel event was one zoom step, so a trackpad swipe of thirty small events ran from 6× to 32×, and the picture grew from the viewport's top-left corner. Steps now add up from the distance turned, 100 px a step, and the viewport scrolls the cursor's cell back under it. The image-import preview scales 8% per 100 px around the cursor the same way.
+- **The Live Editor's creation prompt gives the step one detent really moves each knob.** It said 0.05 a detent (0.10 for Knob 2). With 24 detents a turn and two turns to a range it is 0.021, 0.206, 0.102 and 0.021, worked out from the same ranges the preview's knobs turn through. The note under the Pattern Lab card no longer says the build server compiles firmware in the browser, and links PlatformIO instead of the Arduino IDE.
+- **Community dialogs close with a thumb, and the atlas works from the keyboard.** The × in the header of the community's 13 dialogs was a 10.7×23.2px target and is now 44×44px, with the header the same height. A point on the atlas shows a ring when Tab reaches it, and Space no longer scrolls the page; pin tiles can be reached with Tab and opened with Enter; the live ring stops pulsing under reduced motion.
+- **Pattern Lab's Pixel panel opens with the whole picture in view.** It always opened at 6×, so a 390 px phone showed 56 of 128 columns and 19 of 64 rows. It now opens once at the zoom the Fit button would pick, when 6× does not fit.
+- **The community rate limiter keeps an hour of builds through its cleanup.** Once it held more than 10,000 keys, the cleanup judged every key by the one-minute window of the call that set it off, so somebody who had used their 30 builds for the hour got a 31st. The limiter and the origin check every community route runs first now have tests.
+- **Pattern Lab writes an unnamed knob and a fine knob range into `@knobs` so they read back.** An empty name went out as `-=0..1` and came back as a knob called "-", and 0.0001..0.0004 was rounded to `0..0` and dropped. The `@knobs` and `@matrix` lines and the knob wrap rule now have tests.
+- **The home tabs' link rows draw a focus ring, and each tab's title is its page's h1.** A focused row under Start here or in the case card took only a cream fill, 1.23:1 against white, and now draws the LED ring the case tabs use; the join band on Inside does the same. With a tab open the hero's h1 is inert, so /build had no h1 at all.
+- **A desk run of `check:drift` reports a skipped `links-absolute` as skipped.** In a clone without the release tags the check could not read 24 links and said so, but exited 0, so the runner's summary listed it as passed. It now exits with the drift checks' skip code; under `PF_DRIFT_STRICT=1` it still fails.
+- **Links drawn as buttons and cards keep their look on hover.** The global `a:hover` underline outranked each CSS module's `text-decoration: none`, so 453 links on 16 pages grew an underline when pointed at, and the guide's "Stuck here? Tell us" drew a second line under its own.
+- **Two unimported files and six uncalled exports are gone.** `Deck.tsx`, `waveTest.ts`, `renderPatternStill`, `cloneShow`, `deleteSession`, `useActiveCodeLayer`, `timelinePatternNames` and `licensedHeaderText` had no callers in `web/` or `tools/virtual-panel/`, and `web/ARCHITECTURE.md` no longer lists `Deck.tsx` on the landing page.
+- **The 3D device and the Inside globe hold still for reduced motion.** With the system's reduced-motion setting on, the preview device stands facing the reader instead of swaying 26° each way every 42 seconds, and the globe no longer turns by itself or runs dots along its links. The LED pattern on the panel still plays.
+- **`web/ARCHITECTURE.md` says what Vitest runs and when a licence header is added.** `check:panels` runs every `src/**/*.test.ts(x)`, not only the Pattern Lab panels, and the community bakes the licence header into the stored code on publish and edit. `web/README.md` lists `check:voice`.
+- **The atlas says why it did not save a pin.** Every pin write ignored the server's answer, so after a 401 a dragged tile stayed at its new spot as if saved, and a dropped connection ended in an unhandled rejection. The panel now shows the server's reason, and a refused drop goes back to where the pin is saved.
+- **The guide's panel simulator fails a test when a firmware number it copies changes.** `firmwareConstants.test.ts` reads the 21 timings and limits from `firmware/patternflow/` (brightness idle, SELECT settle, the bus grace, the lane release and others) and the brightness percent rounding, and checks every `FIRMWARE` key against them.
+- **A pattern page stops counting the lines Pattern Lab writes on its own as the author's decisions.** The Lab stamps `@matrix 128x64` on every publish and the stock black-to-white `@ramp` on every value-field layer, so a Gemini variant published untouched showed "Framed" and "Colour shaped". Each line is now read with its parser and set against the Lab's defaults, and "Framed" names the frame.
+- **The atlas fits a phone.** At 390px the page scrolled 99px sideways, "Add my pattern" opened its picker 201px past the left edge where no tile could be tapped, and the placing banner ran off both sides of the map. The toolbar buttons now wrap as one block, the picker grid no longer widens with long titles (it scrolled sideways at 1440px too), and the banner sits under the filter chips instead of on them.
+- **The site's typecheck catches unused locals and parameters, switch fallthrough and a missing `override`.** It found twelve; one was a ramp table the pattern runtime rebuilt on every `setRamp` and never read. The eight `eslint-disable-next-line` comments without a reason now give one.
+- **Community fields a screen reader could not name now have names.** The device address under a deck or Send build was read out as an edit box with no name, and so were the atlas's made-from select and pin knob sliders; the atlas search and the report's detail box read out their placeholder instead. The atlas names them in Korean too.
+- **The home page's tabs open in a browser without WebGL.** Where WebGL 2 was turned off, `/`, Build, Pattern and Inside threw from the 3D view's renderer and showed the site's error screen. The view now asks for a context before it makes a canvas, and a scene that throws anyway is taken off, with one line standing in its place.
+- **The home page poster comes at the width it is drawn.** It was the 4032-wide camera original, 2.1 MB, on every tab of the home view. On a phone at 9 Mbps the first screen's largest paint took 9.6 s, and it now takes 0.5 s.
+- **The Build and Pattern tabs, and the home page on a phone, no longer download the build globe's photos.** The 3D view opened on the globe for a moment on /build and /pattern, and a phone's hidden hero globe warmed every pin's photos as well: 65 images, 8.3 MB on a 3× phone. The view now starts on the page's own tab, and the globe warms its photos only where it is on screen.
+- **Community write routes answer a `null` body with a 400.** `null` parses as JSON, so it got past every route's `catch` and threw on the first field read, and 22 handlers answered an empty 500 with no CORS headers. They now read the body through one helper that answers `{"error":"Expected a JSON object."}`.
+- **Pattern Lab and the Live Editor say when pasted code was cut off.** Code that stopped inside a call reported "Unexpected token 'return'", and code missing its last `}` reported "Unexpected token ')'", tokens the author never typed. The message now counts the brackets that never close and quotes the last line.
+- **Pattern Lab's Recent and Panels menus open inside a phone screen.** Below 800px each menu hung from its button's right edge and opened off the left of the screen. It now takes its left edge from the header's button row, at most 360px wide, with 44px rows. The × in the Lab's dialogs grows from 30px to 44px on the same screens.
+- **Send over Wi-Fi reaches the board when the address field is empty or holds a pasted `http://` address.** An emptied field made `http:///patterns`, which a browser reads as a host called `patterns`, and `http://192.168.0.44/` made a host called `http`. Every handoff and install link now cleans the address the same way, in `lib/community/deviceHost.ts`.
+- **Copy buttons in Pattern Lab and the Live Editor say "Copy failed" when the browser refuses the clipboard.** The Lab's Copy prompt and the To hardware copy buttons kept their label, and the Live Editor said the creation prompt was copied before the write had finished.
+- **Sharing from Pattern Lab measures the code the way the community does.** "Open in Pattern Lab" hands back the stored code with its licence header and footer on, and the publish modal counted them, so an unchanged fork or a reopened post just under the 100 KB cap was refused before it was sent, though the server takes it.
+- **The community API no longer lets an edit move a pattern to MIT or CC0.** Both are retired and stay on the patterns already published under them, but a direct PATCH still moved a CC BY-SA original to CC0 and rewrote the SPDX line in its source.
+- **Five community windows close on Esc and hand the focus back.** Sign-in, report, Edit details, Add firmware header and Publish a performance take the focus when they open and return it to the button that opened them; before, Esc left them open and four kept the focus on the page behind.
+- **The community atlas stands its filter chips above the map on a phone.** At 390px the two rows of chips hid 21 of the map's 83 labels, the map box left 152px blank under the drawing, and touch screens showed the "wheel to zoom · drag to pan" hint, which names gestures they do not have.
+- **Six pages stop prefetching the home page's 3D code.** The Patternflow link on /terms, /contact, /features, /roadmap, the 404 and /community prefetched / as soon as it was in view, and the home page's three.js and post-processing chunks came with it: /terms fetched 767,286 bytes of script at 390px, and 148,456 without them. The home page now loads when the link is clicked.
+- **Pages no longer preload fonts their text doesn't use.** The root layout drops DM Sans, which set no text, and loads Newsreader and Silkscreen without a preload, so only the journal, /terms and the guide fetch them; `/` and Pattern Lab take 166 KB of fonts less.
+- **A wrapping knob on a range other than 0..1 hands the pattern every detent.** Knobs 1 and 4 wrapped at ±0.5 whatever their range, so in the Lab, on community cards and on the guide's desk a pattern declaring `@knobs Hue=0..360` read a full turn as 20.8 clicks instead of 24; the wrap now sits at half the knob's own range.
+- **A mask over a 0% layer in Pattern Lab gates nothing in the exports either.** The exported JS and the C++ header dropped the 0% layer before grouping and handed its mask to the layer below, so a layer the Lab showed whole came out half black; the preview did the same for a code layer that had sat at 0% since it was added.
+- **The build map has a fourth kind of pin, a showing.** A unit set out somewhere for people to play is a triangle under In use, with a line back to the unit it is and no part in the web of builds. *Anbu*, a DJ night in Hongdae, the Hongik course exhibition and the Seoul Design Week party go in as showings, beside new pins for two gifts, SimonePDA's laser-cut case, Besoiobiy's printed case, the MQTT test between Ajman and Seoul, and jonwtr's norns screen mirror in Ghent. A pin's link into the site, like Build this case, changes the tab in place instead of opening a new one.
+- **The Build guide keeps up with a finger on a phone.** A scroll waits for the 3D stage's script only when the finger lands on a knob. The stage's models start downloading with three.js and weigh 266 KB less, and a stage that has stopped moving no longer redraws its shadow map.
+- **Pattern Lab's pixel undo survives a matrix resize.** Undo after shrinking the matrix threw a RangeError, and after growing it wrote the old rows into the new buffer. Each undo frame now keeps its size and goes back through the same centred crop or pad the resize gave the layer.
+- **A link to any page previews with that page's own title and picture.** The guide pages and the home page's tabs went out with no image, and pages without Open Graph tags of their own, such as `/features` and `/editions`, previewed as the home page. `twitter:title` was the home page's everywhere. `/build/breadboard` has its own title now, and it and `/update` are in the sitemap.
+- **Pattern Lab asks before Del removes a layer with work in it.** Deleting a layer has no undo and autosave wrote the smaller stack over the saved project within 3 s, so one misclick lost a pixel drawing. A blank pixel layer or a code layer still holding the starter pattern goes without asking.
+- **Pattern Lab stacks its panels on a phone.** At 800px wide or less the default dock puts Preview across the top and every other panel as a full-width tab under it, where three columns left each panel about 110px. The narrow layout is saved under its own key, so a desktop arrangement survives a phone visit.
+- **Pattern Lab's wheel zoom no longer scrolls the panel under it.** React attaches wheel listeners as passive, so the Pixel viewport and the image-import preview called preventDefault for nothing: each notch zoomed and scrolled at once. Both now listen natively with `passive: false`, and a sideways wheel scrolls instead of zooming out.
+- **The home page video gives way to reduced motion, and the section tabs say which one is open.** With reduced motion on, the hero shows its poster and never loads the YouTube embed, which autoplays with no control to stop it. The desktop tab column is now a `Sections` nav whose open tab is `aria-pressed`, as on the phone, with its ✕ hidden from screen readers, and the panel beside it is the page's ``.
+- **The links at the end of each guide say where they go.** All guides moves to the hub over the same 3D stage instead of reloading the page. A link to another page of the site carries → and opens in the same tab without a reload, and a link away from it carries ↗ and opens in a new tab.
+- **`npm run doc-map`** writes every link into the repository, from Markdown, site code, the console pages and the redirect table, to `docs/investigations/2026-10-doc-map.csv` with whether it resolves. 1007 links at the baseline, none broken.
+- **`check:features` fails when /features and the feature folders disagree.** An id with no folder under `firmware/patternflow/features/`, or a folder with no id, turns `check:ci` red, and the run prints both lists and what is on one side only. `core` is the one id without a folder, because the comment on `Feature.id` says so. It is the first check `npm run check:drift` runs: every file in `web/scripts/drift/` is one, and the rest of the cleanup's step 2 lands there.
+- **`check:links-absolute` fails on a GitHub link into the repository that does not land.** `web/scripts/doc-map.ts --check absolute` reads the doc map's 118 absolute links from 26 files, writes nothing, and fails on one whose path or anchor is missing at HEAD or at the tag it names, or on a repository URL in a file the map does not read. A tag the clone lacks is skipped and named, and `PF_DRIFT_STRICT=1` fails on that skip.
+- **`check:editions` fails when the shelf and the bundles disagree.** A card on /editions with no folder under `firmware/bundles/`, or a bundle that is neither a card nor a try-out on /features, turns `check:ci` red. So does a folder in `web/public/flash/bin/` that no card, try-out or manifest names, and an `EDITIONS` tuple in `release.py` or `check_versions.py` that is not the cards. The default card and a card released from another repository need no bundle, and both are read from the tree.
+- **`check:presets` fails when a web preset cannot reach a panel.** A `pattern-*.ts` in `web/src/lib/presets/` that `index.ts` does not import, or an import with no file, turns `check:ci` red. So does a preset with a firmware twin that `pattern_registry.h` does not compile in and the Basics pack does not carry, and a pack entry whose pattern number no web preset has. The web-only and firmware-only presets are read from `check_presets.py`, not copied.
+- **`check:human-lock` fails a pull request that changes a word of the maintainer's own writing.** The journal, the manifesto, the release notes, the README's three story sections, Part 1 of each RFC and the roadmap's `detail` and `detailKo` strings are compared with the pull request's base as plain text, with the Markdown, MDX and whitespace taken out. Markup, link paths and image paths can change; a typo fix cannot. A locked file moved word for word passes, and the run says the move needs the maintainer's approval. `docs/.human-written` takes more paths, one per line.
+- **`check:changelog` fails when `CHANGELOG.md` and `docs/releases/` disagree.** A release the root keeps in full equals its file once its relative links point two folders up. A line under "Older releases" matches its file's heading, and every `docs/releases/vX.Y.Z.md` is in the root one way or the other. A drifted section prints the line and column where the two copies part, and a release cut by `release.py` stays green.
+- **Every pull request runs the drift checks and the text lock, whatever it touches.** `.github/workflows/drift.yml` (Drift checks) runs `check:drift` on every pull request and on push to `main`; Web CI starts only on `web/` and `hardware/case/`, so a pull request that edits only the BOM or a bundle never ran it. It fetches the tags and installs `pdftotext` and runs with `PF_DRIFT_STRICT=1`, so a check that cannot read a side fails instead of skipping. On a pull request it also runs `check:human-lock` against the base commit.
+- **`check:roadmap` fails when a /roadmap node and its issues disagree.** `gh` reads the state of every issue the page names on engmung/Patternflow. A `done` node with an open issue, a `planned` node with a closed one, or an issue GitHub answers 404 for turns `check:drift` red. An issue left open on purpose goes in the check's own list, with the maintainer's reason. Without `gh`, a login or access to the repository it skips, and the Drift checks workflow runs it strict.
+- **`check:voice` points at the habits of generated prose, line by line.** Run it by hand on new or rewritten text before a pull request opens. With no arguments it reads the lines the branch adds; it also takes files, and `-` reads a PR description from stdin. It flags a word from `docs/voice-banned.txt`, three adjectives or clauses after a serial comma, an opening that says what the text is about and a closing summary, each as `file:line`. CI does not run it.
+- **Five of the site's twelve lint warnings are gone.** The unused `normalizePerformance` and `useMediaQuery` imports go, and so do `REASON_LABEL` on the edition card and `zoomTo` on the roadmap map, which nothing read. The map's `clampPan` is now a `useCallback` on `width` and `zoom`, so the wheel effect lists it and still runs on the same renders. Nothing the site serves changes.
+- **The seven `` lint warnings carry a named exception.** The breadboard build photos, the hero poster and the journal lightbox stay plain ``, each with an `eslint-disable-next-line @next/next/no-img-element` line that says why; `npm run lint` drops from twelve warnings to five.
+- **Pattern Lab's July storage keys say who wrote them and when.** The draft and ramp keys the lab wrote in July 2026, the gallery key it still writes, and the v1 show bake each name their commit and date. The comments no longer say the migration reads the draft once: it runs on every open where no project restores. `legacyGallery` becomes `gallery` with the same key string, and `saveDraft`, which nothing called, is gone. Nothing the site serves changes.
+- **The site's redirects and old routes say what links them.** The two journal `?lang=en` redirects, the `/en/en` collapse in the proxy, the eight campaign short links, `/business` and `/community` on the Vercel site each carry a comment with the commit and date that made them and what still points there. The `/variants` comment now dates the console links to 3.7.0 through 3.8.0. Nothing the site serves changes.
+- **Web CI fails on a lint warning.** The Lint step runs `npm run lint -- --max-warnings=0`, so the site stays at the zero warnings it reached in step 3 of the cleanup.
+- **The community's old formats say what wrote them and when.** The deck's `pf-module-cart` and `pf-saved` keys, the feed's `page`, `size` and `view` parameters, the `"bin"` build format the builds route answers with 410, and the zip page's `duplicate-name` each carry a comment with the commits and dates. `moduleRunner.ts` no longer cites the removed `buildRunner.ts`. Nothing the site serves changes.
+- **A new TODO or FIXME turns the drift checks red.** `web/scripts/drift/markers.ts` reads every tracked text file under `firmware/`, `web/src/`, `web/scripts/`, `tools/` and `integrations/`, and prints the file and line of either word in capitals. Vendored code is skipped, so the one `TODO` in upstream's HUB75 driver stays where its `UPSTREAM.md` records it.
+- **One file holds the values the firmware and the site both carry.** `shared/constants.json` has the three panel geometries, the knob count, 24 detents a turn, the encoder and button timings and the bus scale. `npm run gen:shared` in `web/` writes it out as `firmware/patternflow/src/core_shared.h` and `web/src/lib/shared/constants.ts`, and `check:shared`, part of `check:ci` and the drift workflow, fails when either file is stale or when the frozen `abi/pf_params.h` or `build_module.py` disagrees with it. Nothing reads the two files yet.
+- **The site's lint and test tooling is on its latest releases inside the current majors.** ESLint 9.39.5, `@testing-library/dom` 10.4.2, tsx 4.23.15 and `@types/node` 20.19.43, in `web/package-lock.json` only. ESLint 10, TypeScript 7, vitest 5, jsdom 29, jest-dom 7 and `@types/node` 26 wait until the cleanup is done.
+- **The site reads the panel size and the knob numbers from `shared/constants.json`.** `harness.ts`, `controls.ts`, `matrix.ts`, `panelScreens.ts` and `deviceSim.ts` in `web/src/lib/` take the 128×64 canvas, the four knobs, 24 detents a turn, the bus maximum of 1000, the long press, the debounce and the edges in a detent from `web/src/lib/shared/constants.ts`, and keep every name they export. The guide's panel screens cite firmware constants by name instead of by line number, which had drifted. Nothing a page shows changes, and the rebuilt sandbox differs only where a literal became its constant's name.
+- **Drizzle is on its latest patch releases.** drizzle-orm 0.45.4 and drizzle-kit 0.31.11, in `web/package-lock.json` only.
+- **Tailwind is on its latest 4.x release.** `tailwindcss` and `@tailwindcss/postcss` 4.3.3, in `web/package-lock.json` only.
+- **React Three is on its latest releases inside the current majors.** `@react-three/fiber` 9.8.2, `@react-three/drei` 10.7.9 and `@react-three/postprocessing` 3.2.0, in `web/package-lock.json` only.
+- **zustand is on its latest release inside the current major.** 5.0.15, in `web/package-lock.json` only.
+- **mediabunny is on its latest release inside the current major.** 1.61.4, in `web/package-lock.json` only. A Pattern Lab clip keeps the same video; its header now names `Mediabunny v1.61.4`, and an MP4 also carries a `btrt` bitrate box.
+- **@google/genai is on its latest release inside the current major.** 2.30.0, in `web/package-lock.json` only. Pattern Lab's Generate sends Gemini the same request body; the SDK version in its `x-goog-api-client` header now reads 2.30.0.
+- **posthog-js is on its latest release inside the current major.** 1.438.10, in `web/package-lock.json` only. Pageviews and clicks reach PostHog with the same names and site properties, and session replay still shows the Gemini endpoint as `(redacted)`. The PostHog chunk every page loads grows from 61 KB to 104 KB gzipped.
+- **better-auth is on its latest release inside the current major.** 1.7.9, in `web/package-lock.json` only. The community's auth requests get the same answers as before, and a session made before the update stays signed in. One answer changes: a sign-in with a password over 128 characters now says "Password too long" instead of "Invalid username or password".
+- **React is on its latest release inside the current major.** `react` and `react-dom` 19.3.1 in `web/package.json`, `@types/react` and `@types/react-dom` 19.3.0 in `web/package-lock.json`. The pages still run the React that Next 16.3.0 carries inside it, so the browser gets the same code. The new React runs the component tests.
+- **Next is on its latest release inside the current major.** `next` and `eslint-config-next` 16.4.1 in `web/package.json`, `@next/mdx` 16.4.1 in `web/package-lock.json`. `npm audit` goes from 27 to 25: the nine advisories against Next 16.3.0 and the two against sharp 0.35.3 are gone. Pages keep their text and CSS rules. CSS module class names are shorter in production (`hryNmG_atlas`, was `Atlas-module__hryNmG__atlas`).
+- **The guide's simulated board takes input that no hand made.** `DeviceSim` reads `knobs`, `params`, `paramActive`, `lanes` and `laneActive` as `/api/status` reports them, takes clicks and MIDI-note presses from a socket, lets go of one bus channel or one lane, drives its lanes from the audio WebSocket, and tells what the pattern was given and threw. The guide shows the same frames. The virtual panel of cleanup step 4 is built on it.
+- **`check:virtual-panel` runs every preset on the virtual panel.** Each header in `firmware/patternflow/presets/` but the firmware-only `calib` has to be on the panel's pattern list. Each is selected over REST, runs two seconds of panel time and has to give a 128×64 `/frame.png` with no thrown error. `check:ci` and the drift workflow run it.
- **/build lets you pick the case, and the 3D view shows the one you pick.** The Build panel's switch (the official v3.9 case, Besoiobiy's printed case, SimonePDA's laser-cut one; `?case=` links to one) brings up that case's card, a remix's opening with its README on GitHub, and swaps the device in the preview for that case, with the same v3.9 board, DevKit and LED panel inside (and, on Besoiobiy's case, its own knob caps). SimonePDA's case comes in its two finishes, acrylic and MDF. Each model is made by `tools/case-models` from the case's own files in this repository (the guide's v3.9 case, Besoiobiy's STLs, SimonePDA's cutting layout) and sits beside the case's photos, `web/public/cases//model.glb`. The model the page loads first is 0.32 MB, where the old v3.0 one (`3dforweb.glb`, now removed) was 1.15 MB; a remix's model loads when it is picked, and the guide takes its LED panel from the official one.
+- **/build has a fourth case, mbchars' horizontal desktop one, in the switch and in the 3D view.** Its card is its README's facts and the author's photos, and the switch is four tabs, two by two on a phone. `tools/case-models/mbchars.py` puts its model together from the remix's STLs, since no assembled file exists, and checks it against the README; in it the panel lies the long way (`ledTurn` in `placement.json`), the official knob is printed white (`knobLook`), and the orange is the one in the author's 3MF, `#F46B16` (`pla_orange`). The model is 0.38 MB and loads when the case is picked; the other three rebuild byte for byte. The case is 41 cm across, so on a desktop's tall view it is drawn smaller, by as little as keeps it clear of the arrows; on a phone all four cases, and on a desktop the three upright ones, are still drawn at the one scale. The build map's pin for the same build goes by the maker's name now, mbchars (it was extraplojo).
+- **The 3D view on /build has arrows and a name.** On the Build tab, arrows either side of the device step through the cases, round and back to the first, as the build map's arrows step through builds; the switch, the card and `?case=` follow. Over the device is whose case it is, the name on its tab (Official, Besoiobiy, SimonePDA, mbchars), in the build map's label.
- **The preview's case is white, not beige, and the rest of it looks more like the photos.** The printed parts are a neutral white PLA (`#f4f4f2`, matte), where the guide's warm white had come along; the case front measures b\* +0.4 and L\* 89.8 where it was +1.7 and 82.5. The frame goes through the shoulder of the Neutral tone curve, which leaves what is under its knee as lit and rolls off what is brighter with its channels together, so a bright light no longer clips red and green first and leaves the white yellow. The light is made on the page from light panels, as on the guide's stage, instead of an environment map fetched from a CDN at run time. The LED panel is a grid of round LEDs on a black mask where it was solid blocks on grey (each LED becomes its whole cell once it is under about three pixels across, as on a phone), its sides and back are the panel's black frame, and what glowed before (the whites, and the bright yellows and cyans of a pattern from the editor) glows as before. The official knob is the printed knob's own profile drawn round, where it was a 32-sided prism. SimonePDA's clear acrylic is clear: the canvas is see-through, and every partly covered pixel used to reach the page far too bright, which drew the sheet frosted white; its three feet, which no photo shows on it, now come out only with the parts and on Assemble. Every case is drawn at one scale, so the LED panel is the same size in all three (the remixes drew it 3.5 to 4% smaller). The three models were rebuilt with `tools/case-models/build.sh`; the official one grows by 0.8 KB.
+- **The guide's first flash starts in download mode, and its Wi-Fi assumes the step won't come.** The Play guide's 01 Flash (`/guide/play`, and `/ko`), where the Build guide sends a new DevKit, now has hold BOOT, tap RST, let go as its fourth card, before Flash Patternflow, every first flash, instead of a fallback for a missing port. The Wi-Fi card says plainly that after an install from download mode the dialog usually ends without asking, and gives the moves in order: close it, press RST once, Flash Patternflow again, the port again, Connect to Wi-Fi; its flasher screens lead with that path ("Usually: after RST"), the install-right-away case second, and it carries its own Flash Patternflow button. A new last card, after Power, is the panel's own network for when that didn't take: hold K2 for HOTSPOT and `patternflow-xxxx`, join it with the password `patternflow`, open `http://192.168.4.1/` and pick your network on the Wi-Fi page, with what to do when it isn't in the phone's list yet (it comes up about 15 seconds after power-on). The stage follows the cards, and the device simulator draws the NETWORK screen's hotspot (`setHotspot`). The Build guide's flash card, the Pattern tab's notes under Flash Patternflow and the breadboard page say the same order. Anchors `#flash-1` to `#flash-3` are where they were; `#flash-4` is now download mode.
+- **`/flash` goes to the guide's Flash chapter.** It had no page, though the device's Update page ("flash it over USB at patternflow.work/flash") and every release's FLASHING.md send people there; it now redirects to `/guide/play#flash`. `/flash/manifest.json` and `/flash/bin/*` are untouched.
### Docs
+- **The build service docs describe the worker that runs today.** `docs/SERVICES.md` and `web/.env.example` still said the worker compiles firmware with arduino-cli and offered `ARDUINO_CLI_PATH` and `BUILD_FQBN`, which nothing has read since whole-image builds ended on 12 August. `.env.example` now lists `PF_XTENSA_BIN`, `PF_TARGET_ABI` and `GITHUB_TOKEN`, and `src/lib/envExample.test.ts` fails when it, the code and the table in `web/ARCHITECTURE.md` disagree.
+- **The docs index, the repository map and the contributor guide describe the tree as it is.** `docs/README.md` lists the cleanup RFC and its log, the rename list in `docs/REPOSITORY.md` names all 30 files the site's absolute links reach (it had ten and the BOM), `CONTRIBUTING.md` names the four build kinds and `originId`, and `docs/EDITIONS.md` says `build.sh all` builds five compositions.
+- **The cleanup's progress log records steps 1 to 5 and the gate.** `docs/rfc-cleanup-progress.md` has an entry for each step and checks the six items of §2.12 at `9dff573`, and all six hold. `docs/rfc-cleanup.md` is marked done.
+- **The cleanup has a baseline.** `docs/investigations/2026-10-cleanup-baseline.md` records the commit the cleanup starts from and what the five commands in `docs/rfc-cleanup.md` §2.1 said there. All five pass. `AGENTS.md` gains hard rule 8: during a cleanup window no PR changes behaviour, and moves and renames carry a redirect or an updated link map.
+- **Cleanup step 1 is planned.** [`docs/investigations/2026-10-cleanup-step1-plan.md`](docs/investigations/2026-10-cleanup-step1-plan.md) cuts the step into three lanes that share no file (root moves, the changelog split, the ko/en parity list), lists what the RFC did not know about the tree (`docs/releases/` already exists; the ko/en pairs it names do not), and asks the maintainer five more questions, each with a default.
+- **The v2 build guide moved to `docs/legacy/BUILD_GUIDE_v2.md`.** Every link to it in the repository and the site's v2 board link point at the new path; links from outside to the old root path break, since GitHub serves no redirect for a moved file.
+- **The doc map lists the Korean/English pairs.** `npm run doc-map` in `web/` also writes `docs/investigations/2026-10-doc-parity.csv`: every text the repository keeps in both languages (the journal's posts, the guide's pages and copy, the roadmap, the atlas), one row per pair, with each side's length and whether one is missing or short. Korean is measured at 2.4 English characters a syllable, as the journal runs, so a faithful translation reads `ok`; today it flags 26 rows, among them the device's Format storage dialog, which the Korean guide quotes in English. A later `x.ko.md`, a console page's twin or a `Ko` field in `features-data.ts` lands in the same list.
+- **The doc map reads the YAML under `.github/`.** `npm run doc-map` in `web/` now follows the links in the issue forms and their `config.yml`, so a PR that moves `SUPPORT.md` or `CONTRIBUTING.md` sees the contact link it would break. Six rows today, all resolving.
+- **`CHANGELOG.md` is split by release into `docs/releases/`.** Every release has its own file, `vX.Y.Z.md`, back to v1.0.0, with its section copied as written and its relative links two folders up; the root keeps the three newest in full and a line linking each older one. `release.py cut` writes the new release's file and moves the fourth-newest section out of the root. The long-form notes for 3.0.0 and 3.1.0 are now `v3.0.0-notes.md` and `v3.1.0-notes.md`.
+- **The community files moved to `.github/` and the audio guide to `docs/`.** `CODE_OF_CONDUCT.md`, `SECURITY.md` and `SUPPORT.md` are now in `.github/`, where GitHub still finds them for the community profile and the security tab, and `AUDIO_GUIDE.md` is `docs/AUDIO_GUIDE.md`. Every link to them in the repository, the issue chooser and the site points at the new paths. Links from outside to the old root paths break, since GitHub serves no redirect for a moved file.
+- **Cleanup step 2 is planned.** [`docs/investigations/2026-10-cleanup-step2-plan.md`](docs/investigations/2026-10-cleanup-step2-plan.md) cuts the seven drift checks, the changelog check, the human-text lock and the voice lint into fourteen PRs that share no file, and records what the tree already holds for each: the schematic's text layer carries every designator, and `check_presets.py` already covers half of `check:presets`.
+- **Cleanup step 3 is planned.** [`docs/investigations/2026-10-cleanup-step3-plan.md`](docs/investigations/2026-10-cleanup-step3-plan.md) cuts §2.5 into twenty-seven PRs, no two of which edit a file while both can be open, and records what scouting found: the MIDI page still counts 20 detents a turn where the encoder has 24, the HUB75 fork carries more local changes than `VENDORED.md` lists, and first-party code has no `TODO` or `FIXME` left.
+- **The project's writing voice is written down.** `docs/voice.md` describes, on one page, the voice the README and the build guide already use for English, and a standard for Korean taken from published Korean editing guides: the marks of Korean carried over from English, with two lines from the web guide shown beside a version written in Korean first. `docs/voice-banned.txt` starts the list of words that stay out of the docs, for the voice check the cleanup adds later.
+- **Cleanup step 4 is planned.** [`docs/investigations/2026-10-cleanup-step4-plan.md`](docs/investigations/2026-10-cleanup-step4-plan.md) cuts §2.6 into twenty-four PRs, no two of which edit a file while both can be open, and records what scouting found: the virtual panel can be the guide's `DeviceSim` behind sockets, `/api/status` shows no knob delta that arrives over OSC, MIDI, MQTT or the audio socket, and `rest-api.md` contradicts itself in eight places a vector has to choose between.
+- **The docs index and the repository map catch up with step 1 of the cleanup.** `docs/README.md` and `docs/REPOSITORY.md` say the root keeps three guides and no others, describe `docs/releases/` as one file per release, and list `voice.md`. The doc map is regenerated, and all 1066 of its links resolve.
+- **Cleanup step 5 is planned.** [`docs/investigations/2026-10-cleanup-step5-plan.md`](docs/investigations/2026-10-cleanup-step5-plan.md) cuts §2.7 into four PRs: a record of every place the panel size is set, the any-resolution draft, the Linux player draft, and a close that waits until steps 3 and 4 free the files it shares with them. Scouting found that the 36 references §2.7 counts are a floor (the presets alone carry 142), that the site's pattern engine already takes any size while the device and its simulator do not, and that the S-PWM panels the firmware cannot drive are the ones a Colorlight receiving card, one of the player's outputs, is built for.
+- **The contributor docs say what CI runs on every pull request.** `CONTRIBUTING.md`, `web/README.md`, `web/ARCHITECTURE.md` and `docs/REPOSITORY.md` now name `drift.yml`, which runs the drift checks and `check:human-lock` on every pull request whatever it touches. `docs/REPOSITORY.md` no longer says the absolute `blob/main` links go unchecked: `check:links-absolute` fails a pull request that breaks one.
+- **`AGENTS.md` carries the writing-voice rule and the step 2 check commands.** The writing-voice hard rule from `docs/rfc-cleanup.md` §2.10 is appended as the RFC writes it, and Common commands gains one line each for `check:drift`, `check:human-lock` and `check:voice`, saying where each runs.
+- **The doc map is regenerated at the close of step 2 of the cleanup.** All 1072 of its links resolve. The six new rows are links the step 2 pull requests and the step 5 plan added.
+- **Where the panel size lives, line by line.** `docs/investigations/2026-10-panel-geometry.md` counts every place the firmware and the site set or assume 128×64 at `e3d3e83`, from the 36 `PANEL_RES_W/H` uses §2.7 names to the presets, the module toolchain, the driver's DMA buffers and the site's simulator, with the command behind each count. It proposes nothing. The any-resolution and Linux player drafts build on it.
+- **`docs/midi-spec.md` says 24 detents a turn.** `outDiv=1` is 24 steps a turn and `4` is 6, where it said 20 and 5.
+- **The any-resolution RFC has a draft.** `docs/rfc-any-resolution.md` is Part 2 only. It says where each of the 36 uses of `PANEL_RES_W` and `PANEL_RES_H` goes, proposes a geometry read from NVS at boot and set on a core console page, keeps one module build per size with the size in the cache key, states the limits (classic HUB75 only, S-PWM unsupported, W × H up to 8,192 at depth 8, the stock's own memory) and orders the work as 19 PRs. Part 1 is the maintainer's and is empty.
+- **The Linux player has a draft RFC.** `docs/rfc-linux-player.md` is Part 2 of the player path in the cleanup RFC's §2.7: the site's engine in a Linux process, with outputs for a Pi's HUB75 HAT through rpi-rgb-led-matrix, a Colorlight receiving card and HDMI, input from USB encoders, MIDI, OSC and the web knobs, the contracts it speaks and in what order, what it cannot do, the work as fourteen PRs, and nine questions for the maintainer. Part 1 is left for him. Nothing the firmware or the site does changes.
+- **`AGENTS.md` names `shared/`, the HUB75 driver's `UPSTREAM.md` and `patternflow.patch`, and the commands step 3 added.** The repository map has `shared/constants.json` and the two files generated from it, and says the patch is rebuilt with the command in `UPSTREAM.md`, never edited by hand. Common commands gain `gen:shared` with `check:shared`, the lint gate (`--max-warnings=0`, an exception with its reason after `--`), and the `markers` drift check.
+- **The two step 5 drafts are listed in `docs/README.md`.** `rfc-any-resolution.md` and `rfc-linux-player.md` link each other, and the doc map carries their rows.
+- **The repository maps catch up with step 3.** `docs/REPOSITORY.md` has a top-level row for `shared/`. `web/ARCHITECTURE.md` says the pattern harness takes its constants from `shared/constants.json`, and calls `patternflow_lab_gallery_v1` the live gallery key instead of a legacy one. `firmware/README.md` sends readers of its vendored HUB75 note to `UPSTREAM.md` and `patternflow.patch` instead of naming two additions.
+- **The doc map is regenerated at the close of step 3 of the cleanup.** All 1095 of its links resolve. The three new rows are links step 3 added to `docs/REPOSITORY.md` and `firmware/README.md`.
+- **The REST contract has its first vectors.** `docs/rest-api-vectors/` holds nine, for `GET /api/status`, `POST /api/params` and `POST /api/sleep`, each a sentence of `docs/rest-api.md` and the requests that show a panel doing it. `npm run check:rest-vectors` in `web/` replays them against a fresh virtual panel each, and the drift workflow runs it on every pull request.
+- **The MQTT contract has its first vectors.** `docs/mqtt-vectors/` holds nine, for the topic table and the two roles in `docs/mqtt-spec.md`: the sleep words and `sleep/state`, `param/N` and its empty payload, `knob/N` as a difference, `pattern` by name or slug, and what a Publisher ignores and sends. `npm run check:mqtt-vectors` in `web/` replays them against a fresh virtual panel each, through a test broker of its own (`web/scripts/drift/_broker.ts`), and the drift workflow runs it on every pull request.
+- **The REST vectors reach the rest of the core.** Twenty-five more in `docs/rest-api-vectors/` read `/api/display`, `/api/knobs`, `DELETE /api/crash`, the favicon, the pattern routes and the headers every route shares. On the default build `caps` is `["patterns","params","sleep"]` and each feature's route answers the plain-text 404.
+- **The MQTT vectors reach the channels and the snapshot, and `/api/mqtt` has REST vectors.** Six more in `docs/mqtt-vectors/` replay the prefix naming the channel, channels 1–4 forcing Subscriber, the retained snapshot on channels 1–4 and Live, the Publisher's eight-second heartbeat, a snapshot overwriting a `param/N` write, and Broadcast taking no snapshot. Seven in `docs/rest-api-vectors/` read `GET` and `POST /api/mqtt`: its fields, the password it never returns, `forcesSub`, the knob counts in every role, `mqttRole` in status and the `mqtt` cap.
+- **The maps of the tree name the virtual panel and the contract vectors.** `docs/README.md` puts each contract's vector folder on its line, as it already did for pfst, and adds `docs/contract-vectors.md`. `tools/virtual-panel/README.md` lists its eight transports with the feature each needs and the port it answers on. `CONTRIBUTING.md` says a spec change brings its vectors along, and `web/README.md` and `web/ARCHITECTURE.md` name the five vector suites and `check:virtual-panel`.
+- **The REST and MIDI specs say one thing where they said two.** HTTP writes knobs and reads them from `/api/status`, select answers with the pattern it queued, `d1`..`d4` alone are a whole `POST /api/params`, `caps` names what each edition adds, and only a path no route serves gets a plain-text `404`. In midi-spec.md, CC 24–27 are binary offset and a host's Program Change comes back as the pattern change it made.
+- **`AGENTS.md` names the virtual panel, the contract vectors and their commands.** The repository map adds `mqtt-spec.md`, the vector folder beside each of the five wire contracts with `contract-vectors.md` as their format, and `tools/virtual-panel/`. Common commands gain the five `check:*-vectors` suites, `check:virtual-panel` and `npm run virtual-panel`.
+- **The doc map is regenerated at the close of step 4 of the cleanup.** All 1138 of its links resolve. The 43 new rows are links step 4 added: the five vector folders' READMEs, `docs/contract-vectors.md`, `tools/virtual-panel/README.md`, and the maps of the tree that point at them.
- **The build pages say what is true now.** Laser cut is no longer "being prepared" on /build, in the README, the v2 guide or `llms.txt`: SimonePDA's case is it. The printing time is about 10 hours for the official case on a 256 mm bed (/build said 10–12, the breadboard guide 11), the v2 guide and the assembly map name the v3.9 board, and the breadboard guide flashes through the DevKit's native USB port from the Pattern tab and links the sections that exist.
+- **The README opens with what Patternflow is and three ways in.** The tagline and the first line are the manifesto's fixed lines (L1, L2, L3), with *Try one in your browser*, *Build one* and *Get one assembled* above the badges, and the guide's four parts follow as a list, each with its written guide beside it. Paik is told once, in *Where it began*, closed by L5 in full. Patterns come before the device, and **First light** follows the build: J4-only power, download mode first, the Wi-Fi step that usually doesn't come and what to do, unplugging before the DevKit is seated, the panel's own network, the Basics pack with *Format storage*. Lines that had gone stale were brought up to date: the Crowd Supply campaign reads as it stands, past its goal and open until 15 October, sound and MQTT shipped in editions, the hand-drawn map that still showed the Workshop is out, and the timeline runs to October. The long reference material stays, folded.
+- **The READMEs and docs were tidied.** Where a doc gives the first flash (`firmware/README.md`, `PATTERN_GUIDE.md`, `SUPPORT.md`, `docs/assembly/`) it now says download mode first, that the Wi-Fi step usually doesn't appear the first time and what to do, and the hotspot. The indexes (`docs/README.md`, `docs/REPOSITORY.md`, the hardware and remix READMEs, `web/README.md`, `web/ARCHITECTURE.md`) list the guide pages, the three case remixes, `legacy_lasercut/` and the bundles that exist, and stale wording went: the NETWORK screen's old rows, the custom slots, "variant", the dead `/flash` page. `BUILD_GUIDE_v2.md` is as it was.
+- **The build guide flashes in the Play guide's order.** `BUILD_GUIDE.md` §8.1 is the Play guide's Flash chapter in writing, step for step: download mode (hold BOOT, tap RST, let go) before Flash Patternflow on every first flash, where it had been the fallback for an empty port list; the Wi-Fi step expected not to come the first time, with RST, Flash Patternflow again and *Connect to Wi-Fi* as the moves; the DevKit seated with the power bank unplugged, then powered from `J4`; and the panel's own network, `patternflow-xxxx`, as the next step when that didn't take, before the Basics pack. Section 7 no longer seats the DevKit before it has firmware, so it is *Wiring* now (anchor `#7-wiring`), and the README's First light points at §8 again as the same steps in writing. Besoiobiy's case README cites the renumbered steps.
+
+### Tools
+
+- **A virtual panel for the contract vectors.** `tools/virtual-panel/` runs the guide's `DeviceSim` behind an HTTP port: `GET /frame.png` is the 128×64 frame, `/vp/` is a hand and the clock, and each transport is a file in `transports/`. `npm run virtual-panel` in `web/` starts it.
+- **The virtual panel answers device state over REST.** `tools/virtual-panel/transports/rest-state.ts` serves `GET /api/status` with every field `docs/rest-api.md` documents, `POST /api/params`, `POST /api/sleep`, `GET /api/display`, `/api/knobs`, `DELETE /api/crash` and the favicon, on the guide's `DeviceSim`.
+- **The virtual panel answers the pattern routes.** `GET /api/patterns`, `/api/patterns/select` and `/api/patterns/file` on `tools/virtual-panel/`: the Basics pack's `.pfm` and sidecar out of `basics.zip`, and the `PFT1` picture a pattern leaves once it has run two seconds.
+- **Contract vectors have a format and a replay.** `docs/contract-vectors.md` says how a vector is written: the spec sentence it comes from, the features built in, and steps that send over HTTP, a hand, the clock or one of the four other transports and expect a reply, `/api/status`, what the pattern was given or what the panel sent and how soon. `web/scripts/drift/_vectors.ts` replays a folder of them, each against a fresh virtual panel, for the five suites of cleanup step 4.
+- **The virtual panel speaks MQTT.** `tools/virtual-panel/transports/mqtt.ts` is an MQTT 3.1.1 client on Node's `net`: the Publisher and Subscriber roles, the Broadcast, channel 1 to 4, Live and Custom prefixes, every public topic in `docs/mqtt-spec.md`, and the retained snapshot every eight seconds. Like a device that was never set up, it dials nothing until a role and a broker are set.
+- **The virtual panel answers `/api/mqtt`.** On a panel started with `--features mqtt`, `tools/virtual-panel/transports/mqtt-http.ts` reports the MQTT client's state and sets its broker and role in one `POST /api/mqtt`, as `docs/mqtt-spec.md` describes. `/api/status` gains the MQTT fields the firmware adds there.
+- **The virtual panel speaks OSC.** `tools/virtual-panel/transports/osc.ts` listens on 9001 and sends to port 9000 of the host that last sent it a message it knows: the announce, the heartbeat, a hand's knobs and buttons, and pattern and mode changes, as `docs/osc-spec.md` lists them. `--features osc` composes it in, and `--port oscReply=0` sends each reply back to the port it came from.
+- **OSC has contract vectors.** `docs/osc-vectors/` holds eleven, each one sentence of `docs/osc-spec.md` and the messages that show a panel doing it, from the ping and its announce to a `#bundle` the panel ignores. `npm run check:osc-vectors` in `web/` replays them against the virtual panel built with the `osc` feature, and CI runs it with the other drift checks.
+- **The virtual panel takes the audio WebSocket.** `tools/virtual-panel/transports/audio-ws.ts` listens on 81 for the five messages `docs/audio-ws-spec.md` lists and holds each lane 500 ms past the last message that set it. `GET` and `POST /api/audio` read and flip the AUD switch. `--features audio` composes it in, and `--port ws=…` moves the socket.
+- **The virtual panel speaks RTP-MIDI.** `tools/virtual-panel/transports/midi.ts` takes a session on 5004 and 5005 under the name Patternflow, two participants at most, and moves knobs, buttons and the pattern by the host → device table in `docs/midi-spec.md`: CC 20 to 27, notes 60 to 63 and Program Change on channel 1. `--features midi` composes it in, and `--port midi=N` moves the pair to N and N+1.
+- **The audio WebSocket has contract vectors.** `docs/audio-ws-vectors/` holds twelve, each one sentence of `docs/audio-ws-spec.md` and the messages that show a panel doing it, from `a=` with `-` slots to a lane a hand holds for five seconds. Three more in `docs/rest-api-vectors/` read `GET` and `POST /api/audio`. `npm run check:audio-vectors` in `web/` replays them against the virtual panel built with the `audio` feature, through a WebSocket client that runs on Node 20, and CI runs it with the other drift checks.
+- **The virtual panel sends MIDI back.** `tools/virtual-panel/transports/midi-out.ts` reports a hand's turns on CC 24 to 27 (a position by default, 64 ± steps with `outMode=rel`, at most one message a knob every 50 ms), a hand's buttons on notes 60 to 63 and each pattern change as Program Change, by the device → host table in `docs/midi-spec.md`. It answers `GET` and `POST /api/midi` and adds `midi` to `/api/status`.
+- **MIDI has contract vectors.** `docs/midi-vectors/` holds nine, each one sentence of the host → device table in `docs/midi-spec.md` and the messages that show a panel doing it, from CC 20 at 127 landing on 1000 to a message on channel 2 the panel ignores. `npm run check:midi-vectors` in `web/` replays them against the virtual panel built with the `midi` feature, over an RTP-MIDI session `web/scripts/drift/_rtpmidi.ts` opens for each, and CI runs it with the other drift checks.
+- **MIDI out and `/api/midi` have contract vectors.** Eleven files in `docs/midi-vectors/` start with `out-`, each one sentence of the device → host table or the settings in `docs/midi-spec.md`, from three detents on knob 1 arriving as CC 24 at 67 to a host's own CC that never comes back. `check:midi-vectors` replays them over its RTP-MIDI session. Nine more in `docs/rest-api-vectors/` start with `midi-` and read `GET` and `POST /api/midi` and the `midi` block in `/api/status` under `check:rest-vectors`.
## [3.11.0] - 2026-10-04
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c7d695cde..8e675ccba 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,24 +1,27 @@
# Contributing to Patternflow
-Patternflow is a device, a firmware, a website and a pile of documentation, and people contribute to all of them. This page says where each kind of contribution goes, what checks it meets, and the few rules that are not negotiable. Where to *ask* things is [SUPPORT.md](SUPPORT.md); how we treat each other is [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md); what is where is [docs/REPOSITORY.md](docs/REPOSITORY.md).
+Patternflow is a device, a firmware, a website and a pile of documentation, and people contribute to all of them. This page says where each kind of contribution goes, what checks it meets, and the few rules that are not negotiable. Where to *ask* things is [SUPPORT.md](.github/SUPPORT.md); how we treat each other is [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md); what is where is [docs/REPOSITORY.md](docs/REPOSITORY.md).
## Where your contribution goes
| You have… | It goes in | A change touches | What CI runs on it |
| :--- | :--- | :--- | :--- |
| A fix to a guide, a README or a spec | the markdown file itself: the root guides, `docs/`, any folder's README | one file | every relative link must resolve (`docs-links`) |
+| A fix to the **interactive guide** at [patternflow.work/guide](https://patternflow.work/guide) (Build, Play, Make, Audio) | its words are TypeScript, English and Korean side by side: `web/src/components/guide/copy.ts` for Play, `web/src/components/guide/copy/` for Build, Make, Audio and the hub | the copy, both languages | `web-ci` |
| A fix to the **breadboard** build guide | `web/src/app/build/breadboard/page.tsx` — that guide is React, not markdown | JSX text | `web-ci` (`next build`) |
| Your finished build, for the [build map](https://patternflow.work/inside) | `web/src/components/sections/InsideGlobe/builds.ts` plus a photo folder `web/public/builds//` — see [Your build](#your-build). Not a git person? The [Share your build](../../issues/new?template=share_build.yml) form does the same | one `Build` entry and its photos | `web-ci` (the typecheck rejects a malformed entry) |
| A firmware **feature** | `firmware/patternflow/features//`, plus one line in the bundle it joins (`firmware/bundles//features_local.h`) | the folder, the bundle line, an inventory row in `features/README.md`; a console page also adds a row to `firmware/toolchain/console_pages.py`; a library also goes in `platformio.ini` | `firmware-checks` (boundary, ABI freeze, presets, console pages…) and `firmware-build` (every edition, from scratch) |
| A fix to the firmware **core** (`patternflow.ino`, `src/`) | the file itself — for a bug or a real improvement to the device, never to make room for a feature | | the same, and the change must still be right on a build that has no features at all |
| An enclosure or mechanical **remix** | `hardware/case/remixes/-/`, with a README that states Author, License, what it is based on, which board it fits, and the print or cut settings | one new folder; the official folders stay as they are | the README fields are checked |
| A **bridge** to host software (TouchDesigner, Node-RED, Max, a DAW…) | `integrations//` with a README, plus a row in `integrations/README.md`. It talks to the panel through the contracts in `docs/` and never imports firmware or web code | one new folder | the README has to name the contract it is built against |
-| A change to a **wire contract** | [`docs/rest-api.md`](docs/rest-api.md), [`osc-spec.md`](docs/osc-spec.md), [`midi-spec.md`](docs/midi-spec.md), [`mqtt-spec.md`](docs/mqtt-spec.md), [`audio-ws-spec.md`](docs/audio-ws-spec.md), [`pfst-v2-spec.md`](docs/pfst-v2-spec.md) | the spec: bump its version line and add a Version history row | `docs-links` |
-| Web app work | `web/` — start at [`web/README.md`](web/README.md) and [`web/ARCHITECTURE.md`](web/ARCHITECTURE.md) | | `web-ci`: lint, typecheck, the `check:*` suites, `next build` |
+| A change to a **wire contract** | [`docs/rest-api.md`](docs/rest-api.md), [`osc-spec.md`](docs/osc-spec.md), [`midi-spec.md`](docs/midi-spec.md), [`mqtt-spec.md`](docs/mqtt-spec.md), [`audio-ws-spec.md`](docs/audio-ws-spec.md), [`pfst-v2-spec.md`](docs/pfst-v2-spec.md) | the spec: bump its version line and add a Version history row. A vector that quotes a sentence you change changes with it, in the folder beside the spec ([`contract-vectors.md`](docs/contract-vectors.md)) | `docs-links`, and `drift`, which replays every vector against the [virtual panel](tools/virtual-panel/README.md) |
+| Web app work | `web/` — start at [`web/README.md`](web/README.md) and [`web/ARCHITECTURE.md`](web/ARCHITECTURE.md) | | `web-ci`: lint, typecheck, `check:ci` (the smoke suites and the drift checks), `next build` |
| A **pattern** | **not this repository.** Make it in the [Pattern Lab](https://patternflow.work/pattern-lab), press **Publish**, and it is on the [Community](https://community.patternflow.work/community) for every Patternflow to play, under the license you choose. [PATTERN_GUIDE.md](PATTERN_GUIDE.md) walks the loop. The presets bundled in the firmware and the Basics pack are curated by the maintainer from the repository's own sources | | |
Parts sourcing tips, better photos of a build step, a clearer sentence: all of those are the first row.
+Every pull request, whatever its row, also runs `drift`. It fails when two things the tree keeps by hand stop agreeing, the BOM and the parts table in `BUILD_GUIDE.md` for one, and when a word of the maintainer's own writing changes ([docs/rfc-cleanup.md §2.9](docs/rfc-cleanup.md#29-human-written-text-lock) names the files).
+
## Sending it
1. Fork the repository and branch from `dev` (`fix/…`, `feat/…`, `docs/…`).
@@ -75,7 +78,7 @@ A pin on the map is one entry in [`web/src/components/sections/InsideGlobe/build
Inside includes builds, projects derived from Patternflow, and places where it is used. Add one entry to `web/src/components/sections/InsideGlobe/builds.ts`: a short `title`, `maker`, public `location`, `date`, `description`, and any external `links`. Photos are optional. Keep existing `id` and `slug` values stable so shared links keep working.
-Choose one primary `category`: `builds` for assembled devices and enclosure changes, `projects` for ports and new tools, or `in-use` for installations, performances and other uses. This is separate from `kind`, which preserves the existing build/collaboration relationship and marker style. A maker can have several entries: a new tool and a later exhibition are different records. Link to the maker's own documentation for ongoing updates; a short introduction here is enough. Do not add unannounced work or assume an event happened from a sale or a planned appearance.
+Choose one primary `category`: `builds` for assembled devices and enclosure changes, `projects` for ports and new tools, or `in-use` for installations, performances and other uses. This is separate from `kind`, which says what the pin is and picks its marker on the globe: `build`, `collaboration`, `sold` or `showing`, each explained in the comment above `BuildKind`. A collaboration, a sale or a showing names the pin it grew out of in `originId`, and the globe draws a line back to it. Left out, the line goes to the first build in the file. A maker can have several entries: a new tool and a later exhibition are different records. Link to the maker's own documentation for ongoing updates; a short introduction here is enough. Do not add unannounced work or assume an event happened from a sale or a planned appearance.
Classify the subject of the entry, not its surroundings. A completed device photographed in a studio or DJ booth is still a `builds` entry. Use `in-use` when the entry documents actual use in an exhibition, performance or installation; a sale or delivery alone is not enough. Leave a category empty until a qualifying case is available.
diff --git a/FEATURE_GUIDE.md b/FEATURE_GUIDE.md
index 96db5e767..8b16150c4 100644
--- a/FEATURE_GUIDE.md
+++ b/FEATURE_GUIDE.md
@@ -160,7 +160,9 @@ python firmware/toolchain/console_pages.py check # if you added a console page
Compiling is not testing — most historical regressions were invisible on the
build in front of whoever caused them, which is why `build.sh all` exists.
CI runs the same set ([`firmware-checks.yml`](.github/workflows/firmware-checks.yml),
-[`console-sync.yml`](.github/workflows/console-sync.yml)).
+[`console-sync.yml`](.github/workflows/console-sync.yml), and
+[`firmware-build.yml`](.github/workflows/firmware-build.yml) for every
+composition, one job each).
If the user asks for a PR against this repository: it should touch only
`features//` plus a bundle, pass everything above, and follow
diff --git a/PATTERN_GUIDE.md b/PATTERN_GUIDE.md
index ca4cf7173..3ba06a3da 100644
--- a/PATTERN_GUIDE.md
+++ b/PATTERN_GUIDE.md
@@ -24,22 +24,30 @@ in a window, and a practice AI to take the two prompts.
## 0. Before you start
-**A USB cable is for exactly one moment.** The very first browser flash at
-[patternflow.work/pattern](https://patternflow.work/pattern) — you set up
-Wi-Fi as part of it, and from then on **everything is wireless.** Whether
-you're in the community or in Pattern Lab, every path a pattern takes to the
-device goes over Wi-Fi.
+**A USB cable is for exactly one moment: the very first flash**, with the
+ESP32-S3 out of the board, from **Flash Patternflow** at
+[patternflow.work/pattern](https://patternflow.work/pattern). Put the module
+in download mode first — hold **BOOT**, tap **RST**, let go of BOOT — then
+press the button. The Wi-Fi step usually does not appear after that first
+install, and that is expected: press **RST**, run **Flash Patternflow** again
+and choose **Connect to Wi-Fi**, or seat the module, power the panel and join
+the network it raises for itself, `patternflow-xxxx` (password
+`patternflow`), to set Wi-Fi at `http://192.168.4.1/`. From then on
+**everything is wireless.** Whether you're in the community or in Pattern
+Lab, every path a pattern takes to the device goes over Wi-Fi.
So this whole guide rests on one assumption: **the device is powered on and
on the same Wi-Fi network as your computer.** If you haven't done the first
flash and Wi-Fi setup yet, finish the
-[firmware section of BUILD_GUIDE](BUILD_GUIDE.md#8-firmware) first.
+[firmware section of BUILD_GUIDE](BUILD_GUIDE.md#8-firmware) first, or the
+Play guide's [Flash chapter](https://patternflow.work/guide/play#flash), which
+walks it on a board you can turn.
| You need | Why |
| --- | --- |
| An assembled, flashed Patternflow | It's what this guide is about |
| The device connected to Wi-Fi | The premise of every transfer here |
-| Firmware **v3.2.0 or newer** | Pattern modules (wireless install) need it |
+| Firmware **v3.10.5 or newer** (the flasher installs the current release) | Pattern modules (wireless install) need v3.2.0, the Basics pack v3.5.1, and the larger patterns, as they are built now, v3.10.5 |
| A community account (optional) | Browsing, knobs and deck-building work without one. Sending to your device (building) and sharing / liking / forking need a sign-in |
The device's page lives at **`patternflow.local`**. Android can't resolve
@@ -55,7 +63,7 @@ travels to the device as a module — a `.pfm` file of a few KB — and arrives
over Wi-Fi in seconds.** The firmware is not rebuilt and nothing is reflashed.
The buttons wear slightly different names on different screens —
-`Send to my Patternflow`, `SEND TO MY BOARD`, `APPLY TO MY PATTERNFLOW` — but
+`Send to my Patternflow`, `SEND TO MY BOARD`, `Apply to my Patternflow` — but
they are all the same thing: compile this pattern into a module, hand it to
the board.
@@ -208,8 +216,7 @@ Now for the making side. Click **Pattern Lab ↗** at the community's top right.
Pattern Lab has a lot of panels, but **this guide uses exactly two things:
pattern generation and the color ramp.** They're enough to take a pattern from
-nothing to your device. Layers, pixel art, the gallery — all of that is for a
-later guide.
+nothing to your device. Layers and pixel art are for a later guide.
### 4-1. Pick a resolution first
@@ -313,7 +320,7 @@ to **try it on the board first.**

-1. Press **`APPLY TO MY PATTERNFLOW`** — it builds the module and takes
+1. Press **`Apply to my Patternflow`** — it builds the module and takes
seconds.
2. In the dialog, press **`Send over Wi-Fi`** — with the device on and
connected, it uploads immediately.
@@ -363,11 +370,12 @@ onto their device. The loop has come all the way around.
| Symptom | Check |
| --- | --- |
+| The panel isn't on Wi-Fi, or the flasher never asked for a network | Expected after a first flash. Press **RST**, run **Flash Patternflow** again and choose **Connect to Wi-Fi** (2.4 GHz only; type the name exactly). Or power the panel, wait about 15 s, join its own network `patternflow-xxxx` (password `patternflow`; hold **K2** for the exact name; not in the phone's list yet? refresh it) and pick your network at `http://192.168.4.1/` — [BUILD_GUIDE §8](BUILD_GUIDE.md#8-firmware) |
| `Send over Wi-Fi` does nothing, or fails | Device power · **same Wi-Fi network** as the computer · on Android, use the IP from the NETWORK screen (hold K2) instead of `patternflow.local` |
| The device's Patterns page won't open | Same as above — try typing `patternflow.local` into the address bar directly |
| A card has no `+` (add to deck) | Not a bug — the author shared it without an `.h` header (not hardware-verified) |
| The `.h` conversion looks wrong on the device | Ask the AI to convert again (a plain retry often fixes it) · simplify the pattern code · try a different model |
-| Modules uploaded but don't appear in the list | Firmware older than v3.2.0 — update it at [patternflow.work/update](https://patternflow.work/update) |
+| Modules uploaded but don't appear in the list | Firmware older than v3.2.0 — update it at [patternflow.work/update](https://patternflow.work/update). A larger pattern that shows PATTERN FAILED on firmware older than v3.10.5 needs the same update |
| A `.zip` pack was dropped but nothing installed | The pack has to contain `.pfm` modules. A zip of `.js` patterns is not a pack — build them first |
| The deck's order came out wrong on the device | Re-download the pack: the running order ships inside it, and a pack built before the deck was rearranged carries the old one |
| The device is out of storage | The Patterns page shows free space (KB free) at the top right — delete modules you don't use |
@@ -375,5 +383,5 @@ onto their device. The loop has come all the way around.
---
-*The rest of Pattern Lab — layer compositing, pixel art, the gallery — gets
-its own guide later.*
+*The rest of Pattern Lab, layer compositing and pixel art, gets its own guide
+later.*
diff --git a/README.md b/README.md
index 2208549a1..5df970569 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,20 @@
+
+
# Patternflow
-[](https://github.com/engmung/Patternflow)
+**An open-source LED synthesizer.**
+Play light patterns with your fingertips.
+
+**[Try one in your browser](https://patternflow.work/pattern)** · **[Build one](https://patternflow.work/guide/build)** · **[Get one assembled](https://www.crowdsupply.com/engmung/patternflow)**
+
[](./LICENSE-MIT)
[](./LICENSE-CC-BY-SA)
[](../../releases)
-[](https://www.crowdsupply.com/engmung/patternflow)
+[](https://www.crowdsupply.com/engmung/patternflow)
[](https://discord.gg/Vr9QtsxeTk)
+
-> **Photosensitivity warning.** Patternflow displays rapidly changing light patterns that may trigger seizures in people with photosensitive epilepsy. If you experience any discomfort, stop use immediately.
+> **Photosensitivity warning.** Patternflow displays rapidly changing light patterns that may trigger seizures in people with photosensitive epilepsy. Viewer discretion is advised. If you experience any discomfort, stop use immediately.
+
+**Every Patternflow plays every pattern we make.** Four knobs, and the pattern answers as you turn them: 8,192 points of light, every frame computed live on the device, nothing played back. A pattern is a small file, so one made anywhere plays on all of them. And every part of the instrument is open, from the circuit board to the firmware.
-**Patternflow is an open-source LED synthesizer.** You play it with four knobs. 8,192 pixels across a 128 × 64 matrix respond the instant you turn one; nothing is pre-rendered, every frame is computed live on the device. A pattern is just a small file, so every Patternflow plays every pattern anyone makes.
+Started by media artist SeungHun Lee ([@engmung](https://github.com/engmung)) in Seoul, and made by everyone who builds one, makes a pattern or sends a fix.
-**Start with [the guide](https://patternflow.work/guide).** It builds one with you, shows you how to play it, walks you through making patterns, and connects sound and MIDI, each step on a 3D Patternflow you can turn in the browser: **[Build](https://patternflow.work/guide/build)** · **[Play](https://patternflow.work/guide/play)** · **[Make](https://patternflow.work/guide/make)** · **[Audio](https://patternflow.work/guide/audio)**.
+**Start where you are.** [The guide](https://patternflow.work/guide) shows every step on a 3D Patternflow you can turn, in English and Korean, with a **Stuck here?** link on each:
+
+- **[Build](https://patternflow.work/guide/build):** you're soldering one from bare parts, about US$100–200 of them. Or read [BUILD_GUIDE.md](BUILD_GUIDE.md).
+- **[Play](https://patternflow.work/guide/play):** yours is built. Flash it, put it on your Wi-Fi, learn the four knobs. Or read [First light](#first-light), below.
+- **[Make](https://patternflow.work/guide/make):** make your own patterns, with or without a Patternflow. Or read [PATTERN_GUIDE.md](PATTERN_GUIDE.md).
+- **[Audio](https://patternflow.work/guide/audio):** it plays; now add sound and MIDI with the Audio edition. Or read [AUDIO_GUIDE.md](docs/AUDIO_GUIDE.md).
## Where it began
Almost every screen around us is a playback device. It takes in content and shows it to us, and we sit in front of it and watch. Patternflow is different: touch it and it answers in that instant, and nobody plays the same thing twice.
-In January 2026 I stopped by the Nam June Paik Art Center, which happened to be running an event marking twenty years since Paik's death. That is how I ended up standing in front of *Participation TV* (1963): you speak into a microphone and the picture on the screen moves with your voice. It turned the viewer from someone watching into someone taking part. I spoke into it, and my voice became the picture.
+In January 2026 I stopped by the Nam June Paik Art Center, which happened to be running an event marking twenty years since Paik's death. That is how I ended up standing in front of *Participation TV*: you speak into a microphone and the picture on the screen moves with your voice. It turned the viewer from someone watching into someone taking part. I spoke into it, and my voice became the picture.
Two months later I connected an LED matrix and a potentiometer for the first time.
-Where Paik brought the audience into the work, Patternflow hands over the making of it.
+I put my first pattern, *Patternflow: Origin*, on an LED matrix with four knobs and posted it online. Some people asked how to buy it; far more asked how to build it. So everything went public: schematics, firmware, 3D models, the build guide, and the whole workflow for making new patterns.
-I put that first pattern, *Patternflow: Origin*, on an LED matrix with four knobs and posted it online. Some people asked how to buy it; far more asked how to build it. So everything went public: schematics, firmware, 3D models, the build guide, and the whole workflow for making new patterns.
+**A contemporary reinterpretation of Nam June Paik's *Participation TV* (1963). Paik let the audience change the image. Patternflow lets you make it, and give it away.**
## The community responded
@@ -42,181 +57,239 @@ I put that first pattern, *Patternflow: Origin*, on an LED matrix with four knob
Builds by Nath (UK), shooter (Poland), day (France), Enerjoy (Norway), SimonePDA (UAE), Nick (USA), and Jon (USA).
-Builders from around the world keep adding pins to the **[build map](https://patternflow.work/inside)**. Someone built theirs as a Raspberry Pi port with a 64 × 32 matrix, another layered paper and acrylic into a diffuser, others cut their own enclosures. Two of them live in DJ booths.
+Two of the builds above live in DJ booths. More keep arriving on the **[build map](https://patternflow.work/inside)**: a wooden case in Mexico City, a horizontal desk case in Kyiv that is now one of the remixes in this repo, a plaster shell hanging on a wall in Germany with a Raspberry Pi inside.
+
+
+
+
+
+I started this alone. It hasn't worked that way for a while now. Web upload began as someone else's suggestion and proof of concept, and shipped as a main feature two days later. The pattern modules that install over Wi-Fi came from **[Simone Majocchi](https://github.com/SimonePDA)**, who designed the loader, built it and proved it on hardware before proposing it. They're core to the instrument now, and Simone went on to bring MQTT, a laser-cut case and the Performance edition. Martin Lopez has put more patterns on the Community wall than anyone, and a Patternflow went to California as thanks.
-I started this alone. It hasn't worked that way for a while now. The web-upload feature wasn't mine: someone suggested it, built a proof of concept, and shared the code, and two days later it shipped as a main feature. The `.pfm` pattern modules came the same way. A contributor designed the loader, built it, and proved it on hardware before proposing it. They're core to the instrument now.
+In Iran, Azmano took Patternflow's patterns, added MIDI and a microphone, and built an instrument of their own that runs in a browser, **[MOTIFLOW](https://azmano.art/portfolio-item/motiflow/)**, shown in a gallery and played at events since. Patternflow had become something people build their own instruments out of.
-The clearest case started on the other side of the world. A media art collective took Patternflow's pattern modules, added MIDI control and a microphone, and built a browser-based instrument of their own: **[MOTIFLOW](https://azmano.art/portfolio-item/motiflow/)**, recently shown in a gallery. Patternflow had become something people build their own instruments out of.
+Underneath is an instrument still being designed, in the open, by whoever shows up. It's already moving in directions I didn't choose, and none of it was assigned. The next one is open too: a bridge for TouchDesigner or Node-RED has a place waiting in [`integrations/`](integrations/README.md), and [Contributing](#contributing) says how to start.
**Patternflow is a thing we're all making now.**
## Make it yours
-### 1. The device
+Try it in the Live Editor, make it in Pattern Lab, share it on Community, and it plays on every Patternflow in the world. Then build one to play it on.
-The **[Full Build Guide](BUILD_GUIDE.md)** covers the official route: the custom PCB and a 3D-printed enclosure. The **[build guide in 3D](https://patternflow.work/guide/build)** is the same route, every step shown on a board you can turn. Don't want to order a board? The **[Breadboard Build Guide](https://patternflow.work/build/breadboard)** wires the same electronics with jumper wires instead, and that's a real Patternflow, not a temporary prototype. Every other combination is on the **[Assembly Map](docs/assembly/README.md)**. Parts run about US$100–200 ([BOM](BUILD_GUIDE.md#1-bill-of-materials-bom)), it's all big through-hole joints, and every first-timer who has built one finished it. Most came back saying the soldering was the fun part.
+### Patterns
-**When yours lights up, tell us.** Post it in [Discord](https://discord.gg/Vr9QtsxeTk) or fill in the [Share your build](../../issues/new?template=share_build.yml) form and it goes on the build map, a globe of Patternflows where each pin carries its build's story. The map is for the ones people made themselves: every pin is someone who built one from these files, in their own material, wherever they are. The goal is simple: cover it with pins.
+You don't need hardware to start. The **[Live Editor](https://patternflow.work/pattern)** is a Patternflow in your browser: the same knobs, the same detents, no account and no install. Patterns are JavaScript, and you don't have to write it yourself: click **Copy creation prompt**, paste it into your AI assistant with the look you want, paste back the code it returns, and play. The template handles the encoder mapping, brightness curve and pixel buffer; you describe the look.
-
+
-If you'd rather have one arrive assembled, tested, and flashed, the **[Crowd Supply campaign](https://www.crowdsupply.com/engmung/patternflow)** is currently live. Back the campaign to have an assembled unit, DIY kit, or custom enclosure shipped worldwide.
-
-### 2. Patterns
+**[Pattern Lab](https://patternflow.work/pattern-lab)** is the studio, and where a pattern reaches the hardware. Make variations through any AI chat, then shape the color ramps and retune the knob ranges. Send it to your Patternflow and it builds into a small `.pfm` module that installs over Wi-Fi in about ten seconds, with no cable, no reflash and no IDE. Graphic Export renders the same pattern as a print-size PNG for a business card or an MP4 loop for a post, in your browser, with nothing uploaded.
-You don't need hardware to start. The **[Live Editor](https://patternflow.work/pattern)** is a full Patternflow simulator: turn the virtual knobs and it behaves like the real thing. Patterns start as JavaScript in the browser, and you don't have to write it yourself. Click **Copy creation prompt**, paste it into your AI assistant with a description of the look you want, paste back the code it returns, and play. You don't write GLSL or touch a rendering pipeline: the template handles the encoder mapping, brightness curve and pixel buffer, and you describe the visuals.
+Then publish it to the **[Community](https://community.patternflow.work/community)**: more than a hundred patterns, from quiet waves to chaos-theory studies, all playable under the same four knobs. Collect them into a deck and send it to your board in one click. Browsing needs no account, and publishing asks only a username and password, no email. New studies go up on **[Instagram](https://www.instagram.com/patternflow.work)** almost daily; publish something that stands out and it can get featured there, or ask for a collab and I'll post yours with you. The [Make guide](https://patternflow.work/guide/make) walks the whole loop as a tutorial you do as you read, no hardware needed.
-
+
-**[Pattern Lab](https://patternflow.work/pattern-lab)** is the full studio, and where a pattern reaches the hardware. Generate variations in batches (in the Lab with your own Gemini key, or through any AI chat), shape color ramps, retune knob ranges, then send it to your device: it builds into a small `.pfm` module and installs over Wi-Fi, about ten seconds start to finish. You never plug in a cable, reflash the board, or open an IDE. The Graphic Export panel takes the same pattern off the panel: a PNG at print size for a business card, or an MP4 loop for a post, rendered in your browser with nothing uploaded.
+### The device
-When it looks right, publish it to the **[Community](https://community.patternflow.work/community)**. More than a hundred patterns are up already and the range keeps widening, from quiet waves to chaos-theory studies, every one written as code and every one playable under the same four knobs. Collect patterns into a deck and send it to your board in one click. Browsing needs no account, and publishing asks a username and password, no email. The **[Pattern Guide](PATTERN_GUIDE.md)** walks the whole loop, and the **[Make guide](https://patternflow.work/guide/make)** is that loop as a tutorial you do as you read.
+The **[Build Guide](BUILD_GUIDE.md)** is the official route: the v3.9 board in a 3D-printed case, about US$100–200 in parts ([BOM](BUILD_GUIDE.md#1-bill-of-materials-bom)). Order first and build later: the parts take about two weeks to arrive, and the case prints in about ten hours on a 256 mm bed. The rest is about an hour of hands-on work, half of it soldering, and every joint is a big through-hole one. Every first-timer who has built one finished it, and most came back saying the soldering was the fun part. The **[build guide in 3D](https://patternflow.work/guide/build)** is the same route on a board you can turn.
-
-
-
+**Buy the LED panel the [BOM](BUILD_GUIDE.md#off-the-board) links.** It's verified, and the case is measured around its screw holes. Another 128 × 64 panel can match the spec line for line and still stay completely dark, because its driver chips are wrong for the board; **[Panel Compatibility](docs/panel-compatibility.md)** says how to tell before you pay.
-Patterns don't stay on the wall, either. New pattern studies go up on **[Instagram](https://www.instagram.com/patternflow.work)** almost daily. Publish something that stands out and it can get featured there, or ask for a collab on Instagram and we'll post yours together.
+Other routes lead to the same instrument. No printer? **[Simone Majocchi's case](hardware/case/remixes/simonepda-lasercut/README.md)** is laser-cut from acrylic or MDF, and [two more printed cases](hardware/case/remixes/README.md) sit beside the official one. No board order? The **[breadboard build](https://patternflow.work/build/breadboard)** wires the same electronics with jumper wires, and that's a real Patternflow, not a temporary prototype. Every other combination is on the **[Assembly Map](docs/assembly/README.md)**. Holding a v2.x board? [BUILD_GUIDE_v2.md](docs/legacy/BUILD_GUIDE_v2.md), with everything it needs bundled at [v2.1.0](https://github.com/engmung/Patternflow/releases/tag/v2.1.0). The current board's known issues and design notes are [BUILD_GUIDE §10](BUILD_GUIDE.md#10-known-issues--design-notes).
-### 3. The ecosystem
+**When yours lights up, share it** in [Discord](https://discord.gg/Vr9QtsxeTk) or through the [Share your build](../../issues/new?template=share_build.yml) form, and I'll add your pin to the build map. The goal is simple: cover it with pins.
-Patterns are the surface. Underneath is an instrument still being designed, in the open, by whoever shows up. This is the whole system on one napkin:
+Rather have one arrive assembled, tested and flashed? It's on **[Crowd Supply](https://www.crowdsupply.com/engmung/patternflow)**, shipped worldwide. The campaign is past its goal and runs until 15 October 2026.
-
-
-
+## First light
-It's already moving in directions I didn't choose. One contributor is building out MQTT further than I've had time to follow: units reaching each other across a network, one person's playing coming out of somebody else's device. Another is working on sound on the board itself. Another is taking the TouchDesigner link further than I did. None of it was assigned.
+For a DevKit that has never run Patternflow, or one that needs rescuing. The same steps, with the flasher's own screens and a **Stuck here?** link on each, are the Play guide's **[Flash](https://patternflow.work/guide/play#flash)** and **[Patterns](https://patternflow.work/guide/play#patterns)** chapters; [BUILD_GUIDE §8](BUILD_GUIDE.md#8-firmware) is the same order in writing, with more on each step. Already running Patternflow, or got one assembled? Skip download mode, which erases installed patterns and saved Wi-Fi: power it from `J4` and start at step 5. Later firmware comes from the console's **Update** page over Wi-Fi, which keeps everything.
-**[CONTRIBUTING.md](CONTRIBUTING.md)** covers how contributions flow. Questions and ideas go to the **[Discord](https://discord.gg/Vr9QtsxeTk)**; where to ask what is **[SUPPORT.md](SUPPORT.md)**.
+> **Power goes in through `J4`, the screw terminal, and nowhere else:** a USB cable from a 5 V power bank, cut, red to +5V and black to GND. Never power it through the DevKit's USB-C ports: they are for flashing, with the DevKit out of the board.
-## Quick facts
+1. **Flash the DevKit on its own, out of the board,** from desktop Chrome or Edge. Use its **left** port, labelled `USB` (antenna up, ports toward you): the right one flashes too, but the Wi-Fi step never appears there. Use a USB-C cable that carries data; a charge-only cable is the usual reason nothing shows up.
+2. **Download mode first:** hold **BOOT**, tap **RST** (EN on some DevKits), let go of BOOT. Then press **[Flash Patternflow](https://patternflow.work/guide/play#flash-5)**, pick `USB JTAG/serial debug unit` and install. It takes about two minutes and erases the flash, which a new DevKit doesn't mind.
+3. **Then the Wi-Fi, which takes a second go.** The dialog usually ends without asking for it, and nothing failed: close it. Press **RST**, press **Flash Patternflow** again and pick the port, and this time it opens on Patternflow's own menu. Choose **Connect to Wi-Fi**, let the short scan finish (it lists nothing, by design), and type your network's exact name (capitals count, 2.4 GHz only) and its password.
+4. **Unplug the DevKit from the computer and, with the power bank unplugged, seat it.** Its USB-C ports go toward the `USB` mark on the board, all 44 pins in. Then plug the `J4` cable into the power bank: within a second or two a red, squarish pattern comes on. That's **Origin**, and one pattern is right for now.
+5. **Give it 15 seconds, then hold K2, the top-left knob,** for a second. `CONNECTED` and an address: you're on your Wi-Fi. `HOTSPOT` and a name like `patternflow-a1b2`: it found no network it knows, so it became one. Join it (password `patternflow`; the phone will say there's no internet, which is true, and it stays connected), open `http://192.168.4.1/` and add your network on the Wi-Fi page.
+6. **Back on that Wi-Fi, install the Basics pack.** On the [decks page](https://community.patternflow.work/community/decks), press **Install to my board** on the Basics card: 33 patterns, no account. The first time, the board asks to format its storage: press **Format storage**, confirm, and the pack installs by itself. The board's console is at `patternflow.local`; on Android, which can't open that name, type the address K2 shows into the card's **Device address** first.
-| | |
-| :--- | :--- |
-| **Display** | 128 × 64 HUB75 RGB LED matrix, P2.5 (320 × 160 mm) |
-| **Brain** | ESP32-S3-WROOM-1 **N16R8** (16 MB flash, 8 MB PSRAM) · standalone, no sending card |
-| **Input** | 4× EC11 rotary encoders with push-switch; long-press encoder 4 to switch patterns ([controls](#playing-it)) |
-| **Power** | 5 V over USB from any power bank; about **4 h per 10,000 mAh** at max brightness with a typical pattern (see [runtime](#power--runtime)) |
-| **Size / weight** | 245 × 325 × 36 mm (9.6 × 12.8 × 1.4 in) · 933 g (2.06 lb) |
-| **Firmware** | Arduino-compatible C++, modular pattern architecture, runtime switching (no reflash) |
-| **Flashing** | Everything from the browser: one USB flash the first time, then it's all Wi-Fi. Patterns install as modules in seconds, full firmware builds land wirelessly too. A local build (PlatformIO) only for firmware development or other matrix resolutions |
-| **Connectivity** | Wi-Fi and USB. Network MIDI (a MIDI port in any DAW — Ableton, Logic, Bitwig), bidirectional OSC (Max/TouchDesigner/Resolume), MQTT and audio-react each ship in an [edition](docs/EDITIONS.md) you install in one click — the way patterns do — and switching keeps your patterns, networks and settings |
-| **Editions** | One image ships on the board; others are a click away on [the shelf](https://patternflow.work/editions); what each feature does, with a reel of it, is on [the features page](https://patternflow.work/features). Writing a feature or cutting your own firmware starts at **[docs/EDITIONS.md](docs/EDITIONS.md)** |
-| **Build** | ~1 h hands-on (≈30 min soldering + ≈30 min assembly, first-build friendly) + ~10 h 3D printing · US$100–200 in parts ([BOM](BUILD_GUIDE.md#1-bill-of-materials-bom)) |
-| **License** | MIT (firmware & web code) · CC-BY-SA 4.0 (hardware, docs, bundled patterns) · community patterns are licensed by their authors ([summary](docs/LICENSE-SUMMARY.md)) |
+Panel stays dark? Check that the ribbon is in the panel's `IN`, not `OUT`, that red goes to +5V at both `J3` and `J4`, and that the power bank is on; for a panel other than the BOM's, see [Panel Compatibility](docs/panel-compatibility.md). Only starts after a press of RST? See [BUILD_GUIDE §10](BUILD_GUIDE.md#10-known-issues--design-notes). Then finish with [BUILD_GUIDE §9](BUILD_GUIDE.md#9-final-checks): every knob, one power cycle, the back panel, and the knobs last.
-### Power & runtime
+## Playing it
-Patternflow runs off any standard USB power bank that can supply a couple of amps at 5 V. You don't need a wall adapter, so it stays portable. Rule of thumb: **about 4 hours per 10,000 mAh** at maximum brightness with a typical pattern.
+Every knob does two jobs. Turn it, or give it a short press, and you move the pattern that's running; what each knob does there is the pattern's own decision, so the same knob feels different on every one. Hold it down and a device function opens instead, and a second long-press closes it.
-
-Measured runtimes
+
+
+
-In testing, a 40,000 mAh bank dropped about 13% over 2 hours at **full brightness**, which is roughly **15 hours on a full charge**:
+| Hold | Where | Opens |
+| :--- | :--- | :--- |
+| **K1** | top right | **Brightness** |
+| **K2** | top left | **Network**: the board's address. Type it, or `patternflow.local`, into a browser for the web console, every feature the device has, from a phone too. With no Wi-Fi it shows the name of its own network instead |
+| **K3** | bottom right | **Knob numbers**, each knob's number on the panel; the order isn't the one you'd guess |
+| **K4** | bottom left | **Patterns**: turn to browse what's installed, stop on one to play it, long-press again to close |
-| Power bank | Approx. runtime (max brightness) |
-| :--- | :--- |
-| 10,000 mAh | ~4 hours |
-| 20,000 mAh | ~8 hours |
-| 40,000 mAh | ~15 hours |
+### What's on the device
-These were measured with a bright generative pattern at maximum brightness, which is how the device is normally used. Lowering the brightness (long-press encoder 1) extends runtime well beyond these figures.
+A new board boots into **Origin**, concentric sine waves sampled by an emergent grid. It is the one pattern compiled into the firmware, the failsafe a board can always boot into. Everything else lives on the board's storage as **`.pfm` modules**, up to 128 of them, installed over Wi-Fi with no reflash. Adding a pattern never costs a firmware update, and a firmware update never touches your patterns, your Wi-Fi or your storage.
-**Sleep mode** takes it further: the panel goes dark, the HUB75 transfer stops and the board idles, while staying on Wi-Fi so it can be woken again from MQTT or a browser. Hold encoder 2 for the NETWORK screen, then turn encoder 1; any knob or button wakes it. The pattern that was running comes back, as it does after a full power cut.
+The **Basics pack** is 33 patterns at the top of the [decks shelf](https://community.patternflow.work/community/decks), one click and no account; or drop its `.zip` on your board's Patterns page yourself. The [Live Editor](https://patternflow.work/pattern) opens with a library of its own, over forty patterns, each one loadable and remixable in the browser.
-**A pattern that fills the screen with white is a different story.** The same 40,000 mAh bank dropped 9% in 30 minutes: about 26.6 W, or roughly 4.8 A at 5 V, which is above what a typical USB-A port is rated for and enough to make the connector warm. That works out to ~5.5 hours on a 40,000 mAh bank. A total-pixel-power clamp is going into the firmware so this can't happen; until then, avoid near-full-white patterns on a power bank, or use a supply and cable rated for the current.
+## MIDI, OSC, MQTT & audio-react
-
+
+
+
-## Playing it
+The default firmware is the instrument and nothing else, which leaves the most memory for your patterns. MIDI, OSC, MQTT and audio-react come in **editions**, other builds of the same firmware, installed in one click from **[the editions page](https://patternflow.work/editions)** the way patterns arrive. Switching, and switching back, keeps your patterns, Wi-Fi networks and settings. [The features page](https://patternflow.work/features) shows each feature with a reel of it, and [`docs/EDITIONS.md`](docs/EDITIONS.md) explains why it's arranged this way and how to build an edition of your own.
-Every encoder does two jobs. Turn it, or give it a short press, and you are moving the pattern that's running. Hold it down and a device function opens instead, and a second long-press closes it again.
+| | What it does | Edition | Read |
+| :--- | :--- | :--- | :--- |
+| **MIDI over Wi-Fi** | The panel is a MIDI port in any DAW (Ableton, Logic, Bitwig); its knobs and buttons map like any controller's (Ctrl-M in Live), and CC and Program Change play it back | Audio | [MIDI in Ableton](docs/midi-ableton.md) · [spec](docs/midi-spec.md) |
+| **OSC, both ways** | Knobs, presses and pattern switches stream out to Max, TouchDesigner or Resolume; incoming OSC moves the panel exactly like a hand on the knob | Audio | [spec](docs/osc-spec.md) · [Max for Live bridge](integrations/ableton) |
+| **Audio-react** | A browser tab's audio in four bands you shape, or a small microphone soldered to the DevKit; every pattern reacts with no audio code of its own | Audio | [AUDIO_GUIDE.md](docs/AUDIO_GUIDE.md) · [extension](tools/patternflow-audio-extension) |
+| **MQTT** | Both ways, on the broker you already run; two panels on one broker follow each other | Performance | [spec](docs/mqtt-spec.md) |
+| **HTTP** | Knob state, pattern selection, brightness, status | every firmware | [API](docs/rest-api.md) |
-
-
-
+The panel plays its own patterns, not a video feed: nothing streams pixels to it. Your rig plays it the way a hand does, through the knobs.
-What a knob does to a running pattern is the pattern's own decision, so the same knob feels different on every one of them. That is what encoder 3 is for: hold it and each knob puts its own number on the panel, which saves counting around the front.
+**Performance**, maintained by **[Simone Majocchi](https://github.com/SimonePDA)**, is for a panel that runs a room: sequences on a timeline (cue lists and the night/wake scheduler), MQTT in every role, the weather mapped onto the knobs, and the clock.
-The device functions themselves are fixed. Encoder 1 is brightness. Encoder 4 opens the pattern list, where you turn to browse the names and long-press again to load one, choosing from whatever is installed at the time. Encoder 2 puts the board's IP address on the panel, and typing that into a browser opens the web console, which carries every feature the device has and works from a phone.
+
+More on MIDI, OSC, MQTT and audio-react
-[patternflow.work/guide/play](https://patternflow.work/guide/play) has all of this on a Patternflow you can turn, press and hold, from flashing the ESP32 to the console. The same guide builds one with you ([Build](https://patternflow.work/guide/build)) and takes you through the community and Pattern Lab ([Make](https://patternflow.work/guide/make)); [patternflow.work/guide](https://patternflow.work/guide) is where to start.
+**MIDI, over the network.** RTP-MIDI over Wi-Fi: native on macOS and iOS, one free driver on Windows. The Pattern Lab's Director exports a show as exactly the four CC lanes the knobs answer to ([Director → MIDI](docs/director-midi.md)), so a show can play on the panel from Live's transport. Sensitivity per knob, an absolute or relative mode, and the session that reconnects itself all live on the panel's console.
-## On the device
+**OSC.** Play Patternflow as a controller for your set, let your set play the light, or both at once. OSC is the language of Max, TouchDesigner, Resolume and Processing, and it carries more than MIDI can: pattern names, floats, a heartbeat. For Ableton Live Suite there's a ready-made Max for Live bridge in [`integrations/ableton`](integrations/ableton): click Connect, map the four knobs to any Live parameters, done.
-A new board boots into **Origin**, concentric sine waves sampled by an emergent grid, and long-pressing encoder 4 opens the list of whatever else is installed. Patterns live on the filesystem rather than inside the program. The firmware compiles in Origin alone, as the failsafe a board can always boot into, and everything else installs as **`.pfm` modules over Wi-Fi**, up to 128 of them, no reflash. Adding a pattern never costs a firmware update, and a firmware update never touches your patterns, your Wi-Fi, or your storage.
+**MQTT.** Knob turns and pattern changes publish as they happen, and messages coming the other way move the knobs and switch patterns exactly as a hand on the knob would. It puts the panel on the same bus as the rest of a home or venue, so Home Assistant, Node-RED or a lighting desk can turn its knobs and switch its patterns with nothing Patternflow-specific in the middle. Two boards pointed at the same broker follow each other, which is the short version of why it exists. Point it at a broker on the console's **MQTT** page; a pattern can be addressed by name or by slug. Publishing `1` to `/sleep` puts the panel to sleep and `0` wakes it, with the state mirrored on `/sleep/state`: enough for a Home Assistant switch, and the one command a panel obeys in either role. Simone's work, along with the browser-side zip unpacking that makes pattern packs install in one click.
-A new board is therefore nearly empty, so a set ships with it: the **Basics pack**, 33 patterns, at the top of the [decks shelf](https://community.patternflow.work/community/decks). One click installs the lot, no account and no build queue, or drop the `.zip` on your board's Patterns page yourself. The [Live Editor](https://patternflow.work/pattern) opens with its own preset library of 42 patterns, each loadable and remixable in the browser.
+**Audio-react.** The Chrome/Edge extension in [`tools/patternflow-audio-extension`](tools/patternflow-audio-extension) captures the current tab's audio, splits it into four bands you shape on a response graph, and sends the levels to the panel over a WebSocket. A pattern reads them as ordinary parameters. The panel can also listen for itself: a $5 PDM microphone on five thin leads runs the same analysis on the device, with no computer involved.
-A local firmware build (PlatformIO, driven by `firmware/bundles/build.sh`) is only needed for feature development or targeting an LED matrix with a different resolution; the setup is [`firmware/README.md`](firmware/README.md) and a new feature starts at [FEATURE_GUIDE.md](FEATURE_GUIDE.md). Patterns never need it: [Custom Patterns](firmware/CUSTOM_PATTERNS.md). To rebuild the shipped pack from the repo's own preset sources, see [`firmware/toolchain/make_pack.py`](firmware/toolchain/make_pack.py).
+
-## MIDI, OSC, MQTT & audio-react
+## Quick facts
-
-
-
+| | |
+| :--- | :--- |
+| **Display** | 128 × 64 HUB75 RGB LED matrix, P2.5 (320 × 160 mm); the driver chips decide which panels work ([compatibility](docs/panel-compatibility.md)) |
+| **Brain** | ESP32-S3-DevKitC-1 **N16R8** (16 MB flash, 8 MB PSRAM), plugged into sockets, never soldered · drives the panel itself, no sending card |
+| **Board** | v3.9, every soldered part through-hole · KiCad source, Gerbers and schematic in [`hardware/`](hardware/README.md) |
+| **Input** | 4× EC11 rotary encoders with push-switch ([controls](#playing-it)) |
+| **Power** | 5 V from a USB power bank into **`J4`, the screw terminal**, its only power input · about **4 h per 10,000 mAh** at max brightness ([runtime](#power--runtime)) |
+| **Size / weight** | 245 × 325 × 36 mm (9.6 × 12.8 × 1.4 in) · 933 g (2.06 lb) |
+| **Firmware** | Arduino-compatible C++, patterns as modules switched at runtime, no reflash |
+| **Flashing** | From the browser, once over USB with the DevKit out of the board; after that, patterns and firmware updates arrive over Wi-Fi |
+| **Connectivity** | Wi-Fi, an [HTTP API](docs/rest-api.md), and a network of its own when no known one is in reach · MIDI, OSC, MQTT and audio-react in one-click [editions](#midi-osc-mqtt--audio-react) |
+| **Build** | ~2 weeks for parts · ~10 h of 3D printing · ~1 h hands-on (≈30 min soldering + ≈30 min assembly, first-build friendly) · US$100–200 in parts ([BOM](BUILD_GUIDE.md#1-bill-of-materials-bom)) |
+| **License** | MIT code · CC BY-SA 4.0 hardware, docs and bundled patterns · community patterns licensed by their authors ([License](#license)) |
+
+### Power & runtime
-Each of these lives in an **edition** — a firmware you install from [the shelf](https://patternflow.work/editions) in one click, the same way patterns arrive, and switching keeps your patterns, Wi-Fi networks and settings. MIDI, OSC and audio-react are in **Audio**; MQTT is in **Performance**, alongside sequences and the Director. Why it is arranged that way, and how to build your own: [`docs/EDITIONS.md`](docs/EDITIONS.md).
+Patternflow runs off any standard USB power bank that fits the case's compartment and can supply a couple of amps at 5 V, wired into `J4`. You don't need a wall adapter, so it stays portable.
+
+
+Measured runtimes, sleep mode, and full-white patterns
+
+In testing, a 40,000 mAh bank dropped about 13% over 2 hours at **full brightness**, which is roughly **15 hours on a full charge**:
+
+| Power bank | Approx. runtime (max brightness) |
+| :--- | :--- |
+| 10,000 mAh | ~4 hours |
+| 20,000 mAh | ~8 hours |
+| 40,000 mAh | ~15 hours |
-**MIDI, over the network.** *New.* The panel is a MIDI port: RTP-MIDI over Wi-Fi, native on macOS and iOS, one free driver on Windows. Its four knobs and buttons map to anything in Ableton with Ctrl-M, and a DAW drives the panel back with plain CC and Program Change — the Pattern Lab's Director exports a show as exactly those four CC lanes, so a show can play on the panel from Live's transport. Sensitivity per knob, an absolute or relative mode, and the session that reconnects itself all live on the panel's console. Step by step with screenshots: [`docs/midi-ableton.md`](docs/midi-ableton.md); the contract: [`docs/midi-spec.md`](docs/midi-spec.md).
+These were measured with a bright generative pattern at maximum brightness, which is how the device is normally used. Lowering the brightness (long-press K1) extends runtime well beyond these figures.
-**Bidirectional OSC.** Over Wi-Fi, Patternflow speaks OSC in both directions: knob turns, button presses, and pattern switches stream out to a remote host (Ableton Live, Max/MSP, TouchDesigner, anything that speaks OSC), and incoming OSC messages drive the device exactly like physical encoder motion. Play Patternflow as a controller for your set, let your set drive the light, or both at once. OSC is the language of Max, TouchDesigner, Resolume and Processing, and it carries more than MIDI can — pattern names, floats, a heartbeat. For Ableton Live Suite there's a ready-made Max for Live bridge in [`integrations/ableton`](integrations/ableton). Click Connect, map the four knobs to any Live parameters, done. The wire protocol is documented in [`docs/osc-spec.md`](docs/osc-spec.md).
+**Sleep mode** takes it further: the panel goes dark, the HUB75 transfer stops and the board idles, while staying on Wi-Fi so a browser (or MQTT, on the Performance edition) can wake it again. Hold K2 for the NETWORK screen, then turn K1; any knob or button wakes it. The pattern that was running comes back, as it does after a full power cut.
-**MQTT.** Patternflow also speaks MQTT, both ways, on the broker you already run ([the contract](docs/mqtt-spec.md)). Knob turns and pattern changes publish as they happen; messages coming the other way move the knobs and switch patterns exactly as a hand on the encoder would. Two boards pointed at the same broker follow each other, which is the short version of why it exists. It also puts the device on the same bus as the rest of a home or venue setup, so Home Assistant, Node-RED or a lighting desk can drive it without anything Patternflow-specific in the middle. Point it at a broker on the device's own **MQTT** page; a pattern can be addressed by name or by slug. Publishing `1` to `/sleep` puts the panel to sleep and `0` wakes it, with the current state mirrored on `/sleep/state`. That is enough for a Home Assistant switch, and it is the one command a panel obeys whether it is set to Publisher or Subscriber. Contributed by **[@SimonePDA](https://github.com/SimonePDA)** (Simone Majocchi), along with the browser-side zip unpacking that makes pattern packs install in one click.
+**A pattern that fills the screen with white is a different story.** The same 40,000 mAh bank dropped 9% in 30 minutes: about 26.6 W, or roughly 4.8 A at 5 V, which is above what a typical USB-A port is rated for and enough to make the connector warm (~5.5 hours on that bank). The firmware's total power clamp, on by default, keeps that from happening: it holds the panel to a 2.4 A budget, the high-power USB-A rating, so a white-out frame lands at roughly half brightness instead, while an ordinary bright pattern (~1.9 A) never notices. The budget is `power_budget` on the [`/api/display`](docs/rest-api.md#get-apidisplay) endpoint.
-**Audio-react.** The Chrome/Edge extension in [`tools/patternflow-audio-extension`](tools/patternflow-audio-extension) captures the current tab's audio, splits it into four bands you shape on a response graph, and sends the levels to the panel over a WebSocket. A pattern reads them as ordinary parameters, so every encoder-driven pattern reacts with no audio code of its own. The panel can also listen for itself: an on-board microphone runs the same analysis on the device, with no browser involved.
+
## How it's built
-Patternflow is built around a standalone ESP32-S3 driving a HUB75 RGB LED matrix at low resolution, where each pixel reads as a discrete point of light with its own brightness and color. Four rotary encoders feed firmware written in Arduino-compatible C++ around a modular pattern architecture: each pattern is a self-contained module with its own setup, update, and draw routines, while the shared framework handles input, LED rendering, mode transitions, and color calibration. I designed the PCB myself; the enclosure is 3D-printed by default. Two community remixes sit beside it in [`hardware/case/remixes/`](hardware/case/remixes/README.md), another printed case and a laser-cut one in clear acrylic or MDF, and a stainless steel version is still in progress.
+Patternflow is built around a standalone ESP32-S3 driving a HUB75 RGB LED matrix at low resolution, where each pixel reads as a discrete point of light with its own brightness and color. Four rotary encoders feed firmware written in Arduino-compatible C++ around a modular pattern architecture: each pattern is a self-contained module with its own setup, update and draw routines, while the shared framework handles input, LED rendering, mode transitions and color calibration. I designed the PCB myself; the enclosure is 3D-printed by default, with the community remixes in [`hardware/case/remixes/`](hardware/case/remixes/README.md) beside it. A stainless steel version is still in progress.
+
+A local firmware build (PlatformIO, driven by `firmware/bundles/build.sh`) is only for feature development, or for a panel the stock image can't drive (a different driver profile or module layout, [Panel Compatibility](docs/panel-compatibility.md)). The setup is [`firmware/README.md`](firmware/README.md), and a new feature starts at [FEATURE_GUIDE.md](FEATURE_GUIDE.md). Patterns never need it ([Custom Patterns](firmware/CUSTOM_PATTERNS.md)). The Basics pack is built from the repo's own preset sources and zipped by [`firmware/toolchain/make_pack.py`](firmware/toolchain/make_pack.py).
## Repository & documentation
-> **Moving fast.** [v3.11.0 is out](https://github.com/engmung/Patternflow/releases/tag/v3.11.0): installed patterns run faster - the Basics pack is rebuilt and every one of its 33 patterns gains, 1.5 times the frames a second at the median, once the firmware is updated and the pack installed again - and a pattern you go back to resumes at once instead of loading again. The console is light and reads as one thing, and the Audio edition's page and browser extension are rebuilt: the page says who has the knobs and saves only what you changed, and the extension holds the knobs through silence. There is an [Audio guide](https://patternflow.work/guide/audio) beside Build, Play and Make. Audio is re-cut on this core as v0.7.0; Performance stays at v0.4.0, on core 3.10.4, until its maintainer re-cuts it. [v3.10.5](https://github.com/engmung/Patternflow/releases/tag/v3.10.5) made every pattern 2.45 ms a frame faster and moved a pattern's code to PSRAM, so patterns that were refused for size load and the console has more room while one is running; the panel raises its own Wi-Fi hotspot when it knows no network in reach, a panel that crashed says which pattern it was in and where, a pattern that hangs no longer takes the console down with it, and five requests that could reboot the board cannot any more. [v3.10.4](https://github.com/engmung/Patternflow/releases/tag/v3.10.4) made the panel film properly on a phone. It refreshes at exactly 300 Hz — the one rate the shutter speeds a phone actually uses (1/50, 1/60, 1/100, 1/30) all divide — so the rolling bands are gone at those; 1/120 and slow motion still band, and [the firmware README](firmware/README.md#refresh-rate-anti-flicker-for-video) says why and what to set. [v3.10.3](https://github.com/engmung/Patternflow/releases/tag/v3.10.3) made the panel's colours come out right: bright edges had been growing purple and sky-blue fringes and greys read pink; the cause was in the display driver — which bit plane each brightness window actually lights — and part of it was 3.10.1's own dark-end fix. The planes are exactly binary now, and because the fix is in the driver every installed pattern gets it without a rebuild. It also brings a [Knobs page](docs/rest-api.md#get-apiknobs-and-post-apiknobs) on the device (which way each encoder counts, set from the browser) and the [features catalogue](https://patternflow.work/features). Both editions were re-cut on that core. [v3.10.2](https://github.com/engmung/Patternflow/releases/tag/v3.10.2) made a Format that did not take stop reporting success, and a storage that will not mount say why. [v3.10.1](https://github.com/engmung/Patternflow/releases/tag/v3.10.1) fixed an upload that could reboot the board, a failed settings write that could make the next boot forget a good pattern, a mic-less Audio panel holding the knobs, and MIDI notes that would not let go. [v3.10.0](https://github.com/engmung/Patternflow/releases/tag/v3.10.0) made a heavy pattern come on every time. [v3.9.5](https://github.com/engmung/Patternflow/releases/tag/v3.9.5) taught the web console to survive a slow Wi-Fi link and a heavy pattern, and put thumbnails under the knob. [v3.8.0](https://github.com/engmung/Patternflow/releases/tag/v3.8.0) split the firmware into [editions](docs/EDITIONS.md) — core, Audio (on-board microphone, OSC, network MIDI) and Performance (sequences, MQTT, weather) — installable in one click and switchable without losing your patterns. [v3.2.0](https://github.com/engmung/Patternflow/releases/tag/v3.2.0) made patterns install over Wi-Fi as `.pfm` modules, no reflash. On v2.x hardware? Everything you need stays bundled at [v2.1.0](https://github.com/engmung/Patternflow/releases/tag/v2.1.0). Follow the [changelog](CHANGELOG.md) and the [journal](https://patternflow.work/journal) for what's current.
+Folder by folder, and what you want to do → where to start: **[docs/REPOSITORY.md](docs/REPOSITORY.md)**. Every doc is indexed in [`docs/README.md`](docs/README.md).
+
+
+The top-level folders
| Folder | Contents |
| :--- | :--- |
| `firmware/` | Arduino code for ESP32-S3, the custom pattern template, and the toolchain that builds patterns into `.pfm` modules and packs |
| `hardware/` | Enclosure files and electronics source files (case, PCB, Gerbers, schematic PDF) |
| `web/` | Next.js site (landing, the interactive guide, Live Editor, Pattern Lab, community, browser flasher & build server, journal); the breadboard build guide is a React page here |
-| `docs/` | The contracts (HTTP, OSC, MIDI, MQTT, audio WebSocket, show files), how the firmware is put together (`EDITIONS.md`), the assembly map, walk-throughs, records and media — indexed in [`docs/README.md`](docs/README.md), with the folder-by-folder map in [`docs/REPOSITORY.md`](docs/REPOSITORY.md) |
+| `docs/` | The contracts (HTTP, OSC, MIDI, MQTT, audio WebSocket, show files), how the firmware is put together (`EDITIONS.md`), the assembly map, walk-throughs, records and media |
| `tools/` | Desktop-side helpers, including the audio-react browser extension |
| `integrations/` | Host-software bridges: Ableton Live / Max for Live (OSC knob mapping) |
| `.github/` | Issue and PR templates, and the CI that runs on every pull request (web, every firmware edition, doc links, console pages) |
-**Guide (interactive):** [Start here](https://patternflow.work/guide) · [Build it](https://patternflow.work/guide/build) · [Play it](https://patternflow.work/guide/play) · [Make patterns](https://patternflow.work/guide/make) · [Sound and MIDI](https://patternflow.work/guide/audio): each step shown on a 3D Patternflow you can turn, in English and Korean
-**Build:** [Full Build Guide](BUILD_GUIDE.md) · [Assembly Map](docs/assembly/README.md) · [Panel Compatibility](docs/panel-compatibility.md) · [Hardware files](hardware/README.md)
-**Play:** [Pattern Guide](PATTERN_GUIDE.md) · [Custom Patterns](firmware/CUSTOM_PATTERNS.md) · [Audio Guide](AUDIO_GUIDE.md) · [MIDI in Ableton](docs/midi-ableton.md)
-**Extend:** [Feature Guide](FEATURE_GUIDE.md) · [Editions](docs/EDITIONS.md) · [Firmware build](firmware/README.md) · [Web architecture](web/ARCHITECTURE.md) · [HTTP API](docs/rest-api.md) · [OSC Spec](docs/osc-spec.md) · [MIDI Spec](docs/midi-spec.md) · [Director → MIDI](docs/director-midi.md)
-**Project:** [Contributing](CONTRIBUTING.md) · [Support](SUPPORT.md) · [Repository map](docs/REPOSITORY.md) · [Changelog](CHANGELOG.md) · [License Summary](docs/LICENSE-SUMMARY.md) · [Manifesto](docs/manifesto.md)
+
+**Guide (interactive):** [Start here](https://patternflow.work/guide) · [Build it](https://patternflow.work/guide/build) · [Play it](https://patternflow.work/guide/play) · [Make patterns](https://patternflow.work/guide/make) · [Sound and MIDI](https://patternflow.work/guide/audio), in English and Korean
+**Build:** [Build Guide](BUILD_GUIDE.md) · [Assembly Map](docs/assembly/README.md) · [Panel Compatibility](docs/panel-compatibility.md) · [Hardware files](hardware/README.md) · [v2.x board](docs/legacy/BUILD_GUIDE_v2.md)
+**Play:** [Pattern Guide](PATTERN_GUIDE.md) · [Custom Patterns](firmware/CUSTOM_PATTERNS.md) · [Audio Guide](docs/AUDIO_GUIDE.md) · [MIDI in Ableton](docs/midi-ableton.md)
+**Extend:** [Feature Guide](FEATURE_GUIDE.md) · [Editions](docs/EDITIONS.md) · [Firmware build](firmware/README.md) · [Web architecture](web/ARCHITECTURE.md) · [HTTP API](docs/rest-api.md) · [OSC Spec](docs/osc-spec.md) · [MIDI Spec](docs/midi-spec.md) · [MQTT Spec](docs/mqtt-spec.md) · [Director → MIDI](docs/director-midi.md)
+**Project:** [Contributing](CONTRIBUTING.md) · [Support](.github/SUPPORT.md) · [Repository map](docs/REPOSITORY.md) · [Changelog](CHANGELOG.md) · [License Summary](docs/LICENSE-SUMMARY.md) · [Manifesto](docs/manifesto.md)
**Links:** [patternflow.work](https://patternflow.work) · [Community](https://community.patternflow.work/community) · [Crowd Supply](https://www.crowdsupply.com/engmung/patternflow) · [Releases](../../releases) · [Discord](https://discord.gg/Vr9QtsxeTk) · [Instagram](https://www.instagram.com/patternflow.work)
+## What's new
+
+**Moving fast.** [v3.11.0 is out](https://github.com/engmung/Patternflow/releases/tag/v3.11.0): installed patterns run faster, a pattern you go back to resumes at once, and there is an [Audio guide](https://patternflow.work/guide/audio) beside Build, Play and Make. Follow the [changelog](CHANGELOG.md) and the [journal](https://patternflow.work/journal) for what's current.
+
+
+Recent releases in brief
+
+- **[v3.11.0](https://github.com/engmung/Patternflow/releases/tag/v3.11.0)**: installed patterns run faster. The Basics pack is rebuilt and every one of its 33 patterns runs faster, 1.5 times the frames a second at the median, once the firmware is updated and the pack installed again; a pattern you go back to resumes at once instead of loading again. The console is light and reads as one thing, and the Audio edition's page and browser extension are rebuilt: the page says who has the knobs and saves only what you changed, and the extension holds the knobs through silence. Audio is re-cut on this core as v0.7.0; Performance stays at v0.4.0, on core 3.10.4, until its maintainer re-cuts it.
+- **[v3.10.5](https://github.com/engmung/Patternflow/releases/tag/v3.10.5)**: every pattern 2.45 ms a frame faster, and a pattern's code moved to PSRAM, so patterns that were refused for size load and the console has more room while one is running. The panel raises its own Wi-Fi hotspot when it knows no network in reach, a panel that crashed says which pattern it was in and where, a pattern that hangs no longer takes the console down with it, and four requests that could reboot the board cannot any more.
+- **[v3.10.4](https://github.com/engmung/Patternflow/releases/tag/v3.10.4)**: the panel films properly on a phone. It refreshes at exactly 300 Hz, the one rate the shutter speeds a phone actually uses (1/50, 1/60, 1/100, 1/30) all divide, so the rolling bands are gone at those; 1/120 and slow motion still band, and [the firmware README](firmware/README.md#refresh-rate-anti-flicker-for-video) says why and what to set.
+- **[v3.10.3](https://github.com/engmung/Patternflow/releases/tag/v3.10.3)**: the panel's colours come out right. Bright edges had been growing purple and sky-blue fringes and greys read pink; the cause was in the display driver (which bit plane each brightness window actually lights), and part of it was 3.10.1's own dark-end fix. The planes are exactly binary now, and because the fix is in the driver every installed pattern gets it without a rebuild. Also a [Knobs page](docs/rest-api.md#get-apiknobs-and-post-apiknobs) on the device (which way each encoder counts, set from the browser) and the [features catalogue](https://patternflow.work/features). Both editions were re-cut on that core.
+- **[v3.10.2](https://github.com/engmung/Patternflow/releases/tag/v3.10.2)**: a Format that did not take stops reporting success, and a storage that will not mount says why.
+- **[v3.10.1](https://github.com/engmung/Patternflow/releases/tag/v3.10.1)**: fixed an upload that could reboot the board, a failed settings write that could make the next boot forget a good pattern, a mic-less Audio panel holding the knobs, and MIDI notes that would not let go.
+- **[v3.10.0](https://github.com/engmung/Patternflow/releases/tag/v3.10.0)**: a heavy pattern comes on every time.
+- **[v3.9.5](https://github.com/engmung/Patternflow/releases/tag/v3.9.5)**: the web console survives a slow Wi-Fi link and a heavy pattern, and thumbnails sit under the knob.
+- **[v3.9.0](https://github.com/engmung/Patternflow/releases/tag/v3.9.0)**: the panel is a MIDI port over Wi-Fi, in the Audio edition.
+- **[v3.8.0](https://github.com/engmung/Patternflow/releases/tag/v3.8.0)**: the firmware split into [editions](docs/EDITIONS.md), core, Audio (on-board microphone, OSC, browser and phone audio) and Performance (sequences, MQTT, weather), installable in one click and switchable without losing your patterns.
+- **[v3.2.0](https://github.com/engmung/Patternflow/releases/tag/v3.2.0)**: patterns install over Wi-Fi as `.pfm` modules, no reflash.
+
+
+
## Contributing
-Builds, documentation fixes, part sourcing tips, enclosure remixes, firmware features and host-software bridges are all welcome. **[CONTRIBUTING.md](CONTRIBUTING.md)** has one table of where each kind goes and what CI runs on it; **[SUPPORT.md](SUPPORT.md)** says where to ask. Patterns don't come here: publish them from the Pattern Lab to the Community.
+Builds, documentation fixes, part sourcing tips, enclosure remixes, firmware features and host-software bridges are all welcome. **[CONTRIBUTING.md](CONTRIBUTING.md)** has one table of where each kind goes and what CI runs on it. Questions and ideas go to the **[Discord](https://discord.gg/Vr9QtsxeTk)**, and **[SUPPORT.md](.github/SUPPORT.md)** says where to ask what. Patterns don't come here: publish them from the Pattern Lab to the Community. Galleries, installations and press: [patternflow.work/contact](https://patternflow.work/contact).
## Story so far
+
+From a visit to the Nam June Paik Art Center in January 2026 to two exhibitions in October, month by month
+
| When | Milestone |
| :--- | :--- |
-| **Jan 2026** | *Patternflow: Origin*, the first work as a new media artist, built around 3D-printed forms and the seed of what became Patternflow · visited the Nam June Paik Art Center |
+| **Jan 2026** | *Patternflow: Origin*, my first work as a new media artist, built around 3D-printed forms and the seed of what became Patternflow · visited the Nam June Paik Art Center |
| **Mar 2026** | The Origin pattern ran on a physical LED matrix with four knobs for the first time |
| **Apr 2026** | Instagram and the Arduino subreddit responded strongly (**150,000+ views and 3,700 upvotes**), and the community asked for the files, not a product, so Patternflow went open source · first PCB fabricated *(sponsored by PCBWay)* · website live |
-| **May 2026** | Reached **100 GitHub stars** · the first collaborator joined · Crowd Supply agreement · Discord community growing |
-| **Jun 2026** | [Crowd Supply pre-launch page](https://www.crowdsupply.com/engmung/patternflow) live, backed by countless refinements toward mass production · Instagram passed **1,000 followers** · first community-made pattern shared |
-| **Jul 2026** | Refining the design for mass production · growing an active community · outreach and promotion |
-| **Aug 2026** | [Crowd Supply campaign](https://www.crowdsupply.com/engmung/patternflow) officially launched worldwide · firmware split into [editions](docs/EDITIONS.md) (Audio & Performance) over an unchanged core · global press wave across Mixmag, Synthtopia, AMAZONA.de, and MATRIXSYNTH |
-| **Sep 2026** | Driving the Crowd Supply campaign to funding success · preparing mass production and fulfillment logistics · FreeRTOS dual-core network isolation, fast CIE frame push, and continuous ecosystem polish |
-| **Next** | Deliver rewards to backers at the highest quality · send Patternflow further out into the world · collaborate with more artists · earn academic recognition |
+| **May 2026** | Reached **100 GitHub stars** · the Live Editor and Pattern Lab went live · the first collaborator joined · Crowd Supply agreement |
+| **Jun 2026** | Nath in the UK finished the first Patternflow built outside the project, from the published files alone · [Crowd Supply pre-launch page](https://www.crowdsupply.com/engmung/patternflow) live · Instagram passed **1,000 followers** · first community-made pattern shared |
+| **Jul 2026** | The v3.0 board, every soldered part through-hole · patterns install over Wi-Fi as modules, Simone Majocchi's design · the Pattern Community opened · featured on Creative Applications Network |
+| **Aug 2026** | [Crowd Supply campaign](https://www.crowdsupply.com/engmung/patternflow) launched worldwide · firmware split into [editions](docs/EDITIONS.md) (Audio & Performance) over an unchanged core · press in Mixmag Asia, Synthtopia, AMAZONA.de and MATRIXSYNTH |
+| **Sep 2026** | The Crowd Supply campaign met its funding goal · the funded units will be printed and assembled by hand in Korea, on boards from PCBWay · the first Patternflow sold as an artwork · FreeRTOS dual-core network isolation and a faster frame push |
+| **Oct 2026** | The Crowd Supply campaign, open until 15 October, reached 200% of its goal · v3.11.0 and the Audio guide · *Anbu*, an exhibition by visually impaired artists at Modu Art Space near Seoul Station, with Patternflow versions of their works to play by hand · Patternflow in *USER EXPERIENCE*, a pop-up gallery in San Francisco |
+| **Next** | Deliver every Crowd Supply unit at the highest quality · send Patternflow further out into the world · collaborate with more artists · earn academic recognition |
| **2028** | Grow Patternflow into a self-sustaining community and ecosystem, then move on to the next project |
-Longer write-ups and the full story behind each step live on the **[journal](https://patternflow.work/journal)**: the whole process, written up at least weekly since the beginning, the thinking and the emotions included, and the parts that went badly too. If you want to know why this project exists and what it costs to keep alive, start there.
+
+
+Longer write-ups and the full story behind each step live on the **[journal](https://patternflow.work/journal)**: the whole process, written up week by week since April, the thinking and the emotions included, and the parts that went badly too. If you want to know why this project exists and what it costs to keep alive, start there.
### Sponsor
@@ -230,14 +303,14 @@ Patternflow's PCB fabrication and 3D-printed enclosure are sponsored by **[PCBWa
## License
-A file's SPDX header is the authority where one exists; folders are not license boundaries, and files that cannot carry a header (STL, Gerber, images) take the license the nearest README states. Full breakdown in the **[License Summary](docs/LICENSE-SUMMARY.md)**.
+Two licenses, in two separate files. A file's SPDX header is the authority where one exists; folders are not license boundaries, and files that cannot carry a header (STL, Gerber, images) take the license the nearest README states. Full breakdown in the **[License Summary](docs/LICENSE-SUMMARY.md)**.
-- Firmware & web code: **MIT** ([LICENSE-MIT](./LICENSE-MIT))
-- Hardware, designs & docs: **CC-BY-SA 4.0** ([LICENSE-CC-BY-SA](./LICENSE-CC-BY-SA))
-- Bundled patterns (the presets shipped in the firmware and the editor): **CC-BY-SA 4.0**, per-file SPDX headers.
-- Patterns contributed *to the repository*: inbound = outbound. By sending a pattern as a PR, issue or Discord post you license it under CC-BY-SA 4.0, attribution kept in the code header (no CLA). See [CONTRIBUTING.md](CONTRIBUTING.md).
-- Patterns published *to the Community*: **licensed by their author**, either **CC-BY-SA 4.0** (default) or **CC-BY-4.0**. Both permit commercial use with credit; CC-BY-SA additionally requires adaptations to stay under the same license. A fork can never be looser than what it came from, and carries a `Based on:` credit to the original author in its source.
+- Code (firmware, web, tools, integrations): **MIT** ([LICENSE-MIT](./LICENSE-MIT))
+- Hardware, designs & docs: **CC BY-SA 4.0** ([LICENSE-CC-BY-SA](./LICENSE-CC-BY-SA))
+- Bundled patterns (the presets shipped in the firmware and the editor): **CC BY-SA 4.0**, per-file SPDX headers.
+- Anything sent *to this repository*: inbound = outbound, MIT for code and CC BY-SA 4.0 for the rest, attribution kept in the file (no CLA). See [CONTRIBUTING.md](CONTRIBUTING.md).
+- Patterns published *to the Community*: **licensed by their author**, either **CC BY-SA 4.0** (default) or **CC BY 4.0**; a few older ones keep MIT or CC0, so read the pattern's own header. Both permit commercial use with credit; CC BY-SA additionally requires adaptations to stay under the same license. A fork can never be looser than what it came from, and carries a `Based on:` credit to the original author in its source.
"Patternflow" is a trademark of SeungHun Lee.
-The Patternflow series: LED Synthesizer (2026) · Origin (2026)
+*The Patternflow series, by SeungHun Lee: LED Synthesizer (2026) · Origin (2026)*
diff --git a/docs/.human-written b/docs/.human-written
new file mode 100644
index 000000000..1e088aa53
--- /dev/null
+++ b/docs/.human-written
@@ -0,0 +1 @@
+# Paths the maintainer locks beyond docs/rfc-cleanup.md §2.9, one per line from the repository root; a directory locks what is under it. web/scripts/human-lock.ts reads this file.
diff --git a/AUDIO_GUIDE.md b/docs/AUDIO_GUIDE.md
similarity index 88%
rename from AUDIO_GUIDE.md
rename to docs/AUDIO_GUIDE.md
index 6460a30e4..197c04ff1 100644
--- a/AUDIO_GUIDE.md
+++ b/docs/AUDIO_GUIDE.md
@@ -15,7 +15,7 @@ OSC ([here](#osc--ableton-today-anything-tomorrow)).
The same ground step by step, on a 3D Patternflow in the browser:
[patternflow.work/guide/audio](https://patternflow.work/guide/audio).
-Like the [Feature Guide](FEATURE_GUIDE.md), this page ends with a section
+Like the [Feature Guide](../FEATURE_GUIDE.md), this page ends with a section
written for AI coding agents; everything above it is for people.
---
@@ -25,15 +25,19 @@ written for AI coding agents; everything above it is for people.
Any of these puts the same firmware on your panel:
- **One click:** [patternflow.work/editions](https://patternflow.work/editions)
- → Audio → flash from the browser.
+ → Audio → **Install to my panel**. Your browser hands the image to a panel
+ that already runs Patternflow, over Wi-Fi; patterns, networks and settings
+ stay. A DevKit that has never run Patternflow takes the core first, from
+ **Flash Patternflow** ([BUILD_GUIDE §8](../BUILD_GUIDE.md#8-firmware)), and
+ then this.
- A release's `patternflow-audio.ino.bin` via the console's `/update` page.
- Build it yourself: `./firmware/bundles/build.sh audio` (add
`flash ` to send it over Wi-Fi).
The edition bundles four features: **OSC** (Max/TouchDesigner/Ableton, see
-[`docs/osc-spec.md`](docs/osc-spec.md)), **audio** (the streaming path the
+[`docs/osc-spec.md`](osc-spec.md)), **audio** (the streaming path the
extension uses), **audio_in** (the microphone and the mapping engine) and
-**MIDI** (the panel as a network MIDI port, [`docs/midi-spec.md`](docs/midi-spec.md)).
+**MIDI** (the panel as a network MIDI port, [`docs/midi-spec.md`](midi-spec.md)).
## The Chrome extension
@@ -43,11 +47,11 @@ locally, and streams four knob lanes to the panel over your LAN.
**Install:** it's not on the Web Store — load it straight from this repo.
`chrome://extensions` → enable *Developer mode* → *Load unpacked* → pick
-[`tools/patternflow-audio-extension/`](tools/patternflow-audio-extension/).
+[`tools/patternflow-audio-extension/`](../tools/patternflow-audio-extension/).
After pulling a newer copy, press *Reload* on its card there. Reload keeps the
mapping you already had, so the starting point described below is a new
install's. (Every control is described in its
-[README](tools/patternflow-audio-extension/README.md), and so is how to get
+[README](../tools/patternflow-audio-extension/README.md), and so is how to get
that starting point on an older mapping.)
**Use:** open a tab with sound → click the extension → enter your panel's
@@ -83,7 +87,7 @@ Boxes map to knobs 1:1 and that's fixed on purpose — K2 *is* knob 2.
## The on-board microphone
-
+
A small PDM microphone soldered to the DevKit lets the panel react to the
room itself — no browser, no phone, nothing else running. This is an
@@ -183,7 +187,7 @@ and input gain; it leaves the Microphone switch and Auto range as they are.
## The phone app (for filming)
-[`tools/patternflow-audio-android/`](tools/patternflow-audio-android/) is a
+[`tools/patternflow-audio-android/`](../tools/patternflow-audio-android/) is a
small Android app that captures whatever the phone is playing and drives the
panel with it. It exists for one job: **filming content** — play a track on
the phone (reels, whatever), and the panel reacts to the same audio the
@@ -191,7 +195,7 @@ video records. For everyday listening the extension or the mic is the better
path.
It's not on any store — build and install it yourself; the
-[README](tools/patternflow-audio-android/README.md) has the full recipe. It
+[README](../tools/patternflow-audio-android/README.md) has the full recipe. It
opens the panel's own editor page for configuration, so everything above
about boxes and curves applies unchanged.
@@ -202,13 +206,13 @@ protocol macOS calls *Network MIDI*). Once a session is up the panel is an
ordinary MIDI port in any DAW: four knobs in (CC 20–23 absolute, CC 24–27
relative), four buttons (notes 60–63), a pattern selector (Program Change),
and the same four knobs, buttons and pattern changes out as they happen.
-The contract is [`docs/midi-spec.md`](docs/midi-spec.md); the walk-through
+The contract is [`docs/midi-spec.md`](midi-spec.md); the walk-through
with screenshots — connecting on macOS, Windows (rtpMIDI) and Linux, mapping
in Ableton, the per-knob sensitivity slider, and telling the panel your
computer's address so it reconnects itself after a reboot — is
-[`docs/midi-ableton.md`](docs/midi-ableton.md).
+[`docs/midi-ableton.md`](midi-ableton.md).
-This is the missing half of [`docs/director-midi.md`](docs/director-midi.md):
+This is the missing half of [`docs/director-midi.md`](director-midi.md):
the Director's `.mid` export writes CC 20–23, so drop the clip on a MIDI
track, set the track's output to the panel's port, and the show plays on the
panel from Live's transport. The switch on the console's **MIDI** page turns it
@@ -219,11 +223,11 @@ sensitivity and message counts under `midi`.
The edition also speaks **OSC** — plain OSC 1.0 over UDP, both directions.
The ready-made client is the **Max for Live bridge** in
-[`integrations/ableton/`](integrations/ableton/): load it in Ableton and
+[`integrations/ableton/`](../integrations/ableton/): load it in Ableton and
your set and the panel talk to each other.
But the bridge is just one client of a written contract,
-[`docs/osc-spec.md`](docs/osc-spec.md) — anything that can send a UDP OSC
+[`docs/osc-spec.md`](osc-spec.md) — anything that can send a UDP OSC
message can drive the panel the same way. TouchDesigner, VCV Rack and
Processing speak it natively; Blender does through an OSC add-on or a few
lines of Python. Send `/patternflow/ping` once and the device learns your
@@ -260,12 +264,12 @@ page says which source has the knobs right now.
## For the AI agent
-The hard rules — the same ones humans follow — are in [`AGENTS.md`](AGENTS.md#hard-rules-do-not-violate); the contributor flow is [`CONTRIBUTING.md`](CONTRIBUTING.md).
+The hard rules — the same ones humans follow — are in [`AGENTS.md`](../AGENTS.md#hard-rules-do-not-violate); the contributor flow is [`CONTRIBUTING.md`](../CONTRIBUTING.md).
You were pointed here to work on Patternflow's audio path. The map:
**Firmware — the mapping engine and mic**
-([`firmware/patternflow/features/audio_in/`](firmware/patternflow/features/audio_in/)):
+([`firmware/patternflow/features/audio_in/`](../firmware/patternflow/features/audio_in/)):
| file | role |
| --- | --- |
@@ -273,19 +277,19 @@ You were pointed here to work on Patternflow's audio path. The map:
| `core_audio_pdm.h` | PDM mic driver (GPIO 43/44). Its header comment is the authoritative wiring + driver-choice record. |
| `core_audio_fft.h` | FFT and spectrum buckets. |
| `core_audio_in_map.h` | Bands, curve LUTs (33-point, interpolate-only), attack/damping glide, auto-range, the defaults a reset restores, NVS persistence. Manual mode maps linearly in amplitude between a band's two edges; the page's dB axis is a conversion at the page's edge. |
-| `core_audio_in_http.h` | `/audio-in` (editor page), `GET /api/audio-in` (the configuration, read once), `POST /api/audio-in` (any subset: a request changes only the fields it carries), `POST /api/audio-in/reset` (everything, or one band with `band=N`), the `?levels=1` live poll, and the frame endpoint the phone posts spectra to. The contract is [`docs/rest-api.md`](docs/rest-api.md#microphone-audio-edition); the phone app is a second client of it, so fields are added, never renamed. |
+| `core_audio_in_http.h` | `/audio-in` (editor page), `GET /api/audio-in` (the configuration, read once), `POST /api/audio-in` (any subset: a request changes only the fields it carries), `POST /api/audio-in/reset` (everything, or one band with `band=N`), the `?levels=1` live poll, and the frame endpoint the phone posts spectra to. The contract is [`docs/rest-api.md`](rest-api.md#microphone-audio-edition); the phone app is a second client of it, so fields are added, never renamed. |
| `audio_in_index.h` | **Generated** — never edit (chain below). |
-**Firmware — MIDI** ([`firmware/patternflow/features/midi/`](firmware/patternflow/features/midi/)):
+**Firmware — MIDI** ([`firmware/patternflow/features/midi/`](../firmware/patternflow/features/midi/)):
`core_midi.h` is the mapping (transport-agnostic: handlers in, a sink out),
`core_midi_rtp.h` the RTP-MIDI listener on UDP 5004/5005 over lathoub's
-AppleMIDI library. Contract: [`docs/midi-spec.md`](docs/midi-spec.md).
+AppleMIDI library. Contract: [`docs/midi-spec.md`](midi-spec.md).
`tools/rtpmidi-probe/` exercises the whole map from a PC with no MIDI driver.
**Firmware — the stream path**
-([`firmware/patternflow/features/audio/`](firmware/patternflow/features/audio/)):
+([`firmware/patternflow/features/audio/`](../firmware/patternflow/features/audio/)):
`core_audio_ws.h` is a WebSocket server on port 81. The wire contract is
-[`docs/audio-ws-spec.md`](docs/audio-ws-spec.md) — clients build against the
+[`docs/audio-ws-spec.md`](audio-ws-spec.md) — clients build against the
spec, not the source. Probe `GET /api/status` for `"audio"` in `caps`. A lane
is released 500 ms after the last message that set it, so a sender that means
to hold a value resends it; the spec says which senders in this tree do.
@@ -315,7 +319,7 @@ Preview without building: `python firmware/toolchain/console_serve.py`
assembles `/audio-in` from those sources on every request and answers its API
the way the firmware does; `/mock?audio=music`, `silence`, `off`, `fresh`,
`nomic`, `stalled`, `phone`, `ext` and `noread` pick what the panel hears
-([`console/README.md`](firmware/patternflow/console/README.md#the-page-that-is-assembled-audio-in)).
+([`console/README.md`](../firmware/patternflow/console/README.md#the-page-that-is-assembled-audio-in)).
The page has a budget, `BUDGET` in `build_audio_in_page.py`: 17,500 bytes
gzipped as the panel sends it. It is the heaviest page in the console and
every byte crosses the panel's Wi-Fi, so whole-line comments and indentation
@@ -331,7 +335,7 @@ every request rides `PF.poll`/`PF.status`, nothing is saved until the
panel's mapping has been read, and a save sends only the fields that changed.
**Chrome extension**
-([`tools/patternflow-audio-extension/`](tools/patternflow-audio-extension/)):
+([`tools/patternflow-audio-extension/`](../tools/patternflow-audio-extension/)):
`mapping.js` is the mapping model, one object (`PFMap`): defaults, the
curves, level → knob. `editor.js` is the single shared editor module, and
`editor-adapter.js` is its extension adapter (chrome storage, the capture's
@@ -346,7 +350,7 @@ the panel. The extension keeps its cream skin; only the panel's page wears
the console's.
**Android app**
-([`tools/patternflow-audio-android/`](tools/patternflow-audio-android/)):
+([`tools/patternflow-audio-android/`](../tools/patternflow-audio-android/)):
`Analyzer.kt` mirrors the extension's analysis and mapping (`offscreen.js`
and `mapping.js`); `DeviceLink.kt` speaks the WS
contract + syncs panel config over `/api/audio-in` + posts monitor frames;
@@ -360,9 +364,9 @@ its WebView is set up without the part that shows a `window.confirm` dialog,
which is why the page's Reset mapping is a button pressed twice. Open the
app's editor once before releasing a change to `/audio-in`.
-Rules of the road: firmware changes follow [FEATURE_GUIDE.md](FEATURE_GUIDE.md)
+Rules of the road: firmware changes follow [FEATURE_GUIDE.md](../FEATURE_GUIDE.md)
(hooks, checkers, `build.sh all`); protocol changes update the spec in the
-same PR; API changes extend [`docs/rest-api.md`](docs/rest-api.md) and never
+same PR; API changes extend [`docs/rest-api.md`](rest-api.md) and never
rename what is there; editor changes run `console_pages.py build` so the
panel's page follows — the editor's body is one file for both surfaces, which
is the point.
diff --git a/docs/EDITIONS.md b/docs/EDITIONS.md
index fab3a1838..7e7b69d08 100644
--- a/docs/EDITIONS.md
+++ b/docs/EDITIONS.md
@@ -14,9 +14,10 @@ four knobs.** That sentence decides everything below it, and it is the only
test worth applying.
What that sentence names is not a feature and never composes out: the pattern
-loader, the four encoders, the panel driver, Wi-Fi, `/update`, sleep, the
-console. Those are the device. **The default build carries no features at
-all**, and a panel running it does the whole of what Patternflow is for.
+loader, the four encoders, the panel driver, Wi-Fi (its own hotspot
+included), `/update`, sleep, the console. Those are the device. **The default
+build carries no features at all**, and a panel running it does the whole of
+what Patternflow is for.
Everything else — sequences, MQTT, OSC, weather, audio — is a way of driving
that device from somewhere else. Real, wanted, first-class, and not the thing
@@ -217,7 +218,7 @@ deliberately re-pinned in the same commit.
feature code that recompiles against a checkout, in-tree or out. Widening
is tail-append only (the rule and its reason are written on the struct);
the compiler surfaces every break at build, and `build.sh all` makes sure
-"builds" means all three compositions. The two composition filenames and
+"builds" means all five compositions. The two composition filenames and
their macros are the out-of-tree seam - renamed spellings are shimmed, and
shims get deleted only after their consumers migrate.
@@ -290,22 +291,30 @@ Two files under `firmware/bundles//`:
#include "osc/feature_osc.h"
#include "audio/feature_audio.h"
#include "audio_in/feature_audio_in.h"
+#include "midi/feature_midi.h"
#define PF_FEATURE_LIST \
&PFFeatureOsc::descriptor, \
&PFFeatureAudio::descriptor, \
- &PFFeatureAudioIn::descriptor
+ &PFFeatureAudioIn::descriptor, \
+ &PFFeatureMidi::descriptor
```
```c
// overrides.h — what it calls itself, and anything it needs set differently
#define PF_VARIANT "audio"
-#define PF_VARIANT_VERSION "v0.2.0" // yours, moves at your pace
-#define PF_WIFI_TX_POWER WIFI_POWER_17dBm
+#define PF_VARIANT_VERSION "v0.7.0" // yours, moves at your pace
+#define PF_AUDIO_IN_DRIVES_KNOBS 1 // the microphone drives the knobs here
```
+That is the Audio edition's pair with the comments taken out (v0.7.0 when
+this was written). It pinned `PF_WIFI_TX_POWER` too until the core went to
+full power, and dropped it in v0.7.0 — which is why every setting in an
+`overrides.h` carries its reason: one copied without it stays after the reason
+has gone.
+
A third file, `env`, is optional: one line naming the PlatformIO env to build
in, for the rare edition whose difference is a build flag rather than a
-setting. The MIDI edition is the one so far — its USB port has to be the OTG
+setting. The MIDI bundle is the one so far — its USB port has to be the OTG
controller for a MIDI device to exist, and the Arduino core decides that at
compile time (`ARDUINO_USB_MODE`). Everything else about it is the two files
above; `build.sh` and `shelf.sh` read the env name and look for the outputs
@@ -463,6 +472,9 @@ because no phone showed it in its pick list; and the Wi-Fi setup portal is a
parked branch, `feat/wifi-setup-portal`, in no edition and not to be merged as
it stands — it predates the seam and edits the core. What that attempt learned
is in [`investigations/2026-08-the-wifi-portal-a-samsung-never-drew.md`](investigations/2026-08-the-wifi-portal-a-samsung-never-drew.md).
+The need it was for — setting Wi-Fi up from a phone — is met in the core
+since 3.10.5 by the panel's own hotspot (`src/core_hotspot.h`): the
+device's Wi-Fi, so core rather than a feature.
---
diff --git a/docs/README.md b/docs/README.md
index 096215e3e..007b2f060 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,15 +1,16 @@
# docs/
-What is here, and what is not. The rule for the split: **a guide somebody follows start to finish lives at the repository root** (`BUILD_GUIDE.md`, `BUILD_GUIDE_v2.md`, `PATTERN_GUIDE.md`, `FEATURE_GUIDE.md`, `AUDIO_GUIDE.md`); **a contract, a reference, a walk-through for one task, or a record lives here**; and a folder explains itself in its own README. The folder-by-folder map of the whole repository is [`REPOSITORY.md`](REPOSITORY.md).
+What is here, and what is not. The rule for the split: **the repository root keeps three guides and no others** (`BUILD_GUIDE.md`, `PATTERN_GUIDE.md`, `FEATURE_GUIDE.md`), so a first-time visitor reads it at a glance; **a contract, a reference, a walk-through for one task (the audio guide is one), or a record lives here**; and a folder explains itself in its own README. The folder-by-folder map of the whole repository is [`REPOSITORY.md`](REPOSITORY.md).
## Contracts — other software is built against these, not against the firmware source
-- [`rest-api.md`](rest-api.md) — the device's HTTP API (`/api/*`), the console pages, and the table for choosing between HTTP, OSC, MIDI and MQTT
-- [`osc-spec.md`](osc-spec.md) — OSC over UDP (DAWs, Max, TouchDesigner)
-- [`midi-spec.md`](midi-spec.md) — the panel as a network MIDI port
-- [`mqtt-spec.md`](mqtt-spec.md) — MQTT topics, both directions, the roles, and why the channel decides whether a write sticks
-- [`audio-ws-spec.md`](audio-ws-spec.md) — the audio-react WebSocket the browser extension and the phone app speak
+- [`rest-api.md`](rest-api.md) — the device's HTTP API (`/api/*`), the console pages, and the table for choosing between HTTP, OSC, MIDI and MQTT, with test vectors in [`rest-api-vectors/`](rest-api-vectors/)
+- [`osc-spec.md`](osc-spec.md) — OSC over UDP (DAWs, Max, TouchDesigner), with test vectors in [`osc-vectors/`](osc-vectors/)
+- [`midi-spec.md`](midi-spec.md) — the panel as a network MIDI port, with test vectors in [`midi-vectors/`](midi-vectors/) and those for `/api/midi` in `rest-api-vectors/`
+- [`mqtt-spec.md`](mqtt-spec.md) — MQTT topics, both directions, the roles, and why the channel decides whether a write sticks, with test vectors in [`mqtt-vectors/`](mqtt-vectors/) and those for `/api/mqtt` in `rest-api-vectors/`
+- [`audio-ws-spec.md`](audio-ws-spec.md) — the audio-react WebSocket the browser extension and the phone app speak, with test vectors in [`audio-ws-vectors/`](audio-ws-vectors/) and those for `/api/audio` in `rest-api-vectors/`
- [`pfst-v2-spec.md`](pfst-v2-spec.md) — the `.pfs` show table, with test vectors in [`pfst-v2-vectors/`](pfst-v2-vectors/)
+- [`contract-vectors.md`](contract-vectors.md) — how a vector of the first five is written, and how `npm run check:rest-vectors` and the four like it replay them against the [virtual panel](../tools/virtual-panel/README.md), a Patternflow with no board
- [`panel-compatibility.md`](panel-compatibility.md) — a buying guide first (which HUB75 panels light up and which stay dark), then the reference for other sizes
## How the firmware is put together
@@ -19,32 +20,45 @@ What is here, and what is not. The rule for the split: **a guide somebody follow
## Walk-throughs — one task, start to finish
+- [`AUDIO_GUIDE.md`](AUDIO_GUIDE.md) — sound into the panel
- [`midi-ableton.md`](midi-ableton.md) — the panel as a MIDI port in Ableton Live, with screenshots
- [`director-midi.md`](director-midi.md) — how a Director show becomes a MIDI clip
-- [`assembly/`](assembly/README.md) — the assembly map: which enclosure with which electronics, and where each route is documented
- [`../firmware/CUSTOM_PATTERNS.md`](../firmware/CUSTOM_PATTERNS.md) — the hands-on pattern route (the everyday one is [`PATTERN_GUIDE.md`](../PATTERN_GUIDE.md))
- [`../firmware/patternflow/console/README.md`](../firmware/patternflow/console/README.md) — editing or adding a device console page
## Building it
-- [`assembly/`](assembly/README.md) — the map. The full guide is [`../BUILD_GUIDE.md`](../BUILD_GUIDE.md); the files to order are described in [`../hardware/README.md`](../hardware/README.md)
+- [`assembly/`](assembly/README.md) — the assembly map: which enclosure with which electronics, and where each route is documented. The full guide is [`../BUILD_GUIDE.md`](../BUILD_GUIDE.md); the files to order are described in [`../hardware/README.md`](../hardware/README.md); enclosures other people built are [`../hardware/case/remixes/`](../hardware/case/remixes/README.md)
+- The same build in 3D, step by step, is [patternflow.work/guide/build](https://patternflow.work/guide/build). Its firmware chapter hands over to the Play guide's [Flash chapter](https://patternflow.work/guide/play#flash), which is also where to look when the first flash or the Wi-Fi step goes wrong (the written version is [`BUILD_GUIDE.md` §8](../BUILD_GUIDE.md#8-firmware))
+- [`legacy/BUILD_GUIDE_v2.md`](legacy/BUILD_GUIDE_v2.md) — the build guide for the legacy v2.x board
- [`build-guide/images/`](build-guide/images/) — photos for the build guides (`images/` is the v2 set, `images/v3/` the current board)
- [`LICENSE-SUMMARY.md`](LICENSE-SUMMARY.md) — MIT for code, CC BY-SA 4.0 for hardware, docs and bundled patterns, and what that means for a pattern you publish or a remix you share
+## On the site, not in this folder
+
+The interactive guide at [patternflow.work/guide](https://patternflow.work/guide) has four parts, each in English and Korean: [Build](https://patternflow.work/guide/build) (soldering one from bare parts), [Play](https://patternflow.work/guide/play) (the first hour with a built one: flash, knobs, patterns, console), [Make](https://patternflow.work/guide/make) (community and Pattern Lab) and [Audio](https://patternflow.work/guide/audio) (the Audio edition: a browser tab, the microphone, MIDI with a DAW). They cover the same ground as the written guides, and every step has a **Stuck here?** link that opens an issue with that step filled in.
+
## Contributing
- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — one table: what you have → where it goes → what CI runs on it
-- [`../SUPPORT.md`](../SUPPORT.md) — where to ask what
+- [`../.github/SUPPORT.md`](../.github/SUPPORT.md) — where to ask what
- [`REPOSITORY.md`](REPOSITORY.md) — the folder-by-folder map, and the list of files that outside things link to
+- [`voice.md`](voice.md) — how the docs sound in English and in Korean, with the words that stay out in [`voice-banned.txt`](voice-banned.txt)
## Running the project
- [`RELEASING.md`](RELEASING.md) — the `dev` → `main` routine, cutting a release, what the workflows attach
- [`SERVICES.md`](SERVICES.md) — the production hosts (community, build worker) and their systemd units. Korean.
+## Drafts — Part 2 only, and nothing in them is built
+
+- [`rfc-any-resolution.md`](rfc-any-resolution.md) — a panel of another size on one ESP32-S3, chosen on a console page and applied at the next boot instead of in a rebuild. Classic HUB75 only, within the driver's DMA memory
+- [`rfc-linux-player.md`](rfc-linux-player.md) — the site's pattern engine as a Linux program, onto a HUB75 HAT on a Raspberry Pi, a receiving card or a screen. Not firmware, and not an edition
+
## Records
-- [`investigations/`](investigations/) — dated post-mortems
-- [`releases/`](releases/) — long-form notes for three July 2026 releases, kept as written; notes since 3.2 are on [GitHub Releases](https://github.com/engmung/Patternflow/releases), and `CHANGELOG.md` at the root is the complete record
+- [`investigations/`](investigations/) — dated post-mortems, and the cleanup's plans. `npm run doc-map` in `web/` writes the two lists here: `2026-10-doc-map.csv`, every link in the tree and whether it resolves, and `2026-10-doc-parity.csv`, every Korean/English pair and which side is missing or short
+- [`rfc-cleanup.md`](rfc-cleanup.md) and its [progress log](rfc-cleanup-progress.md) — the October 2026 tidy-up before the any-resolution work, done on 16 October. It changed where things live and what CI checks, and nothing the firmware or the site does. §2.9 lists the writing it locked
+- [`releases/`](releases/) — every release's changelog section as written, one file each (`vX.Y.Z.md`), back to v1.0.0; `CHANGELOG.md` at the root keeps the three newest in full and links the rest. `v3.0.0-notes.md` and `v3.1.0-notes.md` are long-form notes for two July 2026 releases, beside the v3.0.0 launch checklist; notes since 3.2 are on [GitHub Releases](https://github.com/engmung/Patternflow/releases)
- [`manifesto.md`](manifesto.md) — why Patternflow exists, and (section 6) how we treat each other
- [`images/`](images/), [`media/`](media/) — assets the guides and the site reference
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index fb438b7ef..cd7bcde55 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -19,7 +19,8 @@ the `[Unreleased]` section of `CHANGELOG.md` written:
python firmware/toolchain/release.py cut v3.9.4 --audio v0.5.4 --performance v0.2.4
```
-bumps the versions, dates the changelog section, updates `AGENTS.md`, runs
+bumps the versions, dates the changelog section and files it in
+`docs/releases/`, updates `AGENTS.md`, runs
`shelf.sh` for the core and each named edition, points the flasher manifest
and the `/editions` cards at the new images, runs the web checks, commits
`release: v3.9.4` and tags it. Name only the editions that should be re-cut;
@@ -63,7 +64,7 @@ What the two commands do, step by step - and the way to do it by hand.
1. Make sure all intended changes are committed, and `./firmware/bundles/build.sh all` is green if anything under `firmware/` moved.
2. Bump the version the firmware reports: `PF_IMPROV_FW_VERSION` in `firmware/patternflow/net_config.h` (written `X.Y.Z`, no `v`). `shelf.sh` refuses a core image whose define disagrees with the version it is being shelved as.
-3. Turn `CHANGELOG.md`'s `[Unreleased]` into `## [X.Y.Z] - YYYY-MM-DD` and open a fresh `[Unreleased]` above it.
+3. Turn `CHANGELOG.md`'s `[Unreleased]` into `## [X.Y.Z] - YYYY-MM-DD` and open a fresh `[Unreleased]` above it. Copy the new section to `docs/releases/vX.Y.Z.md` (the file name always carries the `v`), with each relative link two folders up (`../../`). The root keeps three releases in full: the fourth-newest section leaves it and becomes a line at the top of *Older releases*, linking its file.
4. Update the version the docs claim: the "current" line in `AGENTS.md`, the *Moving fast* note in `README.md`, and any guide that names a release.
5. If hardware changed: confirm `BUILD_GUIDE.md`'s parts table, `hardware/bom/bom_v*.csv` and the schematic agree; regenerate the Gerber zip, renders and schematic exports with the recipe in `hardware/pcb/README.md`; add a `### Hardware` entry to the changelog section; update the board table in `hardware/README.md` and the version line in `docs/assembly/README.md`; attach the Gerber zip, the BOM CSV and the STLs to the release.
6. Stage the images the site serves:
@@ -102,9 +103,9 @@ Work happens on `dev`, then lands on `main` through a pull request. `main` is pr
## Current release line
-`CHANGELOG.md` is the record — one section per release, newest first — and the [releases page](https://github.com/engmung/Patternflow/releases) carries the notes and the flashable images. The shape of the line, for orientation:
+Every release has its own file in `docs/releases/`, `vX.Y.Z.md`, back to `v1.0.0`. `CHANGELOG.md` holds the three newest in full, newest first, and a line for each older one. The [releases page](https://github.com/engmung/Patternflow/releases) carries the notes and the flashable images. The shape of the line, for orientation:
- `v1.x` -- first public buildable release, then the multi-pattern firmware and browser flasher.
- `v2.x` -- the v2.0 board (GPIO0 cold-boot fix, cleaned silkscreen), custom pattern workflow, the web platform. `v2.1.0` is the last release for v2.x hardware.
-- `v3.0.0` -- the v3.0 board; `v3.9` (2026-09, unreleased as a tag) removed its USB-C footprint and changed nothing else. Every later `v3.x` is firmware/web on that hardware: `.pfm` modules over Wi-Fi (3.2), shows and the Director (3.6), the feature seam (3.7), editions (3.8).
+- `v3.0.0` -- the v3.0 board; `v3.9` (2026-09, unreleased as a tag) removed its USB-C footprint and changed nothing else. Every later `v3.x` is firmware/web on that hardware: `.pfm` modules over Wi-Fi (3.2), shows and the Director (3.6), the feature seam (3.7), editions (3.8), the panel's own hotspot and pattern code in PSRAM (3.10.5), patterns that stay loaded when you leave them (3.11).
- Editions on the shelf (`audio`, `performance`) carry their own version lines, independent of the project version — see `docs/EDITIONS.md`. `clock` and `midi` are bundles CI builds but the shelf does not carry.
diff --git a/docs/REPOSITORY.md b/docs/REPOSITORY.md
index e22c6b03d..adbf7f49b 100644
--- a/docs/REPOSITORY.md
+++ b/docs/REPOSITORY.md
@@ -6,27 +6,28 @@ A map for people. (AI coding agents get the same map with more rules in [`../AGE
| Folder | What it is | Start at |
| :--- | :--- | :--- |
-| `firmware/` | The ESP32-S3 firmware. `patternflow/` is the sketch: `patternflow.ino`, `config.h` (pins, brightness, limits, calibration), `net_config.h` (Wi-Fi, OTA, update defaults), `src/` (the engine: HUB75 driver, encoders, canvas, colour, noise, Wi-Fi, web console — the **core**, compiled unchanged into every published firmware), `features/` (everything optional: OSC, audio, MQTT, MIDI, shows, weather, clock — attached through hooks the core exposes), `presets/` (the curated patterns), `abi/` (the frozen contract a `.pfm` module is compiled against), `console/` (the device's web pages as plain HTML). `bundles/` names the editions (which features compile in) and holds `build.sh`. `toolchain/` is the repo-level scripts: build a module, make the Basics pack, and the `check_*.py` that CI runs. `modules/` is the module toolchain's working folder, not a place patterns are contributed to | [`firmware/README.md`](../firmware/README.md) for building; [`EDITIONS.md`](EDITIONS.md) before touching anything |
-| `hardware/` | `pcb/` (KiCad source, Gerbers per revision, schematic PDF), `case/` (STLs by printer bed size, knob plates, the Blender source, legacy v2 cases, and `remixes/` for community variants), `bom/` (the CSV that is the BOM source of truth) | [`hardware/README.md`](../hardware/README.md) |
-| `web/` | The Next.js site at patternflow.work: landing page, the interactive guide (`/guide`, with `/guide/build`, `/guide/play`, `/guide/make` and `/guide/audio`), Live Editor, Pattern Lab, the community (only on the community host), browser flasher, edition shelf, device update handoff, journal, roadmap. The JS presets in `src/lib/presets/` are the source of truth for the firmware's preset headers. The breadboard build guide is a React page here, `src/app/build/breadboard/` | [`web/README.md`](../web/README.md), then [`web/ARCHITECTURE.md`](../web/ARCHITECTURE.md) |
-| `docs/` | Contracts other software is built against, how the firmware is put together, walk-throughs, the assembly map, project records and media | [`README.md`](README.md) |
-| `tools/` | Clients Patternflow ships that run on their own: the audio-react browser extension (also the authoring source of the device's `/audio-in` page), the Android capture app, an RTP-MIDI probe. Also `case-models/`, the scripts that make the /build page's 3D model of each case | [`tools/README.md`](../tools/README.md) |
+| `firmware/` | The ESP32-S3 firmware. `patternflow/` is the sketch: `patternflow.ino`, `config.h` (pins, brightness, limits, calibration), `net_config.h` (Wi-Fi, OTA, update defaults), `src/` (the engine: HUB75 driver, encoders, canvas, colour, noise, Wi-Fi, web console — the **core**, compiled unchanged into every published firmware), `features/` (everything optional: OSC, audio, the microphone, MQTT, MIDI, shows, weather, clock, BLE — attached through hooks the core exposes), `presets/` (the curated patterns), `abi/` (the frozen contract a `.pfm` module is compiled against), `console/` (the device's web pages as plain HTML). `bundles/` names the compositions — the shelf editions `audio` and `performance`, plus `clock` and `midi`, which CI keeps compiling but the shelf does not carry — and holds `build.sh` and `shelf.sh`. `toolchain/` is the repo-level scripts: build a module, make the Basics pack, and the `check_*.py` that CI runs. `modules/` is the module toolchain's working folder, not a place patterns are contributed to. `encoder_test/` is a standalone sketch for checking the four encoders, and `CUSTOM_PATTERNS.md` the hands-on pattern route | [`firmware/README.md`](../firmware/README.md) for building; [`EDITIONS.md`](EDITIONS.md) before touching anything |
+| `hardware/` | `pcb/` (KiCad source, Gerbers per revision, schematic PDF), `case/` (STLs by printer bed size, knob plates, the Blender source, legacy v2 cases, the old v1/v2 laser-cut drawings in `legacy_lasercut/`, and `remixes/` — enclosures other people designed and built, one folder each), `bom/` (the CSV that is the BOM source of truth) | [`hardware/README.md`](../hardware/README.md) |
+| `web/` | The Next.js site at patternflow.work: landing page, the interactive guide (`/guide`, with `/guide/build`, `/guide/play`, `/guide/make` and `/guide/audio`), Live Editor, Pattern Lab, the community (only on the community host), the browser flasher (the **Flash Patternflow** button on the Pattern tab and in the Play guide; the images it installs are `public/flash/`), edition shelf, feature catalogue, device update handoff, journal, roadmap. The JS presets in `src/lib/presets/` are the source of truth for the firmware's preset headers. The breadboard build guide is a React page here, `src/app/build/breadboard/` | [`web/README.md`](../web/README.md), then [`web/ARCHITECTURE.md`](../web/ARCHITECTURE.md) |
+| `shared/` | `constants.json`, the values the firmware and the site both carry: the panel geometries, the knob count, the encoder's 24 detents a turn, the bus maximum and the button timings. `npm run gen:shared` in `web/` writes `firmware/patternflow/src/core_shared.h` and `web/src/lib/shared/constants.ts` from it. Neither is edited by hand, and `check:shared` fails while either is stale. The folder exists because the lab counted 20 detents a turn, against the encoder's 24, until 31 July 2026 | [`constants.json`](../shared/constants.json) |
+| `docs/` | Contracts other software is built against, each with a folder of test vectors beside it, how the firmware is put together, walk-throughs, the audio guide (`AUDIO_GUIDE.md`), the assembly map, the legacy v2.x build guide (`legacy/`), the writing voice (`voice.md` and `voice-banned.txt`), project records (`releases/`, one file per release) and media | [`README.md`](README.md) |
+| `tools/` | Clients Patternflow ships that run on their own: the audio-react browser extension (also the authoring source of the device's `/audio-in` page), the Android capture app, an RTP-MIDI probe. Also `case-models/`, the scripts that make the /build page's 3D model of each case, the official one and every remix on its case switch. `virtual-panel/` is the other end of the contracts: a Patternflow with no board, on the firmware's ports, that the vectors in `docs/` are replayed against | [`tools/README.md`](../tools/README.md) |
| `integrations/` | Bridges that run inside or beside someone else's software (Ableton / Max for Live today) and talk to the panel only through the contracts in `docs/` | [`integrations/README.md`](../integrations/README.md) |
-| `.github/` | Issue and pull-request templates, the CI workflows (web, firmware editions, doc links, console-page sync, firmware release assets, Discord dev-log) and their helper scripts | the header comment of each workflow |
-| root | `README.md`, the five guides (`BUILD_GUIDE.md`, `BUILD_GUIDE_v2.md`, `PATTERN_GUIDE.md`, `FEATURE_GUIDE.md`, `AUDIO_GUIDE.md`), `CHANGELOG.md`, `CONTRIBUTING.md`, `SUPPORT.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, `CITATION.cff`, the two license texts and the `LICENSE` pointer, `AGENTS.md` (+ `CLAUDE.md`, a one-line shim) | |
+| `.github/` | Issue and pull-request templates (including the guide's **Stuck here?** form and **Share your build**), `SUPPORT.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, the CI workflows (web, every firmware composition, the core-names-no-feature checks, doc links, the drift checks, community folders carrying their README, console-page sync, firmware release assets, Discord dev-log) and their helper scripts | the header comment of each workflow |
+| root | `README.md`, the three guides (`BUILD_GUIDE.md`, `PATTERN_GUIDE.md`, `FEATURE_GUIDE.md`), `CHANGELOG.md`, `CONTRIBUTING.md`, `CITATION.cff`, the two license texts and the `LICENSE` pointer, `AGENTS.md` (+ `CLAUDE.md`, a one-line shim) | |
-The rule for where a document lives: **a guide somebody follows start to finish is at the root; a contract, a reference or a record is in `docs/`; a folder explains itself in its own README.**
+The rule for where a document lives: **the root keeps three guides and no others, so a first-time visitor reads it at a glance; a contract, a reference, a walk-through for one task or a record is in `docs/`; a folder explains itself in its own README.**
## What you want to do → where to start
| Goal | Path |
| :--- | :--- |
| Build one | [`README.md`](../README.md) → [`BUILD_GUIDE.md`](../BUILD_GUIDE.md) (or the [assembly map](assembly/README.md) for the other routes) → [`hardware/README.md`](../hardware/README.md) for which files to order. The same build in 3D: [patternflow.work/guide/build](https://patternflow.work/guide/build) |
-| Use the one you built | [patternflow.work/guide/play](https://patternflow.work/guide/play) — flashing, the knobs, installing patterns and the console, on a Patternflow you can turn |
+| Use the one you built | [patternflow.work/guide/play](https://patternflow.work/guide/play) — flashing, the knobs, installing patterns and the console, on a Patternflow you can turn. The first flash goes through download mode (hold BOOT, tap RST, let go of BOOT), and the Wi-Fi step usually does not appear the first time: the [Flash chapter](https://patternflow.work/guide/play#flash) and [`BUILD_GUIDE.md` §8](../BUILD_GUIDE.md#8-firmware) say what to do, in order |
| Make a pattern | [`PATTERN_GUIDE.md`](../PATTERN_GUIDE.md) — the Pattern Lab, the community, and the device. No local toolchain involved. The same loop as a tutorial: [patternflow.work/guide/make](https://patternflow.work/guide/make). [`firmware/CUSTOM_PATTERNS.md`](../firmware/CUSTOM_PATTERNS.md) is the older, more hands-on route |
| Write a firmware feature or cut an edition | [`FEATURE_GUIDE.md`](../FEATURE_GUIDE.md) → [`EDITIONS.md`](EDITIONS.md) → [`features/README.md`](../firmware/patternflow/features/README.md) → [`bundles/README.md`](../firmware/bundles/README.md) |
-| Control the panel from other software | [`rest-api.md`](rest-api.md) has the table for choosing between HTTP, OSC, MIDI and MQTT; then the spec for the one you chose |
-| Put sound into it | [`AUDIO_GUIDE.md`](../AUDIO_GUIDE.md). The browser tab, the microphone and MIDI with a DAW, step by step: [patternflow.work/guide/audio](https://patternflow.work/guide/audio) |
+| Control the panel from other software | [`rest-api.md`](rest-api.md) has the table for choosing between HTTP, OSC, MIDI and MQTT; then the spec for the one you chose. [`tools/virtual-panel/`](../tools/virtual-panel/README.md) answers on the same ports with no board behind it |
+| Put sound into it | [`AUDIO_GUIDE.md`](AUDIO_GUIDE.md). The browser tab, the microphone and MIDI with a DAW, step by step: [patternflow.work/guide/audio](https://patternflow.work/guide/audio) |
| Work on the site | [`web/README.md`](../web/README.md) → [`web/ARCHITECTURE.md`](../web/ARCHITECTURE.md) |
| Contribute anything | [`CONTRIBUTING.md`](../CONTRIBUTING.md) — one table of where each kind of contribution goes |
| Cut a release, run the servers | [`RELEASING.md`](RELEASING.md), [`SERVICES.md`](SERVICES.md) |
@@ -41,11 +42,15 @@ Filenames are lowercase with underscores. Tags are `vX.Y.Z`; the firmware source
## Files that other things link to — check before renaming
-The site, the firmware and the release notes hold absolute `blob/main/…` links into this tree, and none of them is checked by CI. Before renaming or moving any of these, grep `web/src`, `web/content`, `firmware/patternflow/console` and `firmware/patternflow/features/*/*_index.h`:
+The site, the firmware and the release notes hold absolute `blob/main/…` links into this tree, and a pull request that breaks one fails `check:links-absolute`. Before renaming or moving any of these, grep `web/src`, `web/content`, `firmware/patternflow/console` and `firmware/patternflow/features/*/*_index.h`:
-- `README.md`, `BUILD_GUIDE.md` (three section anchors are linked from the site), `AUDIO_GUIDE.md`, `FEATURE_GUIDE.md`, `PATTERN_GUIDE.md`
-- `docs/assembly/README.md`, `docs/midi-ableton.md`, `docs/midi-spec.md` (the last two are baked into the Audio edition's console pages)
-- `docs/EDITIONS.md`, `docs/rest-api.md`, `firmware/README.md`, `hardware/bom/bom_v3.9.csv`
+- `README.md`, `BUILD_GUIDE.md` (the site links its section anchors — §1, §2, §4, §8, §8.2 and §10 when this was written — so a renamed heading breaks them), `FEATURE_GUIDE.md`, `PATTERN_GUIDE.md`, `CHANGELOG.md`, `CONTRIBUTING.md` and `.github/SUPPORT.md` (the issue forms link the last two)
+- `docs/AUDIO_GUIDE.md`, `docs/assembly/README.md`, `docs/midi-ableton.md`, `docs/midi-spec.md` (the last two are baked into the Audio edition's console pages)
+- `docs/EDITIONS.md`, `docs/rest-api.md`, `docs/osc-spec.md`, `docs/mqtt-spec.md`, `docs/audio-ws-spec.md`, `docs/pfst-v2-spec.md`, `docs/director-midi.md` (the features page links each of these)
+- `docs/panel-compatibility.md`, `docs/legacy/BUILD_GUIDE_v2.md`, `docs/LICENSE-SUMMARY.md`, `docs/releases/v2.0.0.md`, `firmware/README.md`, `firmware/CUSTOM_PATTERNS.md`, `hardware/README.md`, `hardware/bom/bom_v3.9.csv`, `tools/patternflow-audio-android/README.md`
+- `hardware/case/README.md`, and each folder in `hardware/case/remixes/` with the files in it. The Build panel's case cards link them by name: a README, a `print_layout.3mf`, the mbchars case's `assembly.md` and `bom.md`
- `web/src/app/compliance/` — the `/compliance` URL is printed on the box. Never move, redirect or noindex it.
-`.github/scripts/check_links.py` checks the *relative* links in every tracked markdown file and runs on every pull request that touches one.
+Folders are linked too. The features page links each feature's folder in `firmware/patternflow/features/`, and the site's Build guide links files in `hardware/case/bed_256mm/`, `hardware/case/bed_330mm/` and `hardware/pcb/`. Every row, with the file the link sits in, is in [`investigations/2026-10-doc-map.csv`](investigations/2026-10-doc-map.csv) under the kind `absolute-blob`. `npm run doc-map` in `web/` writes that list again from the tree as it is.
+
+`.github/scripts/check_links.py` checks the *relative* links in every tracked markdown file and runs on every pull request that touches one. `check:links-absolute` (`web/scripts/doc-map.ts --check absolute`, in `drift.yml`) checks the absolute ones on every pull request, against the tree or the tag a link names.
diff --git a/docs/SERVICES.md b/docs/SERVICES.md
index ee49989ab..fca4ac937 100644
--- a/docs/SERVICES.md
+++ b/docs/SERVICES.md
@@ -1,6 +1,6 @@
# Patternflow Server Services & Worker Management
-Patternflow 커뮤니티 웹 서버 및 C++ 펌웨어 웹플래시 컴파일 빌드 워커 서비스 구성과 운영 방법 가이드입니다.
+Patternflow 커뮤니티 웹 서버와, 패턴 C++ 헤더를 `.pfm` 모듈로 굽는 빌드 워커 서비스 구성과 운영 방법 가이드입니다.
---
@@ -32,10 +32,10 @@ RestartSec=3
OOM 킬은 `dmesg -T | grep -i "killed process"` 로 봅니다.
### ② `patternflow-worker.service`
-- **역할**: C++ 펌웨어 빌드 워커 (`scripts/build-worker.ts`)
-- **설명**: 웹플래시 요청 대기열(`queued`)을 수신하여 `arduino-cli`를 이용해 ESP32-S3 바이너리를 컴파일합니다.
+- **역할**: 패턴 모듈 빌드 워커 (`scripts/build-worker.ts`)
+- **설명**: 빌드 대기열(`queued`)에서 잡을 하나씩 가져가 헤더마다 `firmware/toolchain/build_module.py`와 Xtensa 컴파일러로 ESP32-S3용 `.pfm` 모듈을 굽습니다. 펌웨어 이미지 통째 빌드는 2026-08-12(`4c13474`)에 끝났고, 그 뒤로 워커는 `arduino-cli`를 부르지 않습니다.
- **위치**: `/etc/systemd/system/patternflow-worker.service`
-- **환경변수 중요 사항**: `Environment=PATH`에 `arduino-cli` 설치 경로(`~/.local/bin` 또는 `/usr/local/bin`) 포함 필수
+- **환경변수 중요 사항**: 워커는 `python`(다른 이름이면 `PYTHON_PATH`)과 `PF_XTENSA_BIN`이 가리키는 컴파일러(아래 2단계)를 띄웁니다. `Environment=PATH`에는 Python이 있어야 하고, 예전에 넣은 `arduino-cli` 경로는 이제 쓰이지 않습니다.
#### 워커는 격리된 채로 돈다 — `hardening.conf` (2026-09-04)
@@ -93,9 +93,10 @@ strace로 확인한 전부다: 데이터 디렉터리(DB·WAL·산출물), 워
것으로 재현했다. 두 단계 중 첫째는 끝났다:
1. **시크릿은 유닛 밖 (완료, 2026-09-04).** 워커가 필요로 하는 변수
- (`COMMUNITY_ENABLED`, `BUILD_ENABLED`, `BUILD_FQBN`, `COMMUNITY_DB_PATH`)는
- root 전용 `/etc/patternflow/worker.env`(0600)에서 `EnvironmentFile=`로
- 받는다. `.env.local`은 `InaccessiblePaths`로 막으면 워커가 못 뜬다 —
+ (`COMMUNITY_ENABLED`, `BUILD_ENABLED`, `COMMUNITY_DB_PATH`, 2단계의
+ `PF_XTENSA_BIN`)는 root 전용 `/etc/patternflow/worker.env`(0600)에서
+ `EnvironmentFile=`로 받는다. 그날 같이 넣은 `BUILD_FQBN`은 통째 이미지
+ 빌드의 보드 옵션이라 8월 12일 뒤로 읽는 코드가 없다. `.env.local`은 `InaccessiblePaths`로 막으면 워커가 못 뜬다 —
`scripts/loadEnv.ts`의 `existsSync`는 접근 불가 파일에도 `true`를 돌려주고,
그다음 `process.loadEnvFile`이 EACCES에 예외를 던진다. 그래서
`BindReadOnlyPaths=/dev/null:…/.env.local`로 **빈 파일로 보이게** 한다:
@@ -314,8 +315,11 @@ npm run build:enqueue
## 3. 자주 발생하는 문제 & 조치법 (Troubleshooting)
-- **웹플래시 요청이 계속 `queued`에서 안 넘어감**:
+- **빌드 요청이 계속 `queued`에서 안 넘어감**:
- `patternflow-worker.service`가 켜져 있는지 확인: `sudo systemctl status patternflow-worker.service`
- 꺼져 있다면 재시작: `sudo systemctl restart patternflow-worker.service`
-- **`spawn arduino-cli ENOENT` 에러 발생**:
- - `patternflow-worker.service` 파일 내 `PATH` 환경변수에 `arduino-cli` 설치 경로가 올바르게 지정되었는지 확인.
+- **빌드 오류가 `The build service's compiler is not available right now. No Xtensa compiler found`로 시작함**:
+ - 워커가 `xtensa-esp32s3-elf-g++`를 찾지 못했습니다. `/etc/patternflow/worker.env`의 `PF_XTENSA_BIN`이 래퍼 디렉터리(`/etc/patternflow/xtensa-sandbox`)를 가리키는지, 래퍼가 부르는 `~/.arduino15/packages/esp32/tools/esp-x32/2601`이 그대로 있는지 확인.
+ - 오류 전문은 `npm run build:status -- `의 `--- error ---` 아래에 나옵니다.
+- **빌드 오류에 `[cannot run python: spawn python ENOENT]`가 나옴**:
+ - 유닛의 `PATH`에 `python`이 없습니다. `worker.env`에 `PYTHON_PATH=python3`처럼 실행 파일 이름이나 경로를 넣고 워커를 재시작.
diff --git a/docs/assembly/README.md b/docs/assembly/README.md
index 07010a0d1..beb91b416 100644
--- a/docs/assembly/README.md
+++ b/docs/assembly/README.md
@@ -1,13 +1,13 @@
# Build Patternflow — Assembly Map
> **Which version am I building?** Current hardware is the **v3.9 board** (the v3.0 board with the USB-C footprint removed; same case, same guide): follow **[BUILD_GUIDE.md](../../BUILD_GUIDE.md)** and take the files from the tree — [`hardware/README.md`](../../hardware/README.md) says exactly which Gerber, BOM and STL to use. The [v3.0.0 release](https://github.com/engmung/Patternflow/releases/tag/v3.0.0) bundles the previous revision's files.
-> Own a **v2.x board**? Use **[BUILD_GUIDE_v2.md](../../BUILD_GUIDE_v2.md)** and the **[v2.1.0 release](https://github.com/engmung/Patternflow/releases/tag/v2.1.0)** instead. **v2 and v3 parts are not interchangeable** — the boards do not fit each other's cases.
+> Own a **v2.x board**? Use **[BUILD_GUIDE_v2.md](../legacy/BUILD_GUIDE_v2.md)** and the **[v2.1.0 release](https://github.com/engmung/Patternflow/releases/tag/v2.1.0)** instead. **v2 and v3 parts are not interchangeable** — the boards do not fit each other's cases.
> 🔦 **Buying the LED panel? Read [LED Panel Compatibility](../panel-compatibility.md) first.** Patternflow scans the panel directly from the ESP32-S3, so the **driver IC** decides whether it lights up — "HUB75E" on the listing does not, and a spec-matching panel with S-PWM "video wall" drivers stays completely dark with no firmware fix. Since that part number is almost never in the listing, the practical check is the **buyer reviews**: someone running it off an ESP32 or Raspberry Pi is the best evidence you'll get.
Patternflow is not a single, rigid kit. It is a modular system divided into two core parts:
-1. **Enclosure** — how you house the device (3D printed, or a community remix such as the laser-cut one below).
+1. **Enclosure** — how you house the device (3D printed, or one of the community remixes below).
2. **Electronics** — how you wire the hardware (custom PCB, or breadboard).
Build those two, flash the firmware, and your Patternflow is alive.
@@ -31,6 +31,7 @@ This is the route [BUILD_GUIDE.md](../../BUILD_GUIDE.md) walks start to finish,
| 3D printed enclosure | Breadboard / jumper-wire electronics | Available — [Breadboard Build Guide](https://patternflow.work/build/breadboard) |
| Laser-cut acrylic or MDF sheet | Custom PCB | Community — [`remixes/simonepda-lasercut/`](../../hardware/case/remixes/simonepda-lasercut/README.md), by Simone Majocchi. Built by its author in both materials; its README stands in for the printing and case-assembly sections of the build guide |
| 3D printed, glued halves with screwed-on covers | Custom PCB | Community — [`remixes/besoiobiy-printed/`](../../hardware/case/remixes/besoiobiy-printed/README.md), by Besoiobiy. Built by its author; the panel is bolted through the case into its own M3 sockets, so check your panel's thread size and socket depth first. Its README stands in for the printing and case-assembly sections of the build guide and sets the wiring order |
+| 3D printed, horizontal on a desk, with stands | Custom PCB | Community — [`remixes/mbchars-horizontal-desktop-printed/`](../../hardware/case/remixes/mbchars-horizontal-desktop-printed/README.md), by Mykyta Bilous (mbchars). Built by its author; the panel lies the long way with the controls on the right, and there is no power-bank bay — a separate USB-PD trigger module feeds `J4`, the screw terminal, at 5 V. Its README and assembly notes stand in for the printing and case-assembly sections of the build guide |
| Any other enclosure | either | Not an official path. Community variants live in [`hardware/case/remixes/`](../../hardware/case/remixes/README.md); the old acrylic drawings, for the v1 and v2 boards, are in [`legacy_lasercut/`](../../hardware/case/legacy_lasercut/README.md) |
The breadboard path exists to make Patternflow easier and cheaper to start. A breadboard build is not just a temporary prototype — if that form is enough for you, it is a valid Patternflow build. Want a more finished object later? Move to the PCB and printed-enclosure path whenever you like.
@@ -41,7 +42,7 @@ The custom PCB path is stable. PCBA may become a later electronics path for peop
To bring the hardware to life you flash it with firmware, and you can **create and run your own generative patterns with AI coding assistants**.
-Custom patterns no longer need a local toolchain or a reflash: a pattern built in the browser installs on the panel over Wi-Fi as a `.pfm` module in seconds (the browser flasher writes the firmware itself over USB once, and sets up Wi-Fi while it is at it). The PlatformIO route remains only for firmware feature development or targeting a different LED matrix resolution.
+Custom patterns no longer need a local toolchain or a reflash: a pattern built in the browser installs on the panel over Wi-Fi as a `.pfm` module in seconds (the browser flasher writes the firmware over USB once, with the DevKit out of the board and in download mode — hold BOOT, tap RST — and Wi-Fi is set after it, usually on a second go with the flasher or from the panel's own network; the Play guide's [Flash chapter](https://patternflow.work/guide/play#flash) has the order, and [`BUILD_GUIDE.md` §8](../../BUILD_GUIDE.md#8-firmware) the details). The PlatformIO route remains only for firmware feature development or targeting a different LED matrix resolution.
- **[Make your own patterns](../../PATTERN_GUIDE.md)** — the Pattern Lab, the community, and sending a pattern to your device over Wi-Fi. [`firmware/CUSTOM_PATTERNS.md`](../../firmware/CUSTOM_PATTERNS.md) is the hands-on route.
@@ -50,4 +51,4 @@ Custom patterns no longer need a local toolchain or a reflash: a pattern built i
| Version | Guide | Everything bundled |
| --- | --- | --- |
| **v3.9 board** (current; v3.0 is the same board with a USB-C footprint you leave empty) | [BUILD_GUIDE.md](../../BUILD_GUIDE.md) | the files in [`hardware/`](../../hardware/README.md); the [v3.0.0 release](https://github.com/engmung/Patternflow/releases/tag/v3.0.0) bundles the v3.0 revision |
-| v2.1.0 (legacy) | [BUILD_GUIDE_v2.md](../../BUILD_GUIDE_v2.md) | [v2.1.0 release](https://github.com/engmung/Patternflow/releases/tag/v2.1.0) |
+| v2.1.0 (legacy) | [BUILD_GUIDE_v2.md](../legacy/BUILD_GUIDE_v2.md) | [v2.1.0 release](https://github.com/engmung/Patternflow/releases/tag/v2.1.0) |
diff --git a/docs/audio-ws-vectors/README.md b/docs/audio-ws-vectors/README.md
new file mode 100644
index 000000000..c9ec8b651
--- /dev/null
+++ b/docs/audio-ws-vectors/README.md
@@ -0,0 +1,13 @@
+# Audio WebSocket vectors
+
+Each file here is one sentence of [audio-ws-spec.md](../audio-ws-spec.md) and the messages that show a panel doing it, written as [contract-vectors.md](../contract-vectors.md) sets out. `npm run check:audio-vectors` in `web/` replays every one against a fresh [virtual panel](../../tools/virtual-panel/README.md) built with the `audio` feature, and prints what the vector expects beside what the panel did. CI runs it on every pull request with the other drift checks. When the spec rewords a sentence a vector quotes, that vector fails until someone brings it along.
+
+A file's name starts with the part of the spec it reads. `messages-` is a row of the message table, or the rule for anything else. `lanes-` is what a lane is and what outranks it, and `release-` is the 500 ms release. `switch-` is the AUD switch. `/api/audio` is REST, so its vectors are in [rest-api-vectors](../rest-api-vectors/README.md) and start with `audio-`.
+
+A message is the text of one frame, `"a=0.8,-,-,0.2"`, and `{ "close": 1000 }` closes the socket with that code. Node 20 has no WebSocket client, so the suite brings one, `web/scripts/drift/_ws.ts`, and opens one socket a vector before the first step. After each message it sends a ping and waits for the pong, so the panel holds the message before the clock moves. The panel reads what has arrived at the end of a frame, 16.7 ms at 60 fps. A vector gives a lane 20 ms before it reads `/api/status`, and a `d=` 50 ms, since its clicks reach the pattern a frame later. A fresh panel has Audio-React on, as a board with nothing saved boots, so no vector switches it on first.
+
+`messages-d.json` reads "normalized" as a share of a parameter's whole range, which rest-api.md puts at 48 detents. A `d=` of 0.5 is 24 clicks.
+
+Three vectors keep the spec where the firmware does something else. `messages-a.json` sends `a=-0.5,0.25,-,-`, and the firmware takes the leading `-` for a skipped slot and puts 0.5 on the second lane (the `a=` parse in `core_audio_ws.h`). `messages-unknown.json` sends `off=x`, which the firmware reads as `off=0`. `lanes-hand.json` sends a `d=` to a knob a lane drives, and the firmware holds that knob five seconds as if a hand had turned it (`applyLaneMotion()` in `patternflow.ino`).
+
+Some of the spec has no vectors. The virtual panel has no microphone and no weather, so the WebSocket is the only source its lanes have. A vector talks over one socket, so two clients writing the same knob are not replayed, and neither is a socket that closes while another stays open. The one-in-flight pacing and the extension's resend timer are rules for a client, and a panel has nothing to show for them.
diff --git a/docs/audio-ws-vectors/lanes-bus.json b/docs/audio-ws-vectors/lanes-bus.json
new file mode 100644
index 000000000..7dd5dbd4b
--- /dev/null
+++ b/docs/audio-ws-vectors/lanes-bus.json
@@ -0,0 +1,12 @@
+{
+ "contract": "docs/audio-ws-spec.md#semantics--what-a-lane-is",
+ "says": "Priority per knob, highest first (see `abi/pf_params.h`):",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.5,0.5,-,-" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [true, true, false, false] } } },
+ { "send": { "http": "POST /api/params?p1=700" } },
+ { "send": { "ws": "a=0.5,0.5,-,-" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "params": [700, "", "", ""], "paramActive": [true, false, false, false], "laneActive": [false, true, false, false] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/lanes-close.json b/docs/audio-ws-vectors/lanes-close.json
new file mode 100644
index 000000000..86bd585cf
--- /dev/null
+++ b/docs/audio-ws-vectors/lanes-close.json
@@ -0,0 +1,11 @@
+{
+ "contract": "docs/audio-ws-spec.md#semantics--what-a-lane-is",
+ "says": "the firmware also releases lanes when the socket closes",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.25,0.5,0.75,1" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [true, true, true, true], "audioClients": 1 } } },
+ { "send": { "ws": { "close": 1000 } } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [false, false, false, false], "audioClients": 0 } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/lanes-hand.json b/docs/audio-ws-vectors/lanes-hand.json
new file mode 100644
index 000000000..ef21309e7
--- /dev/null
+++ b/docs/audio-ws-vectors/lanes-hand.json
@@ -0,0 +1,17 @@
+{
+ "contract": "docs/audio-ws-spec.md#semantics--what-a-lane-is",
+ "says": "A physical turn of an encoder takes that knob back and holds it for five seconds; keep streaming and the lane resumes when the hold expires.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.5,0.5,-,-" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [true, true, false, false] } } },
+ { "send": { "hand": { "knob": 1, "turn": 1 } } },
+ { "send": { "ws": "d=1,v=0.25" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [false, true, false, false] } } },
+ { "send": { "wait": 4700 } },
+ { "send": { "ws": "a=0.5,0.5,-,-" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [false, true, false, false] } } },
+ { "send": { "ws": "a=0.5,0.5,-,-" } },
+ { "send": { "wait": 300 }, "expect": { "status": { "laneActive": [true, true, false, false] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/messages-a.json b/docs/audio-ws-vectors/messages-a.json
new file mode 100644
index 000000000..ba3f74ad0
--- /dev/null
+++ b/docs/audio-ws-vectors/messages-a.json
@@ -0,0 +1,15 @@
+{
+ "contract": "docs/audio-ws-spec.md#messages",
+ "says": "A literal `-` in a slot leaves that lane untouched: `a=0.8,-,-,0.2`.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.25,0.5,0.75,1" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "lanes": [0.25, 0.5, 0.75, 1], "laneActive": [true, true, true, true] } } },
+ { "send": { "wait": 200 } },
+ { "send": { "ws": "a=0.8,-,-,0.2" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "lanes": [0.8, 0.5, 0.75, 0.2], "laneActive": [true, true, true, true] } } },
+ { "send": { "wait": 400 }, "expect": { "status": { "laneActive": [true, false, false, true] } } },
+ { "send": { "ws": "a=-0.5,0.25,-,-" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "lanes": ["", 0.25, 0.75, 0.2] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/messages-d.json b/docs/audio-ws-vectors/messages-d.json
new file mode 100644
index 000000000..b0e0c37aa
--- /dev/null
+++ b/docs/audio-ws-vectors/messages-d.json
@@ -0,0 +1,12 @@
+{
+ "contract": "docs/audio-ws-spec.md#messages",
+ "says": "Add a normalized delta `F` (−1..1) to knob `N` — encoder-style motion rather than a level.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "d=1,v=0.5" } },
+ { "send": { "wait": 50 }, "expect": { "input": { "given": { "deltas": [0, 24, 0, 0] } }, "status": { "laneActive": [false, false, false, false] } } },
+ { "send": { "ws": "d=1,v=-0.25" } },
+ { "send": { "ws": "d=3,v=-1" } },
+ { "send": { "wait": 50 }, "expect": { "input": { "given": { "deltas": [0, 12, 0, -48] } } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/messages-k.json b/docs/audio-ws-vectors/messages-k.json
new file mode 100644
index 000000000..7d0b43f11
--- /dev/null
+++ b/docs/audio-ws-vectors/messages-k.json
@@ -0,0 +1,11 @@
+{
+ "contract": "docs/audio-ws-spec.md#messages",
+ "says": "Set lane `N` (0..3) to `F` (clamped to `0..1`).",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "k=0,v=0.25" } },
+ { "send": { "ws": "k=2,v=1.5" } },
+ { "send": { "ws": "k=3,v=-2" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "lanes": [0.25, "", 1, 0], "laneActive": [true, false, true, true] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/messages-off-n.json b/docs/audio-ws-vectors/messages-off-n.json
new file mode 100644
index 000000000..e0c93db68
--- /dev/null
+++ b/docs/audio-ws-vectors/messages-off-n.json
@@ -0,0 +1,13 @@
+{
+ "contract": "docs/audio-ws-spec.md#messages",
+ "says": "Release knob `N` back to encoder control.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.25,0.5,0.75,1" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [true, true, true, true] } } },
+ { "send": { "ws": "off=2" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [true, true, false, true] } } },
+ { "send": { "ws": "off=0" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [false, true, false, true] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/messages-off.json b/docs/audio-ws-vectors/messages-off.json
new file mode 100644
index 000000000..ed702e332
--- /dev/null
+++ b/docs/audio-ws-vectors/messages-off.json
@@ -0,0 +1,11 @@
+{
+ "contract": "docs/audio-ws-spec.md#messages",
+ "says": "Release all four.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.25,0.5,0.75,1" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [true, true, true, true] } } },
+ { "send": { "ws": "off" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [false, false, false, false] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/messages-unknown.json b/docs/audio-ws-vectors/messages-unknown.json
new file mode 100644
index 000000000..03995d998
--- /dev/null
+++ b/docs/audio-ws-vectors/messages-unknown.json
@@ -0,0 +1,17 @@
+{
+ "contract": "docs/audio-ws-spec.md#messages",
+ "says": "Anything else is **silently ignored** — that is the compatibility rule.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.25,0.5,0.75,1" } },
+ { "send": { "wait": 20 } },
+ { "send": { "ws": "x=1" } },
+ { "send": { "ws": "k=4,v=0.5" } },
+ { "send": { "ws": "d=4,v=1" } },
+ { "send": { "ws": "off=x" } },
+ { "send": { "ws": "hello" } },
+ { "send": { "wait": 50 }, "expect": { "status": { "lanes": [0.25, 0.5, 0.75, 1], "laneActive": [true, true, true, true] }, "input": { "given": { "deltas": [0, 0, 0, 0] } } } },
+ { "send": { "ws": "k=1,v=0" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "lanes": [0.25, 0, 0.75, 1] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/release-500ms.json b/docs/audio-ws-vectors/release-500ms.json
new file mode 100644
index 000000000..d4d37d3dc
--- /dev/null
+++ b/docs/audio-ws-vectors/release-500ms.json
@@ -0,0 +1,11 @@
+{
+ "contract": "docs/audio-ws-spec.md#holding-a-value--the-500-ms-release",
+ "says": "A lane is handed back to its encoder 500 ms after the last message that set it.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "http": "POST /api/audio?on=1" } },
+ { "send": { "ws": "a=0.8,-,-,0.2" } },
+ { "send": { "wait": 400 }, "expect": { "status": { "laneActive": [true, false, false, true] } } },
+ { "send": { "wait": 200 }, "expect": { "status": { "laneActive": [false, false, false, false] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/release-resend.json b/docs/audio-ws-vectors/release-resend.json
new file mode 100644
index 000000000..114351b02
--- /dev/null
+++ b/docs/audio-ws-vectors/release-resend.json
@@ -0,0 +1,14 @@
+{
+ "contract": "docs/audio-ws-spec.md#holding-a-value--the-500-ms-release",
+ "says": "So a client that means to keep driving **resends its last message at least every 250 ms**, changed or not.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.5,-,-,-" } },
+ { "send": { "wait": 250 } },
+ { "send": { "ws": "a=0.5,-,-,-" } },
+ { "send": { "wait": 250 } },
+ { "send": { "ws": "a=0.5,-,-,-" } },
+ { "send": { "wait": 250 }, "expect": { "status": { "laneActive": [true, false, false, false] } } },
+ { "send": { "wait": 300 }, "expect": { "status": { "laneActive": [false, false, false, false] } } }
+ ]
+}
diff --git a/docs/audio-ws-vectors/switch-off.json b/docs/audio-ws-vectors/switch-off.json
new file mode 100644
index 000000000..c2cc95d50
--- /dev/null
+++ b/docs/audio-ws-vectors/switch-off.json
@@ -0,0 +1,16 @@
+{
+ "contract": "docs/audio-ws-spec.md#the-switch-on-the-panel",
+ "says": "Switched off, the messages are parsed and nothing is driven — an open socket is not evidence that the panel is listening.",
+ "features": ["audio"],
+ "steps": [
+ { "send": { "ws": "a=0.25,0.5,0.75,1" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "laneActive": [true, true, true, true] } } },
+ { "send": { "http": "POST /api/audio?on=0" }, "expect": { "status": { "audioRuntime": false, "laneActive": [false, false, false, false] } } },
+ { "send": { "ws": "a=0.25,0.5,0.75,1" } },
+ { "send": { "ws": "d=0,v=0.5" } },
+ { "send": { "wait": 50 }, "expect": { "status": { "laneActive": [false, false, false, false], "audioClients": 1 }, "input": { "given": { "deltas": [0, 0, 0, 0] } } } },
+ { "send": { "http": "POST /api/audio?on=1" } },
+ { "send": { "ws": "k=0,v=0.25" } },
+ { "send": { "wait": 20 }, "expect": { "status": { "audioRuntime": true, "laneActive": [true, false, false, false] } } }
+ ]
+}
diff --git a/docs/contract-vectors.md b/docs/contract-vectors.md
new file mode 100644
index 000000000..ed05b768a
--- /dev/null
+++ b/docs/contract-vectors.md
@@ -0,0 +1,81 @@
+# Contract vectors
+
+A vector is one sentence from a contract and the steps that show a panel doing what it says. Each is a JSON file in a folder beside its spec: `rest-api-vectors/` for [rest-api.md](rest-api.md), and `osc-vectors/`, `midi-vectors/`, `mqtt-vectors/` and `audio-ws-vectors/` for the other four. Each folder has a suite in `web/scripts/drift/`, run as `npm run check:rest-vectors` in `web/` and the same way for the other four. It gives every vector a fresh [virtual panel](../tools/virtual-panel/README.md) and prints what the vector expects beside what the panel did. [`pfst-v2-vectors/`](pfst-v2-vectors/) came first and works differently: three `.pfs` files and a table in its spec.
+
+```json
+{
+ "contract": "docs/rest-api.md#post-apisleep",
+ "says": "`sleep` is the state **before** the transition — the request is only queued here.",
+ "features": [],
+ "steps": [
+ { "send": { "http": "POST /api/sleep?on=1" }, "expect": { "reply": { "status": 200, "body": { "ok": true, "sleep": false } } } },
+ { "send": { "wait": 20 }, "expect": { "status": { "sleep": true } } }
+ ]
+}
+```
+
+`contract` is the spec and the heading the sentence sits under, written the way a link to it is. `says` is the sentence, copied out of the spec. Markdown and line breaks do not count, every word does. When the spec rewords it, the vector fails until someone brings it along. `features` are the features the panel is built with, by their directories in `firmware/patternflow/features/`, and `[]` is the default build. A REST vector for the Audio edition's routes names `audio` there.
+
+## Steps
+
+Each step sends one thing:
+
+| send | |
+| --- | --- |
+| `http` | `"POST /api/sleep?on=1"`, or `{ "request": "POST /api/sleep", "form": "on=1", "headers": { "Host": "patternflow.local" } }` |
+| `hand` | `{ "knob": 2, "turn": -3 }` turns knob 2 three detents back. `{ "knob": 4, "down": true }` presses its button, and `false` lets go |
+| `wait` | `500` moves the panel's clock 500 ms |
+| `osc`, `midi`, `mqtt`, `ws` | a message on that transport, written as in the examples below; the suite's sender puts it on the wire |
+
+The clock stands still between steps. Only `wait` and `out` move it, so a 500 ms release costs no real time. Knobs count from 1, as every contract does.
+
+A step can then expect four things, each read once the step is over. `reply` is what the send answered: `status`, `headers` with their names in lower case, and `body`, parsed when it is JSON. `status` is `GET /api/status`. `input` is the panel's `/vp/state`, the knob deltas and presses the running pattern has been given since start, its name card and the first error it threw. A delta that comes over OSC or MIDI never shows in `knobs`, which counts a hand's clicks, so `input` is where it shows. `out` is what the panel sent.
+
+## Shapes
+
+An expectation is a shape. A literal has to be equal. `""`, `""`, `""`, `""`, `""`, `"