Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions .github/workflows/firmware-build.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
name: Firmware builds

# The three editions, compiled, on every firmware change.
# Every composition, compiled, on every firmware change - one job each.
#
# For two months this repository shipped a firmware whose CI compiled nothing:
# the boundary and ABI checks in firmware-checks.yml finish in seconds and
# catch what a grep can catch, but a core change that broke only one edition
# was found by whoever next ran `firmware/bundles/build.sh all` at their desk.
# The 3.9.1 cycle moved the network onto its own core across a dozen files
# and reached dev without a compiler ever seeing it in CI. That is what this
# workflow closes: the same `build.sh all` — default, audio, performance, clock, then
# the marker scan that proves each image carries exactly its own features —
# on a runner, with the PlatformIO packages cached so a warm run is minutes.
# workflow closes: `build.sh one <composition>` - default, audio, performance,
# clock, midi - each in its own job, so the wall clock is one build rather
# than five in a row, and each image scanned for the marker strings that prove
# it carries exactly its features. Not every composition is on the shelf
# (clock and midi are compositions the tree keeps buildable); every one of
# them has to compile, because the point of the feature seam is that a core
# change breaks nothing it cannot see.
#
# No secrets file exists on the runner, so every image here is built the way
# a published one is (the placeholder SSID, not somebody's Wi-Fi). Nothing is
Expand All @@ -34,8 +38,13 @@ permissions: {}

jobs:
build:
name: ${{ matrix.composition }}
runs-on: ubuntu-latest
timeout-minutes: 45
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
composition: [default, audio, performance, clock, midi]
steps:
- uses: actions/checkout@v4

Expand All @@ -44,8 +53,9 @@ jobs:
python-version: "3.11"

# The xtensa toolchain and the Arduino core are ~700 MB; the build tree
# under ~/pf-build keeps the framework's objects between editions and
# between runs. Keyed on platformio.ini, which names both.
# under ~/pf-build keeps the framework's objects between compositions
# and between runs. Keyed on platformio.ini, which names both. Five jobs
# share the key: the first to finish saves it, the rest find it saved.
- name: Cache PlatformIO packages and the build tree
uses: actions/cache@v4
with:
Expand All @@ -59,16 +69,13 @@ jobs:
- name: Install PlatformIO
run: pip install --quiet platformio==6.1.19

- name: Build every edition and scan the images for their features
run: bash firmware/bundles/build.sh all

