diff --git a/AGENTS.md b/AGENTS.md index 7749a233..cde3a657 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ It is standalone (not Eurorack), powered from a 5 V power bank through a screw t ## 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//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 vA.B.C` (docs/RELEASING.md). +- Editions carry their own version line, independent of the project version (`PF_VARIANT_VERSION` in `firmware/bundles//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. The one exception is a try-out image: a frozen copy of a composition that is not on the shelf, named by the features page (`tryOut` in `web/src/app/features/features-data.ts`) so people can install it to try, never bumped with the core, and retired only by a newer try-out of the same name. `clock` and `midi` are bundles the tree keeps building (CI compiles every composition) but does not put on the shelf: the clock is a feature, and the MIDI bundle is the composition that proves the USB-OTG build; each has a try-out image (`clock-v0.1.5`, `midi-v0.1.0`). 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 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6974b51e..2d9f921c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ All notable changes to Patternflow will be documented in this file, newest first ### Web -- **The features page.** `/features` is the catalogue the shelf never was: every feature as one row — name, one line, and a tag saying where it lives (every firmware, Audio, Performance, a recipe in the tree, or an attempt that did not work) — so the whole list fits on a screen, and a row opens to what it needs, which firmware carries it, the links and a reel of it running. No headings between rows: which firmware carries a feature is a fact on its row, never a category over it, or the shelf would be back one level up. Reels are Instagram embeds by permalink, loaded only when a row is opened. The shelf's lede links here. +- **The features page.** `/features` is the catalogue the shelf never was: every feature as one row — name, one line, and a tag saying where it lives (every firmware, Audio, Performance, a recipe in the tree, or an attempt that did not work) — so the whole list fits on a screen, and a row opens to what it needs, which firmware carries it, the links and a reel of it running. No headings between rows: which firmware carries a feature is a fact on its row, never a category over it, or the shelf would be back one level up. Reels are Instagram embeds by permalink, loaded only when a row is opened. The shelf and the catalogue share one header: Firmware and Features as a pair of tabs where the title was, the current one the title, the other a click away. +- **Try-out images on the features page.** The clock and the USB-MIDI build are not on the shelf, and until now trying either meant building it. Their rows carry a frozen image now — `clock-v0.1.5` as it shipped with 3.10.2, `midi-v0.1.0` built from the tree on 2026-09-17 — installed the way the shelf installs, through the panel's own `/update` page. Frozen means frozen: the image is the whole firmware as it was, core included, and nobody bumps it when the core moves; patterns, Wi-Fi networks and settings stay, and the way back is one click on the shelf. `check_versions.py` reads these folders from `features-data.ts`, so the rule that the shelf holds only what is named still holds. ### Docs diff --git a/docs/EDITIONS.md b/docs/EDITIONS.md index c50b0588..18ab18d5 100644 --- a/docs/EDITIONS.md +++ b/docs/EDITIONS.md @@ -369,10 +369,13 @@ The shelf has two tiers, and the difference is who to ask when it breaks, not quality. A bundle in the tree is not on the shelf by itself. `firmware/bundles/clock` -and `firmware/bundles/midi` are compositions CI keeps compiling, with no card -and no image: the clock is a feature, and the MIDI bundle proves the USB-OTG -build. An edition is listed when its maintainer cuts it (`release.py edition`, -see [`RELEASING.md`](RELEASING.md)) and a card names the image. +and `firmware/bundles/midi` are compositions CI keeps compiling, with no card: +the clock is a feature, and the MIDI bundle proves the USB-OTG build. What +each has instead is a try-out image on the features page - a frozen copy, +installed the way the shelf installs but never bumped with the core, and +named in `web/src/app/features/features-data.ts` rather than by a card. An +edition is listed when its maintainer cuts it (`release.py edition`, see +[`RELEASING.md`](RELEASING.md)) and a card names the image. **Official** — built from this repository. A core change has to compile against it before that change lands, so it cannot silently rot. Its image is served diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 18ee4811..39511e1a 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -72,7 +72,7 @@ What the two commands do, step by step - and the way to do it by hand. ./firmware/bundles/shelf.sh core vX.Y.Z ``` - then point `web/public/flash/manifest.json` at the new folder. An edition that also moved gets its own `shelf.sh vA.B.C` and a card update in `web/src/app/editions/editions-data.ts`. The shelf retires the previous folder of the same name — older images stay on their tags. + then point `web/public/flash/manifest.json` at the new folder. An edition that also moved gets its own `shelf.sh vA.B.C` and a card update in `web/src/app/editions/editions-data.ts`. The shelf retires the previous folder of the same name — older images stay on their tags. Try-out images (`clock-v0.1.5`, `midi-v0.1.0`, the ones the features page installs) are not part of a release and stay as they are; a fresh one is its own `shelf.sh vA.B.C` and a row update in `web/src/app/features/features-data.ts`, done when somebody wants a newer one, not because the core moved. 7. Run the web checks from `web/`: `npm run lint && npm run typecheck && npm run check:ci && npm run build`. 8. Commit and tag: diff --git a/firmware/bundles/README.md b/firmware/bundles/README.md index cacf25ab..36493f08 100644 --- a/firmware/bundles/README.md +++ b/firmware/bundles/README.md @@ -68,8 +68,12 @@ know whether they are picking up something unfinished or something frozen. A bundle can also be in the tree and not on the shelf at all — `clock` and `midi` are. Then it is a composition CI keeps compiling so a core change -cannot break it silently, and there is no card, no image and nothing to cut. -The shelf is what its maintainer stands behind, and it is short on purpose. +cannot break it silently, and there is no card and nothing to cut. It can +still be tried: the features page carries a frozen image of it, staged with +`shelf.sh vA.B.C` like a shelf image but named from +`web/src/app/features/features-data.ts`, and nobody bumps it when the core +moves. The shelf is what its maintainer stands behind, and it is short on +purpose. ## Graduating diff --git a/firmware/bundles/shelf.sh b/firmware/bundles/shelf.sh index b03ac539..fe03581b 100755 --- a/firmware/bundles/shelf.sh +++ b/firmware/bundles/shelf.sh @@ -6,9 +6,10 @@ # ./shelf.sh audio v0.3.0 # ./shelf.sh performance v0.2.1 # ./shelf.sh core v3.8.0 (the featureless default) +# ./shelf.sh midi v0.1.0 (a try-out image for the features page) # # Stages four files into web/public/flash/bin/-/ and stops. -# Editing variants-data.ts and deploying the site are separate, deliberate +# Editing editions-data.ts (features-data.ts for a try-out) and deploying the site are separate, deliberate # steps: a staged image nobody has looked at should not become the thing a # stranger flashes. # @@ -191,7 +192,9 @@ cp "$BOOT_APP0" "$OUT/boot_app0.bin" # reads them from there). Keeping them in the tree meant 22 folders and 26 MB # by 2026-09 for three that were served. So an older folder of the same name # leaves when its successor arrives — including core's pre-shelf spelling, -# the bare "v3.x.y" folders. +# the bare "v3.x.y" folders. A try-out image - a composition the features +# page installs for trying, not a shelf entry - goes the same way, and only +# that way: nothing but a newer try-out of its name retires it. shopt -s nullglob for old in web/public/flash/bin/"$NAME"-v*; do [ "$old" = "$OUT" ] && continue @@ -212,4 +215,5 @@ ls -l "$OUT" | tail -4 | awk '{printf " %8s %s\n", $5, $9}' echo "" echo "next, by hand and on purpose:" echo " - point the card at it in web/src/app/editions/editions-data.ts" +echo " (a try-out image: the row's tryOut in web/src/app/features/features-data.ts)" echo " - deploy the site" diff --git a/firmware/toolchain/check_versions.py b/firmware/toolchain/check_versions.py index 248be221..18247216 100644 --- a/firmware/toolchain/check_versions.py +++ b/firmware/toolchain/check_versions.py @@ -105,11 +105,22 @@ def main() -> int: if not (ROOT / "web/public/flash/bin" / folder / "patternflow.ino.bin").is_file(): problems.append(f"the {ident} card names an image that is not on the shelf: web/public/flash/bin/{folder}/") - # The shelf holds only what the cards name. - named = {f"core-{core_v}"} | {f"{n}-{v}" for n, v in editions.items() if v} + # Try-out images: frozen copies of compositions that are not on the shelf + # (the clock, the USB-MIDI build), which the features page installs for + # trying. release.py never touches them; they are named in features-data.ts + # and must exist, and they count as named below. + features = text("web/src/app/features/features-data.ts") + tryouts = set() + for path, folder in re.findall(r'url: "(/flash/bin/([^/"]+)/patternflow\.ino\.bin)"', features): + tryouts.add(folder) + if not (ROOT / "web/public" / path.lstrip("/")).is_file(): + problems.append(f"features-data.ts names a try-out image that is not there: web/public{path}") + + # The shelf holds only what the cards and the try-out rows name. + named = {f"core-{core_v}"} | {f"{n}-{v}" for n, v in editions.items() if v} | tryouts on_shelf = {p.name for p in (ROOT / "web/public/flash/bin").iterdir() if p.is_dir()} for extra in sorted(on_shelf - named): - problems.append(f"web/public/flash/bin/{extra} is on the shelf but no card or manifest names it") + problems.append(f"web/public/flash/bin/{extra} is on the shelf but no card, manifest or try-out row names it") if problems: print(f"{len(problems)} version problem(s):") @@ -117,7 +128,8 @@ def main() -> int: print(f" - {p}") return 1 print(f"versions agree: core {core_v}, " + ", ".join(f"{n} {v}" for n, v in editions.items()) - + " - in net_config.h, the overrides, AGENTS.md, the manifest and the /editions cards, images on the shelf") + + " - in net_config.h, the overrides, AGENTS.md, the manifest and the /editions cards, images on the shelf" + + (f"; try-out images: {', '.join(sorted(tryouts))}" if tryouts else "")) return 0 diff --git a/web/ARCHITECTURE.md b/web/ARCHITECTURE.md index 2c60a198..50f517c1 100644 --- a/web/ARCHITECTURE.md +++ b/web/ARCHITECTURE.md @@ -15,7 +15,7 @@ The `web/` app is the Patternflow site at [patternflow.work](https://patternflow | `/pattern-lab` | The pattern workspace — see below | | `/community/**` | The community: feed, pattern pages (`/p/[id]`), profiles (`/u/[username]`), decks (`/d/[id]`, `/decks`), the Workshop (`/workshop/[code]`), territories and the atlas, notifications, featured, reports. Renders a pointer to the community host unless `COMMUNITY_ENABLED=1` | | `/editions` | The edition shelf: every firmware you can put on a panel, official and community, with one-click install. `/variants` (the URL until 2026-09, baked into shipped console pages) redirects here permanently | -| `/features` | The catalogue: every feature on its own — what it needs, which firmware carries it, and a reel of it running (Instagram embeds by permalink, lazy). The other axis of the shelf; hand-curated in `features-data.ts` | +| `/features` | The catalogue: every feature on its own — what it needs, which firmware carries it, and a reel of it running (Instagram embeds by permalink, lazy). The other axis of the shelf; hand-curated in `features-data.ts`. A feature that is not on the shelf can carry a frozen try-out image (`tryOut`), installed the way the shelf installs | | `/update` | The device's firmware-update handoff: the browser downloads an image and POSTs it to the panel over the LAN, because the panel cannot fetch over TLS | | `/flash` (static) | esp-web-tools flasher driven by `public/flash/manifest.json` + the images in `public/flash/bin/` — only the currently served ones are committed | | `/journal` · `/journal/[slug]` (+ `/en`) | Bilingual (ko/en) MDX journal with per-article OG image generation | diff --git a/web/public/builds/slowrush/clock.jpg b/web/public/builds/slowrush/clock.jpg new file mode 100644 index 00000000..7b8d2025 Binary files /dev/null and b/web/public/builds/slowrush/clock.jpg differ diff --git a/web/public/builds/slowrush/doorstep.jpg b/web/public/builds/slowrush/doorstep.jpg new file mode 100644 index 00000000..99485087 Binary files /dev/null and b/web/public/builds/slowrush/doorstep.jpg differ diff --git a/web/public/builds/slowrush/garden.jpg b/web/public/builds/slowrush/garden.jpg new file mode 100644 index 00000000..d3123a58 Binary files /dev/null and b/web/public/builds/slowrush/garden.jpg differ diff --git a/web/public/flash/bin/clock-v0.1.5/boot_app0.bin b/web/public/flash/bin/clock-v0.1.5/boot_app0.bin new file mode 100644 index 00000000..13562cab Binary files /dev/null and b/web/public/flash/bin/clock-v0.1.5/boot_app0.bin differ diff --git a/web/public/flash/bin/clock-v0.1.5/patternflow.ino.bin b/web/public/flash/bin/clock-v0.1.5/patternflow.ino.bin new file mode 100644 index 00000000..f50d7edf Binary files /dev/null and b/web/public/flash/bin/clock-v0.1.5/patternflow.ino.bin differ diff --git a/web/public/flash/bin/clock-v0.1.5/patternflow.ino.bootloader.bin b/web/public/flash/bin/clock-v0.1.5/patternflow.ino.bootloader.bin new file mode 100644 index 00000000..247411ea Binary files /dev/null and b/web/public/flash/bin/clock-v0.1.5/patternflow.ino.bootloader.bin differ diff --git a/web/public/flash/bin/clock-v0.1.5/patternflow.ino.partitions.bin b/web/public/flash/bin/clock-v0.1.5/patternflow.ino.partitions.bin new file mode 100644 index 00000000..223d9ebf Binary files /dev/null and b/web/public/flash/bin/clock-v0.1.5/patternflow.ino.partitions.bin differ diff --git a/web/public/flash/bin/midi-v0.1.0/boot_app0.bin b/web/public/flash/bin/midi-v0.1.0/boot_app0.bin new file mode 100644 index 00000000..13562cab Binary files /dev/null and b/web/public/flash/bin/midi-v0.1.0/boot_app0.bin differ diff --git a/web/public/flash/bin/midi-v0.1.0/patternflow.ino.bin b/web/public/flash/bin/midi-v0.1.0/patternflow.ino.bin new file mode 100644 index 00000000..a22c195f Binary files /dev/null and b/web/public/flash/bin/midi-v0.1.0/patternflow.ino.bin differ diff --git a/web/public/flash/bin/midi-v0.1.0/patternflow.ino.bootloader.bin b/web/public/flash/bin/midi-v0.1.0/patternflow.ino.bootloader.bin new file mode 100644 index 00000000..247411ea Binary files /dev/null and b/web/public/flash/bin/midi-v0.1.0/patternflow.ino.bootloader.bin differ diff --git a/web/public/flash/bin/midi-v0.1.0/patternflow.ino.partitions.bin b/web/public/flash/bin/midi-v0.1.0/patternflow.ino.partitions.bin new file mode 100644 index 00000000..223d9ebf Binary files /dev/null and b/web/public/flash/bin/midi-v0.1.0/patternflow.ino.partitions.bin differ diff --git a/web/src/app/editions/Editions.module.css b/web/src/app/editions/Editions.module.css index d1d69f20..5120ec12 100644 --- a/web/src/app/editions/Editions.module.css +++ b/web/src/app/editions/Editions.module.css @@ -318,3 +318,24 @@ line-height: 1.65; color: var(--pf-ink-muted); } + +/* ── the two views, as tabs where the title was ────────────────────────── + "Firmware" is the shelf, "Features" the catalogue. The current one is + the h1; the other is a link in the same size, dimmed - so the other view + is one click away and impossible to miss, which a sentence in the lede + was not. */ +.tabs { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 6px 26px; + margin: 0 0 14px; +} +.tabs .title { margin: 0; } +/* The link carries .title too, so it is set exactly like the h1 at every + width (the phone size above included); this only dims it. */ +.tabLink { + color: var(--pf-ink-faint); + text-decoration: none; +} +.tabLink:hover { color: var(--pf-led); } diff --git a/web/src/app/editions/ShelfTabs.tsx b/web/src/app/editions/ShelfTabs.tsx new file mode 100644 index 00000000..5495be33 --- /dev/null +++ b/web/src/app/editions/ShelfTabs.tsx @@ -0,0 +1,37 @@ +import Link from "next/link"; +import styles from "./Editions.module.css"; + +// The two views of the same thing, as a pair of tabs where the page title +// would be: "Firmware" is the shelf (what you install), "Features" is the +// catalogue (what you are choosing between). The current one IS the h1; the +// other is a link in the same size, dimmed. Two routes still — deep links +// and search keep working — this just makes the other one impossible to +// miss, which a sentence in the lede was not. +const TABS = [ + { id: "firmware", href: "/editions", label: "Firmware" }, + { id: "features", href: "/features", label: "Features" }, +] as const; + +export type ShelfTab = (typeof TABS)[number]["id"]; + +export default function ShelfTabs({ active }: { active: ShelfTab }) { + return ( + + ); +} diff --git a/web/src/app/editions/page.tsx b/web/src/app/editions/page.tsx index ea8e1d3c..81ea30db 100644 --- a/web/src/app/editions/page.tsx +++ b/web/src/app/editions/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import styles from "./Editions.module.css"; import EditionCard from "./EditionCard"; +import ShelfTabs from "./ShelfTabs"; import { EDITIONS } from "./editions-data"; const OFFICIAL = EDITIONS.filter((v) => v.tier === "official"); @@ -43,14 +44,12 @@ export default function EditionsPage() { Patternflow -

