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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>/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 <name> vA.B.C` (docs/RELEASING.md).
- Editions carry their own version line, independent of the project version (`PF_VARIANT_VERSION` in `firmware/bundles/<name>/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 <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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/EDITIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions docs/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<panel>/update?size=<bytes>"`. 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:
Expand Down Expand Up @@ -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*.
Expand Down
2 changes: 1 addition & 1 deletion firmware/bundles/performance/overrides.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
31 changes: 20 additions & 11 deletions firmware/patternflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion firmware/toolchain/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <noreply@anthropic.com>"
CO_AUTHOR = "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
TRAILER = "🤖 Generated with [Claude Code](https://claude.com/claude-code)"

if hasattr(sys.stdout, "reconfigure"):
Expand Down
Binary file not shown.
Binary file not shown.
7 changes: 4 additions & 3 deletions web/src/app/editions/editions-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 3 additions & 5 deletions web/src/app/features/features-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") },
Expand Down
Loading