diff --git a/AGENTS.md b/AGENTS.md index e85dc47d..3bb882cd 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.4 (current, released 2026-09-19), 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.4, Performance v0.2.9 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). +- Editions carry their own version line, independent of the project version (`PF_VARIANT_VERSION` in `firmware/bundles//overrides.h`): Audio v0.6.4, Performance v0.4.0 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 a8e35833..6f53b61c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to Patternflow will be documented in this file, newest first ### Firmware +- **A firmware upload survives a Wi-Fi hitch.** The `/update` connection now waits up to two minutes for a stalled upload instead of five seconds, and the page gives up after five minutes with a message instead of waiting forever. Checked on a panel with a 12 s stall halfway through a flash. `PUT /update` takes a raw image, for `curl -T`. From Simone Majocchi ([#450](https://github.com/engmung/Patternflow/pull/450)). +- **Two 64×64 modules chained make a 128×64 panel.** `PANEL_GEOMETRY` in `config.h` picks the stock 128×64, one 64×64, or two 64×64 daisy-chained (the `firmware64x2` PlatformIO env). The driver is told the module and the chain, and everything else sees the canvas; the stock build is unchanged. From Simone Majocchi ([#446](https://github.com/engmung/Patternflow/pull/446)). +- **Performance v0.4.0 carries the clock and a Weather face**, and its show catalogue holds 100 sequences instead of 48. An MQTT subscriber following a fast sequence stream now drains the broker and applies the latest value per knob each frame instead of falling one message per frame behind (a jump of more than 16 clicks in one frame is capped), and a publisher no longer replays its own retained snapshot when it reconnects. From Simone Majocchi ([#445](https://github.com/engmung/Patternflow/pull/445), [#448](https://github.com/engmung/Patternflow/pull/448), [#449](https://github.com/engmung/Patternflow/pull/449)). - **The panel is a network of its own.** With no known Wi-Fi in reach it raises a hotspot, `patternflow-a1b2` (its alias), WPA2, password `patternflow` until changed, and the whole console is at `http://192.168.4.1/` on it - patterns, knobs, the Wi-Fi page to add the next place's network, updates. Modes on `/wifi`: `auto` (default: up fifteen seconds after the last link, down once a network is joined and nobody is on it), `always`, `off`; `GET`/`POST /api/hotspot`, and a `hotspot` object in status. The NETWORK screen shows the name while the hotspot is what there is. What the bench decided (`src/core_hotspot.h` says why, line by line): the channel comes from a scan - the least loaded of 1/6/11 - never a fixed one; alone, the radio runs AP-only, and the station comes back only for a rare probe with nobody connected or when credentials arrive; a DNS responder on the hotspot resolves every name to the panel and the phone's internet probe fails within a second instead of timing out for twenty; the hotspot is 20 MHz. Pretending to be the internet was tried and put a Samsung into its limited-connectivity state, which drops the network. - **Console services start on any link.** Every core page, and the audio, microphone, clock and MQTT pages, used to wait for `WL_CONNECTED` before registering - on the hotspot a phone got an address and port 80 never opened. `PatternflowWifi::linkUp()` is the condition now, the station or the hotspot, and the hotspot raises the same link edge the station does. - **Full transmit power.** The 13 dBm cap from 2026-08 is retired (`PF_WIFI_TX_POWER` is the radio's 19.5 dBm again). Measured on the hotspot: a phone next to the panel took 4-9 s per 10 KB page at 13 dBm and under a second at full power - the phone's own transmitter had hidden the asymmetry, and a router's antenna had hidden it on the home network. Owner's decision. diff --git a/docs/EDITIONS.md b/docs/EDITIONS.md index 18ab18d5..06bd50f3 100644 --- a/docs/EDITIONS.md +++ b/docs/EDITIONS.md @@ -63,7 +63,7 @@ in one click. Four exist: |---|---|---| | **Patternflow** | nothing — the device itself | the product | | **Audio** | OSC, MIDI, browser audio, the on-board microphone | SeungHun Lee | -| **Performance** | sequences, MQTT, FlowLocal, the Director, weather | Simone Majocchi | +| **Performance** | sequences, MQTT, FlowLocal, the Director, weather, the clock | Simone Majocchi | | **Clock** | the time, cut out of the running pattern | SeungHun Lee | The word "addon" is retired. It suggested something optional or third-party, diff --git a/docs/rest-api.md b/docs/rest-api.md index edb72a21..c8ad45f4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -428,13 +428,16 @@ Requires `PF_WEBUPDATE_ENABLED` (default on). | Route | | |---|---| | `GET /update` | Drop-zone page | -| `POST /update` | Multipart firmware image, streamed into `Update.h` | +| `POST /update` | Multipart firmware image, streamed into `Update.h`. What the `/update` page sends. | +| `PUT /update` | The same image as a raw body, for scripts: `curl -T patternflow.ino.bin "http:///update?size="`. Same gate, same reply, same reboot. | | `GET /update/status` | `{armed, busy, version, lastError, lastRejected, lastOk, received, expected, attempts}` | `armed` is the gate. With `PF_WEBUPDATE_ALWAYS_ARMED 1` (the default) it is always true and anyone on the Wi-Fi can flash the device from a phone browser — the same exposure ArduinoOTA's no-password default already has, and the right call on a home or studio network. Set the flag to `0` for shared, office or exhibition Wi-Fi and uploads are refused unless the UPDATE screen is physically open on the device (hold K2 → NETWORK, turn K4). An incoming image also wakes a sleeping device. +An upload may stall for up to two minutes — a Wi-Fi hitch halfway through a flash — before the device gives up on the connection; before 1.5 it was five seconds. The image goes to the other app slot, so an upload that is dropped leaves the running firmware exactly as it was. + ## Identifying a device There is **no** MAC address, serial number or other hardware identifier on any endpoint. What exists: @@ -472,7 +475,7 @@ In short: HTTP is the management and state transport, OSC and MIDI are the low-l ## Version history -- **1.5** (unreleased) — the hotspot (`hotspot` in status, `GET`/`POST /api/hotspot`); status gains `network` and `thumbs` diagnostics, `fsError` (why storage is not mounted) and `flashId`; a failed `POST /api/patterns/format` returns the reason as its `error`; `POST /api/wifi/reconnect` reconnects without a reboot; core name registration retries partial failures and preserves feature-owned services; the MIDI edition adds a `midiUsb` block to status ([`midi-spec.md`](midi-spec.md)); `GET`/`POST /api/knobs` and the `/knobs` page (encoder direction and edges per click, per knob, persisted). +- **1.5** (unreleased) — the hotspot (`hotspot` in status, `GET`/`POST /api/hotspot`); status gains `network` and `thumbs` diagnostics, `fsError` (why storage is not mounted) and `flashId`; a failed `POST /api/patterns/format` returns the reason as its `error`; `POST /api/wifi/reconnect` reconnects without a reboot; core name registration retries partial failures and preserves feature-owned services; the MIDI edition adds a `midiUsb` block to status ([`midi-spec.md`](midi-spec.md)); `GET`/`POST /api/knobs` and the `/knobs` page (encoder direction and edges per click, per knob, persisted); `PUT /update` takes a raw image, and an upload survives a stall of up to two minutes. - **1.4** (2026-09-06) — `GET /api/patterns/file` gains `ext=thumb`; `GET /api/display` takes `brightness` and status reports it; status gains `resetReason` and `load.internal`/`load.psram`; console pages are served gzip-compressed (`Content-Encoding: gzip`); the page sender no longer truncates on a slow link; the server no longer trips the Core-0 watchdog on a request that stalls mid-header. - **1.3** (2026-09-04) — `GET`/`POST /api/clock` (Utility edition) and the `clock` block in status; `caps` gains `"clock"`. - **1.2** (2026-09-03) — the server is serviced on Core 0 (the one-connection rule stands; the render-pays rule is history); status gains `httpCore`, `netStackMin`, `loopSyncServed`/`loopSyncMaxUs`; `POST /api/params` documents `d1`..`d4` and how a held value reaches a legacy pattern; `GET /api/patterns/select` gains `step`; `GET`/`POST /api/audio` (Audio-React) are documented; `featureNav`'s microphone label is *Audio*. diff --git a/firmware/bundles/performance/overrides.h b/firmware/bundles/performance/overrides.h index c6455945..f34efa54 100644 --- a/firmware/bundles/performance/overrides.h +++ b/firmware/bundles/performance/overrides.h @@ -15,7 +15,7 @@ // edition's own and moves at whatever pace suits it — it has nothing to say // about the core version, which is reported separately. #define PF_VARIANT "performance" -#define PF_VARIANT_VERSION "v0.2.9" +#define PF_VARIANT_VERSION "v0.4.0" // ── Feature presets ───────────────────────────────────────────────────── // Black: show scheduler night/alarm face (hidden from K4 browse). diff --git a/firmware/patternflow/README.md b/firmware/patternflow/README.md index 51f3997f..a9dd99d9 100644 --- a/firmware/patternflow/README.md +++ b/firmware/patternflow/README.md @@ -56,21 +56,30 @@ Each file starts with a metadata header kept in sync with the JS source: Two different things, and it's worth keeping them apart. -**Your panel** is set in [`config.h`](./config.h): +**Your panel** is set in [`config.h`](./config.h) by `PANEL_GEOMETRY`: ```cpp -#define PANEL_RES_W 128 -#define PANEL_RES_H 64 -#define PANEL_CHAIN 1 +#define PANEL_GEOM_128x64_SINGLE 1 // one 128×64 module (stock) +#define PANEL_GEOM_64x64_SINGLE 2 // one 64×64 module +#define PANEL_GEOM_64x64_CHAIN2 3 // two 64×64 modules daisy-chained → 128×64 ``` -Running something other than the stock 128×64 — a 64×64 module, or two -128×64 panels chained into 256×64 — means **editing those lines to match your -hardware and reflashing. That's the whole change.** Nothing else in the -firmware hardcodes a panel size: the HUB75 driver config, the radius/angle -tables, the canvas buffer and the on-screen menus all derive from these three -values. The patterns do too, as long as they loop over `PANEL_RES_W` / -`PANEL_RES_H` instead of typing `128` and `64`. +The stock panel needs nothing. For one of the others, uncomment the +`#define PANEL_GEOMETRY` line in `config.h` — or build the `firmware64x2` +PlatformIO env for two chained 64×64 modules — and reflash. Anything else +means editing the numbers in the last branch of that block: `PANEL_PHYS_W/H` +is one module, `PANEL_CHAIN` how many are daisy-chained, and +`PANEL_RES_W/H` the canvas they add up to. The build stops with an `#error` +if the canvas is not the module times the chain. A bigger canvas costs +internal RAM, three bytes a pixel. + +The driver is told the module and the chain; everything else — the canvas +buffer, the radius/angle tables, the on-screen menus and the patterns — sees +only the canvas, `PANEL_RES_W` × `PANEL_RES_H`. Patterns follow any size as +long as they loop over those two instead of typing `128` and `64`. The +chained 64×64 build compiles in CI but had not been lit on real modules when +it landed; if you run one, say how it went on +[#224](https://github.com/engmung/Patternflow/issues/224). **A pattern's frame** is the pixel grid it was *composed* for, which is not always the panel's. A 64×128 pattern on a 128×64 panel is just the device diff --git a/firmware/toolchain/release.py b/firmware/toolchain/release.py index 07c342ae..a6b6c9a0 100644 --- a/firmware/toolchain/release.py +++ b/firmware/toolchain/release.py @@ -55,7 +55,7 @@ EDITIONS_TS = ROOT / "web/src/app/editions/editions-data.ts" BIN_DIR = ROOT / "web/public/flash/bin" EDITIONS = ("audio", "performance") -CO_AUTHOR = "Co-Authored-By: Claude Fable 5.1 " +CO_AUTHOR = "Co-Authored-By: Claude Opus 5.5 " TRAILER = "🤖 Generated with [Claude Code](https://claude.com/claude-code)" if hasattr(sys.stdout, "reconfigure"): diff --git a/web/public/flash/bin/performance-v0.2.9/patternflow.ino.bin b/web/public/flash/bin/performance-v0.2.9/patternflow.ino.bin deleted file mode 100644 index 9b7447a4..00000000 Binary files a/web/public/flash/bin/performance-v0.2.9/patternflow.ino.bin and /dev/null differ diff --git a/web/public/flash/bin/performance-v0.2.9/boot_app0.bin b/web/public/flash/bin/performance-v0.4.0/boot_app0.bin similarity index 100% rename from web/public/flash/bin/performance-v0.2.9/boot_app0.bin rename to web/public/flash/bin/performance-v0.4.0/boot_app0.bin diff --git a/web/public/flash/bin/performance-v0.4.0/patternflow.ino.bin b/web/public/flash/bin/performance-v0.4.0/patternflow.ino.bin new file mode 100644 index 00000000..b20b9114 Binary files /dev/null and b/web/public/flash/bin/performance-v0.4.0/patternflow.ino.bin differ diff --git a/web/public/flash/bin/performance-v0.2.9/patternflow.ino.bootloader.bin b/web/public/flash/bin/performance-v0.4.0/patternflow.ino.bootloader.bin similarity index 100% rename from web/public/flash/bin/performance-v0.2.9/patternflow.ino.bootloader.bin rename to web/public/flash/bin/performance-v0.4.0/patternflow.ino.bootloader.bin diff --git a/web/public/flash/bin/performance-v0.2.9/patternflow.ino.partitions.bin b/web/public/flash/bin/performance-v0.4.0/patternflow.ino.partitions.bin similarity index 100% rename from web/public/flash/bin/performance-v0.2.9/patternflow.ino.partitions.bin rename to web/public/flash/bin/performance-v0.4.0/patternflow.ino.partitions.bin diff --git a/web/src/app/editions/editions-data.ts b/web/src/app/editions/editions-data.ts index 60f1f320..cbd9b245 100644 --- a/web/src/app/editions/editions-data.ts +++ b/web/src/app/editions/editions-data.ts @@ -194,11 +194,12 @@ export const EDITIONS: Edition[] = [ 'Sequences — cue lists, timelines, the night/wake scheduler', 'MQTT in every role — publisher, subscriber, bridge', 'FlowLocal and the Director', - 'Weather — temperature and wind mapped onto the knobs', + 'Weather — temperature and wind mapped onto the knobs, and a face of its own', + 'The clock — the time, cut out of the running pattern', ], hosted: { - version: 'v0.2.9', - url: 'https://patternflow.work/flash/bin/performance-v0.2.9/patternflow.ino.bin', + version: 'v0.4.0', + url: 'https://patternflow.work/flash/bin/performance-v0.4.0/patternflow.ino.bin', }, source: 'https://github.com/engmung/Patternflow/tree/main/firmware/bundles/performance', note: diff --git a/web/src/app/features/features-data.ts b/web/src/app/features/features-data.ts index 6add649f..bfd7a9cd 100644 --- a/web/src/app/features/features-data.ts +++ b/web/src/app/features/features-data.ts @@ -230,11 +230,9 @@ export const FEATURES: Feature[] = [ "console page whose preview draws the same pixels the panel does and " + "sends every change as you make it.", needs: [], - home: "recipe", - where: - "Joining the Performance firmware. Until then a recipe in the tree, " + - "firmware/bundles/clock — build it with build.sh clock.", - whereHref: tree("firmware/bundles/clock"), + home: "performance", + where: "In the Performance firmware.", + whereHref: "/editions#performance", links: [ { label: "Console API", href: blob("docs/rest-api.md") }, { label: "Source", href: tree("firmware/patternflow/features/clock") },