Firmware

+

One panel, more than one firmware. The first is what ships on the board and does everything; the rest exist for what it cannot carry. Switching is one click, and your patterns, Wi-Fi networks and - settings come with you. What each firmware carries, feature by - feature and with a reel of each, is on the{" "} - features page. + settings come with you.

diff --git a/web/src/app/features/Features.module.css b/web/src/app/features/Features.module.css index 5ef1c61b..fa9a563c 100644 --- a/web/src/app/features/Features.module.css +++ b/web/src/app/features/Features.module.css @@ -116,6 +116,22 @@ .facts dd a { color: var(--pf-ink); } .facts dd a:hover { color: var(--pf-led); } +/* Trying one. A frozen image, installed the way the shelf installs a live + one; the button, the address row and the note are the shelf's own. */ +.tryOut { + margin: 0 0 18px; + padding: 14px 16px 4px; + border: var(--pf-rule-w) solid var(--pf-rule); +} +.tryHead { + margin: 0 0 12px; + font-family: var(--pf-mono); + font-size: 10px; + letter-spacing: var(--pf-track-mono); + text-transform: uppercase; + color: var(--pf-ink-faint); +} + /* The reel. Instagram's embed page is tall; 560px shows the reel's frame with its header and the post's own footer, and scrolls inside itself for anything longer. It is only in the document once the row is open, so it diff --git a/web/src/app/features/TryInstall.tsx b/web/src/app/features/TryInstall.tsx new file mode 100644 index 00000000..60f1281c --- /dev/null +++ b/web/src/app/features/TryInstall.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useSyncExternalStore } from "react"; +import Link from "next/link"; +import { useDeviceHost } from "@/lib/community/deviceHost"; +import shelf from "../editions/Editions.module.css"; +import styles from "./Features.module.css"; +import type { TryOut } from "./features-data"; + +// "Try it" on a feature that is not on the shelf: one button that installs a +// frozen image the way the shelf installs a live one. Same mechanism - the +// panel's own /update page takes a `?src=` URL and the browser does the fetch +// and the POST - so this is a plain link to the panel, and the address is the +// one the shelf and the deck panel already share. +// +// What is different is said, once, under the button: the image is the whole +// firmware as it was when it was built, core included, and nobody bumps it +// when the core moves. The way back is the shelf. + +const SITE = "https://patternflow.work"; + +export default function TryInstall({ t }: { t: TryOut }) { + const { deviceHost, changeDeviceHost } = useDeviceHost(); + // The panel's page fetches the image, so the URL has to be absolute. The + // production origin is the server-side answer, so the first paint matches; + // a preview or a dev server reads its own in on the client. A store that + // never changes, read the way the device address is, rather than an effect + // setting state after mount. + const origin = useSyncExternalStore( + () => () => {}, + () => window.location.origin, + () => SITE, + ); + + const host = deviceHost.trim() || "patternflow.local"; + const src = new URL(t.url, origin).toString(); + const href = `http://${host}/update?src=${encodeURIComponent(src)}`; + + return ( +
+

Try it

+ +

+ + image {t.version} + + + core {t.core} + + + built {t.built} + +

+ +

+ {t.note} A frozen image: the whole firmware as it was on the day it was + built, core included, and it is not kept up with the core. Your + patterns, Wi-Fi networks and settings stay; the way back is one click on{" "} + the shelf. +

+
+ ); +} diff --git a/web/src/app/features/features-data.ts b/web/src/app/features/features-data.ts index 028a1b12..6add649f 100644 --- a/web/src/app/features/features-data.ts +++ b/web/src/app/features/features-data.ts @@ -23,6 +23,27 @@ export type Reel = { /** The small tag on the row: where the feature is, in two words. */ export type Home = "every" | "audio" | "performance" | "recipe" | "none"; +/** + * A frozen image served from here so a feature that is not on the shelf + * can still be tried in one click. Installed exactly the way the shelf + * installs - through the panel's own /update page, the browser doing the + * fetch - but never bumped with the core: it is the whole firmware as it + * was the day it was built, and the row says so. check_versions.py reads + * these urls, so the folder has to exist under web/public/flash/bin/. + */ +export type TryOut = { + /** What the image reports as its variant version. */ + version: string; + /** The core it was built on, as the image reports it. */ + core: string; + /** The day it was built - what "frozen" is frozen at. */ + built: string; + /** Site-relative path of the app image. Made absolute in the browser. */ + url: string; + /** What is particular about trying THIS one. The generic caveat is printed once, by the component. */ + note: string; +}; + export type Feature = { /** Directory under firmware/patternflow/features/, or "core". */ id: string; @@ -42,6 +63,7 @@ export type Feature = { maintainerHref?: string; links: { label: string; href: string }[]; reel?: Reel; + tryOut?: TryOut; }; export const HOME_LABEL: Record = { @@ -175,7 +197,7 @@ export const FEATURES: Feature[] = [ home: "audio", where: "Over Wi-Fi: in the Audio firmware. Over USB: the midi composition in the " + - "tree, built with build.sh midi — not on the shelf.", + "tree, not on the shelf — there is an image to try, below.", whereHref: "/editions#audio", links: [ { label: "MIDI spec", href: blob("docs/midi-spec.md") }, @@ -187,6 +209,16 @@ export const FEATURES: Feature[] = [ url: REEL_MIDI, caption: "Ableton Live driving the panel, and the panel's knobs mapped back into Live, over the network.", }, + tryOut: { + version: "v0.1.0", + core: "3.10.2", + built: "2026-09-17", + url: "/flash/bin/midi-v0.1.0/patternflow.ino.bin", + note: + "The USB build: while it is on, the DevKit's native USB port is a " + + "class-compliant MIDI port plus a serial port, and the Wi-Fi route works " + + "as well. Data only; the panel is powered from its screw terminal as ever.", + }, }, { id: "clock", @@ -208,6 +240,15 @@ export const FEATURES: Feature[] = [ { label: "Source", href: tree("firmware/patternflow/features/clock") }, ], reel: { url: REEL_CLOCK, caption: "The clock, in a few of its faces, over a running pattern." }, + tryOut: { + version: "v0.1.5", + core: "3.10.2", + built: "2026-09-11", + url: "/flash/bin/clock-v0.1.5/patternflow.ino.bin", + note: + "The clock as it shipped on the shelf with 3.10.2, before it left it: " + + "the seven faces, the time zone, the console page.", + }, }, { id: "mqtt", diff --git a/web/src/app/features/page.tsx b/web/src/app/features/page.tsx index 06d8bf4b..e1c54df0 100644 --- a/web/src/app/features/page.tsx +++ b/web/src/app/features/page.tsx @@ -1,8 +1,10 @@ import type { Metadata } from "next"; import Link from "next/link"; import shelf from "../editions/Editions.module.css"; +import ShelfTabs from "../editions/ShelfTabs"; import styles from "./Features.module.css"; import Reel from "./Reel"; +import TryInstall from "./TryInstall"; import { FEATURES, HOME_LABEL, type Feature } from "./features-data"; // /features — the catalogue, as one list. @@ -69,6 +71,7 @@ function Row({ f }: { f: Feature }) { + {f.tryOut ? : null}
{f.links.map((l) => l.href.startsWith("http") ? ( @@ -102,7 +105,7 @@ export default function FeaturesPage() { Patternflow -

Features

+

Everything a panel does beyond showing patterns is a feature: a piece of firmware that attaches to the core without the core diff --git a/web/src/components/sections/InsideGlobe/builds.ts b/web/src/components/sections/InsideGlobe/builds.ts index 1cef9870..b695a094 100644 --- a/web/src/components/sections/InsideGlobe/builds.ts +++ b/web/src/components/sections/InsideGlobe/builds.ts @@ -251,6 +251,23 @@ export const builds: Build[] = [ { src: '/builds/alfredo/glow.jpg', alt: "Colored light from Alfredo Borboa's Patternflow spilling across a wooden table" }, ], }, + { + id: 'uk-slowrush', + title: 'Orange, in the garden', + category: 'builds', + slug: 'slowrush-uk', + kind: 'build', + location: { lat: 54.0, lng: -2.0, label: 'United Kingdom' }, + maker: 'slowrush', + date: '2026-09', + description: + 'A first build, on a v3 board that xponentone had spare and sent over — one member’s stock becoming another’s panel. Plain by the maker’s own account except for the colour, and the colour is the point: a fresh orange that sits well among the flowers. It came out beautifully, and it runs the clock.', + images: [ + { src: '/builds/slowrush/garden.jpg', alt: "slowrush's orange Patternflow standing in a flower bed, a blue pattern on the panel" }, + { src: '/builds/slowrush/doorstep.jpg', alt: "slowrush's orange Patternflow on a doorstep with the garden behind it, a speckle of cyan and yellow on the panel" }, + { src: '/builds/slowrush/clock.jpg', alt: "slowrush's Patternflow on its side on a shelf at night, the clock showing 10:15 cut out of a pattern" }, + ], + }, ]; // One order for the text list and the globe's previous/next controls.