- name: Image sizes
run: ls -l "$HOME/pf-build-editions"/*.bin
- name: Build ${{ matrix.composition }} and scan the image for its features
run: bash firmware/bundles/build.sh one ${{ matrix.composition }}

# Flash is not the scarce resource here; internal DRAM is, and a loadable
# pattern's code budget is whatever is left of it. The step above has always
# measured the one that does not matter. This reads each edition's elf and
# fails on a static-RAM move, per edition, because the whole point of the
# edition split is that the blast radius differs.
- name: Internal RAM each edition has already spent
run: python firmware/toolchain/check_footprint.py
# pattern's code budget is whatever is left of it. The size printed above
# measures the one that does not matter. This reads the composition's elf
# and fails on a static-RAM move, per composition, because the whole point
# of the split is that the blast radius differs.
- name: Internal RAM this composition has already spent
run: python firmware/toolchain/check_footprint.py --only ${{ matrix.composition }}
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ It is standalone (not Eurorack), powered from a 5 V power bank through a screw t

## 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/<name>/<name>_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) — 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 **editions**: `audio`, `performance`, `clock`; 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).
- `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/<name>/<name>_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) — 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), 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), 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.
- `web/` — Next.js site at patternflow.work: landing page (`/`, `/pattern`, `/build`, `/inside` are tabs of one view), 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`.
- `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.

Expand All @@ -27,14 +27,14 @@ It is standalone (not Eurorack), powered from a 5 V power bank through a screw t
## 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.
- 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` and `clock`. `./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.
- 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).
- KiCad exports: Export Gerbers from `hardware/pcb/kicad/patternflow.kicad_pcb`. Export STLs from `hardware/case/source/patternflow_case.blend`.

## Versioning
- Project: v3.10.2 (current, released 2026-09-11), using unified semantic versioning across firmware, hardware, web, and docs — the firmware reports it as `PF_IMPROV_FW_VERSION` in `net_config.h`, and `CHANGELOG.md` gets a section per release. Current hardware is the v3.9 board — the v3.0 board with the USB-C footprint removed, same enclosure, pin map and guide; every v3.x release has been firmware/web on it.
- Editions carry their own version line, independent of the project version (`PF_VARIANT_VERSION` in `firmware/bundles/<name>/overrides.h`): Audio v0.6.2, Performance v0.2.7, Clock v0.1.5 at the time of writing. The shelf (`web/src/app/editions/editions-data.ts`) and the flasher manifest (`web/public/flash/manifest.json`) name the images that are live; only those images are kept in `web/public/flash/bin/` — older ones live on their release tags.
- Editions carry their own version line, independent of the project version (`PF_VARIANT_VERSION` in `firmware/bundles/<name>/overrides.h`): Audio v0.6.2, Performance v0.2.7 at the time of writing. The shelf (`web/src/app/editions/editions-data.ts`) and the flasher manifest (`web/public/flash/manifest.json`) name the images that are live; only those images are kept in `web/public/flash/bin/` — older ones live on their release tags. `clock` and `midi` are bundles the tree keeps building (CI compiles every composition) but does not publish: the clock is a feature, and the MIDI bundle is the composition that proves the USB-OTG build. Neither is in the `EDITIONS` tuples of `release.py`/`check_versions.py`; an edition joins those, the cards and the line above only when it goes on the shelf, and its maintainer cuts it with `release.py edition <name> vA.B.C` (docs/RELEASING.md).
- Firmware source lives in `firmware/patternflow/`; use release tags for versioning instead of encoding the release in the folder name. Tags are `vX.Y.Z`.
- Conventions: filenames lowercase with underscores; commit messages start with the area (`firmware:`, `web:`, `docs:`, `hardware:`) then a short present-tense summary.

Expand Down
2 changes: 1 addition & 1 deletion BUILD_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ OSC, network MIDI and audio-react ship in the **Audio** edition: open [patternfl
- **USB-C input removed in v3.9.** A USB-C-powered v3.0 board ran normally for 20–30+ minutes, then smoked at a connector pin and destroyed the receptacle and surrounding power path ([#221](https://github.com/engmung/Patternflow/issues/221)). Whether that was a soldering defect on the tight-pitch THT pins or a structural limit of the part under the matrix's peak current was never settled — so the footprint came off the board instead of shipping an input that must not be populated. **Every build powers through the `J4` screw terminal** (Section 2); on a v3.0 board, leave `USB1`/`R1`/`R2` bare.
- **C11 (1000µF bulk cap) retained** — Patternflow is power-bank-powered; the cap stabilizes the boot transient. Designing a desktop-USB derivative? Drop it to ≤50µF.
- **GPIO0 left floating by design** — most modules don't need the pullup; if yours does, it's a one-resistor fix (Section 5 note, [#16](https://github.com/engmung/Patternflow/issues/16)).
- **Encoder direction is handled in firmware** — the default suits the Bourns PEC11R; if your encoders read backwards, set `INVERT_ENCODER` to `1` in `config.h` instead of touching hardware.
- **Encoder direction is handled in firmware** — the default suits the Bourns PEC11R; if your encoders read backwards, or one click moves two steps, fix it per knob on the device's `/knobs` page — no rebuild; `INVERT_ENCODER` in `config.h` is only the compile-time default.
- **No LED-matrix bump trimming.** The enclosure recesses the panel's two alignment bumps, so the old nipper step ([#19](https://github.com/engmung/Patternflow/issues/19)) is gone. The design also gives you a snap-fit back panel and two wall-mount holes.

---
Expand Down
Loading
Loading