Skip to content

Absolute param bus, deck performances, and the readme rewrite - #307

Merged
engmung merged 19 commits into
mainfrom
dev
Aug 15, 2026
Merged

Absolute param bus, deck performances, and the readme rewrite#307
engmung merged 19 commits into
mainfrom
dev

Conversation

@engmung

@engmung engmung commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Batch merge of the work stacked on dev.

Absolute parameter bus

Patterns can be pinned to an exact look instead of only being nudged by relative encoder clicks. The firmware carries an absolute param bus so the Director can set a value and hands can still take it back, the site emits every pattern absolute-ready from birth, and the existing catalog (presets, the Basics pack, and the community DB) was migrated with firmware/toolchain/migrate_community_absolute.py.

Deck performances

A deck can carry a recorded performance: JSON in, .pfs out, with the pack decorated at serve time so attaching or editing a performance never queues a compile. Publishing takes the .pfs along, and the pack decoration now has a test covering it.

Firmware show player: added, then reverted

The panel-side show player (.pfs tables, /show, night wake) went in and came back out again on this branch, along with its PSRAM and knob-handback fixes. That work hands back to Simone, and dev keeps the web side and the shared floor. Net effect on this merge: no show player in firmware, but the MQTT, pack-select and patterns-HTTP work stays.

README rewrite

Restructured around the path a visitor actually takes, which is Instagram to the site to here. Paik and Participation TV open it, the community builds and MOTIFLOW carry the proof, and three ways in (the device, patterns, the ecosystem) replace the old feature list. Emoji and em dashes are gone throughout.

Two corrections of substance:

  • The build map is stated as hand-built units only, and the breadboard route is described as the same electronics without ordering a custom PCB, not as a solder-free build.
  • Power figures were wrong. Four hours per 10,000 mAh is a typical pattern, not a worst case. A near-full-white screen pulls about 26.6 W, roughly 4.8 A at 5 V, which is above a typical USB-A rating, and gets about 5.5 hours from a 40,000 mAh bank. That is now documented with the connector warning, and the readme promises a pixel-power clamp, so this text needs revisiting when the clamp ships.

Also picks up the campaign's Ableton photo for the OSC section and its library grid for the community wall.

🤖 Generated with Claude Code

engmung and others added 19 commits August 14, 2026 10:26
The right-hand UART port is the one that needs CP2102 / CH34x, and
uploads moved to the left port a while ago, so it is now a port almost
nobody uses. Explaining which links belong to it was more words than
the situation deserves.

Kept to one line rather than removed, because the flasher's own
troubleshooting screen still shows those links, and someone who follows
them from here gets nowhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…an take it back

Simone's performance-director branch, absorbed: MQTT param/1..4 (0..1000)
with channel presets (Broadcast/Ch1-4/Live), retained channel snapshots for
mid-join, a Director mode that overlays a local broker without touching the
saved one, and PFParams:: helpers that give every pattern one line per knob
instead of four. Physical encoder motion releases an absolute hold — the
Director yields to whoever is in the room.

One deliberate departure from his tree: descriptors now stamp
PF_ABI_MODULE_VERSION 2 while the host API stays 1. His fork keeps
everything at 1, which lets a new module load on pre-absolute firmware and
read 12 bytes past the host's InputFrame — knobs silently freeze on garbage.
With the bump, the old loader's exact-match check turns that into its
existing clean refusal, and this loader accepts both generations.

Also from his tree: /patterns multi-select → Download ZIP (browser-side
fflate pack), the console-pause eviction guards it needs, and the JSON
escaping his rework dropped is restored — a community pattern name with a
quote in it must not read as a dead device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Pattern Lab scaffold owns knob accumulation in one deterministic loop,
so swapping that loop for PFParams::apply makes every layered export
absolute-ready with zero prompt changes — the LLM was never allowed near
the knobs. The single-file conversion prompt is the one place a model
writes update() by hand, so it now teaches the four PFParams helpers,
requires ABSOLUTE_READY, and guards button resets against an active hold.

build_module.py sidecars say abi 2 and carry absoluteReady, detected from
the source's own declaration rather than assumed — a hand-written pattern
that still integrates deltas stays honestly delta-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…B tool

All 33 showcase presets adopt Simone's PFParams conversions (his converter
ran on exactly these sources; every diff reviewed, zero manual leftovers).
Three of them — 0629, 0629_2, 0718 — kept his soft 0..10 range with the
real clamp trailing behind, which squeezes an absolute sweep into a corner
of the fader; their apply calls now carry the documented ranges. Basics
pack rebuilt from the converted sources: same 33 patterns, sidecars now
abi 2 + absoluteReady.

migrate_community_absolute.py converts community code_cpp rows the same
way, with two rules his script doesn't have: all-or-nothing per pattern
(anything not fully claimed stays byte-identical and is reported, instead
of being flagged ready with deltas still inside), and originals dumped to
JSON before any write. Rows that are byte-copies of repo presets — 34 of
the 43 in the dev DB — skip the regexes entirely and take the hand-checked
converted file, matched by source hash. Dev DB migrated; the Pi runs the
same command at deploy. Deck zips need no cache sweep — their fingerprint
digests code_cpp.

Verified: all 33 pack modules and all 21 regex-converted community headers
compile as .pfm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… night wake

Simone's on-device show player, ported: the Director's packed PFST cue
tables live on FFat and play locally — pattern switches, absolute params,
banners — with no broker anywhere. /show (Sequences) uploads, plays, loops
and deletes them; while a show runs it owns pattern selection, and
OSC/MQTT/HTTP pickers wait their turn.

His night/wake schedule comes along, minus his Weather stack: a new
core_clock.h carves out exactly the NTP+timezone slice the scheduler needs
(same NVS key his Weather page writes, so the two stay interchangeable),
with the UTC offset set on /show itself. One fix over the fork: his cached
clock refreshed only until first sync, which would freeze a clock face;
accessors here refresh through a 500 ms throttle. The clock face falls back
to the stock GFX font — the TomThumb rework stays unported until it gets
eyes on hardware. Black joins Origin as the compiled-in night face.

/patterns unpack now routes .pfs pack members to the Sequences store, so a
deck zip that carries a performance installs it alongside the modules; the
end-of-batch rescan flag learned to skip those. performance.json stays in
the browser — the device has no use for the editable source.

Static DRAM 90.7 → 108.1 KB (show table buffers); 219 KB static headroom
left. Runtime heap on real hardware is the release gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…decorated

A deck owner attaches a Director performance (paste or drop the Save-JSON
file); the server validates it against the same limits the device's player
enforces and stores it canonically. The pack zip is decorated at serve
time — performance.json for re-editing, the encoded .pfs for the panel —
so attaching or editing one updates downloads instantly and never queues a
compile, and a performance that stops encoding degrades to the plain pack
rather than taking it down.

The TS PFST encoder is a port of the Director's show-table.js, verified
byte-identical against all four of its demo .pfs saves; performance-smoke
pins the byte layout from here on. Pack manifests now report the highest
descriptor ABI in the pack (2 since the absolute bus) instead of a
hardcoded 1 — the Basics manifest lost its publisher field in the earlier
rebuild and gets it back here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…loop

The first on-device flash of the show player never finished booting: abort()
inside mDNS service registration, every cycle, ~1 s after Wi-Fi came up.
Decoded backtrace: newlib failing to malloc a lock inside a log call —
internal DRAM exhausted. The player's cue table + string pool (8.2 KB), the
/api/shows upload buffer (8.3 KB) and the Director pack-select list (2.5 KB)
sat in .bss, and this board runs its whole runtime on ~15 KB of internal
heap once the panel and network are up. The compile-time 'plenty of static
headroom' number was the wrong meter, again.

All three are now PFMem allocations (PSRAM-first, lazy), the same rule the
pattern registry's pools follow: a device that never plays a sequence pays
nothing, and none of these are touched per pixel. Static DRAM 108.1 → 89.1
KB — below what P1 shipped.

Verified on the panel (v3.4.0 → wireless /update): clean serial boot, old
ABI-1 modules load, a fresh ABI-2 module installs and runs at 46 fps, every
console page answers in tens of ms during playback, demo .pfs plays its 30 s
timeline to the end, and the final cue's [900,550,350,850] shows up as the
live absolute hold on /api/mqtt — the whole bus, wire to PFParams, on real
hardware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found on hardware: after a .pfs played out, its final cue's absolute values
stayed held — those channels' knob deltas were zeroed every frame, so the
knobs read as dead and SELECT browsing (K4's channel) couldn't move. The
MQTT channel flow clears holds with its end-of-show retained sweep, but the
on-device player has no broker and nothing ever released them short of a
reboot.

stop() — every exit path: natural end, /api/shows stop, night/snooze —
now drops all absolute holds. Entering SELECT clears them too: choosing a
pattern by hand is the strongest hands-on signal there is, and browsing
must never fight a pinned channel.

Verified on the panel: boot clean, demo show plays 30 s to the end,
paramActive all false afterwards, pattern still rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…shared floor

The on-device show player and night/wake scheduler come off dev (three
reverts; all of it preserved on fw/performance-director) — @SimonePDA is
still developing that side and will deliver his own. What stays is what any
version of his firmware will stand on and what the web needs regardless:
the PFParams helpers and ABI fields, the MQTT absolute bus and channels,
the converted catalog + Basics pack, the generation pipeline, and deck
performances in pack zips. The changelog now says which parts ship today
(deck performances — inert-but-harmless on current firmware) and which wait
for the performance firmware (everything stamped ABI 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he card

Two things people do without opening a pattern, now doable where they are.

Performances: a recording (Director Save-JSON) publishes onto its pattern
the exact way firmware ports do — anyone may propose one, it is live
immediately and credited, several coexist, and the author's own recording
or their pin decides which represents the pattern (resolution mirrored
from ports.ts, minus stale: wire values keep applying across edits). Rows
travel as summaries; each downloads as editable .json or the packed .pfs
the panel plays, encoded by the same byte-verified encoder deck packs use.
The pattern PATCH grew pinnedPerformanceId beside pinnedHeaderId, deletes
clear a dangling pin, and the author hears about new recordings and pins.
Authoring stays in the Director tool — the site stores and picks. Deck
performances (the bigger stage) already ship in pack zips; when deck-level
proposals arrive they follow this table's shape.

Likes: the card got a heart beside its add-to-deck button, on the same
dodging edge — optimistic toggle, count mirrored into the byline, sign-in
modal through a portal so the card stays one link. Feed pages light the
hearts with one indexed IN query per page (likedPatternIds grew an
optional id filter), and the infinite-scroll API now resolves the session
on every sort so page two arrives lit.

Verified in the browser: signed-out heart → modal, no navigation;
signed-in toggle syncs both surfaces; a recording published end-to-end
lists with IN USE, and its .pfs endpoint answers a structurally valid
PFST v1 (magic/loop/length/cues/pool/title all checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ting

Three things the first cut got wrong.

A performance save has two halves — the .json the Director edits and the
.pfs it sends to a panel — and only the JSON was accepted, which is the
wrong half to demand from someone whose recording just went to their
board. decodePfst reads a table back into a performance: the cue flags
carry which param channels each cue set, so even sparse patches survive,
and both modals now take either file and unpack a .pfs into the editable
timeline. Storage stays canonical JSON (the .pfs is regenerated on
download), so nothing forks. Round-tripped in performance-smoke against
all four of the Director's own demo tables: decode → encode reproduces
the bytes exactly.

The card's ♥ and + were text, and text is placed by font metrics — two
glyphs with different ascents sit at different heights inside identical
boxes, which no amount of line-height tuning fixes honestly. They are SVG
now, centred by viewBox geometry: measured identical centres, and it
holds in any font at any DPI.

Also: the deck page threw on load. Its local performance summariser was
replaced with the shared one and the import never landed — my mistake,
caught by the user on a page tsc is happy with because the symbol existed
in the file it was deleted from.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chip hid 128×64 but nearly every pattern here is authored portrait, so
64×128 — the same board, mounted the other way up — got a chip on every
card in the feed. It said nothing a reader did not already assume.

isStockPanelFrame treats either orientation of the stock panel as the
default, so the chip is back to meaning what it was for: this one is NOT
the panel you have. Checked against the feed (36 cards, zero chips) and
across sizes (64×64, 32×32, 256×128, 128×128 all still chip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publishing this as it stood would have broken installs for everyone. The
descriptor stamp was a build-time constant, so every module the site built
claimed ABI 2 whether or not its source used the absolute bus — and the
committed Basics pack was already rebuilt that way, which means the set
that ships with the board would have been refused by every v3.4.0 device
in the field.

The fix moves the decision to where it belongs. pf_params.h compiles its
absolute tier out when built for the older host, so one converted source
yields two honest modules: at PF_TARGET_ABI=1 it behaves exactly as it did
(audio, then knob deltas) and stamps a descriptor old loaders accept; at 2
the bus is live and an old loader refuses it, which is the point. The
target defaults to 1 because the wrong guess is not symmetric — targeting
old firmware costs a feature nobody is using yet, targeting new firmware
costs installs.

So the catalogue can be converted whenever, in one pass, long before the
fleet updates; the day the absolute firmware is the norm, PF_TARGET_ABI=2
turns the bus on everywhere with no second migration and no second build
system. Basics pack rebuilt at the safe default and is installable again.

Proved on the panel running stock v3.4.0, same converted source both ways:
the ABI-2 build installs and then refuses to load — "module rejected host
ABI or panel size", the explicit error rather than silent corruption — and
the ABI-1 build runs it at 48.6 fps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebuilt around the path a visitor actually takes, which is Instagram to
the site to here. Paik and Participation TV open it, the community
builds and MOTIFLOW carry the proof that it worked, and three ways in
(the device, patterns, the ecosystem) replace the old feature list.
Dropped "The idea" as its own section and moved the manifesto to the
docs line. Emoji and em dashes are gone throughout.

The build map is now stated as hand-built units only, and the breadboard
route is described as what it is: the same electronics without ordering
a custom PCB, not a solder-free build.

Power numbers corrected. Four hours per 10,000 mAh is a typical pattern,
not a worst case, so the "most patterns draw less" claim is out. A
near-full-white screen pulls about 26.6 W (roughly 4.8 A at 5 V, above a
typical USB-A rating) and gets ~5.5 h from a 40,000 mAh bank. Documented
with the connector warning until the pixel-power clamp ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uted

It rewrites the archive on every download of a deck that has a performance,
and no test covered it and no deck had one — so the first person to attach a
recording would have been the first person to run the code, on a route that
serves other people's downloads.

Lifted out of the route handler into decoratePackWithPerformance() so it can
be tested at all, and covered: both forms land, the modules and catalog.txt
come through untouched, the .pfs decodes back to the same show (loop, cues,
a sparse param patch), a deck with no performance gets the identical object
back rather than a pointless repack, and a malformed attachment still leaves
an installable pack.

Then end-to-end against a real deck: three modules, catalog.txt,
performance.json, and deck_ride.pfs — PFST v1, "Deck Ride", 3 cues, 20 s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OSC section had no picture, which is the one part of the readme that
is hard to picture from words. The campaign's Ableton photo shows the
Bridge device open with the four knobs mapped, so it opens that section
now. Resized to 1800px and recompressed, 3.4 MB down to 319 KB.

The community wall picture is now the dense library grid instead of the
page hero. Dozens of patterns by different authors, with a deck being
assembled along the bottom, which is what the surrounding text claims.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pattern-flow-open Ready Ready Preview Aug 15, 2026 7:54am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
pattern-flow_origin Ignored Ignored Aug 15, 2026 7:54am

@engmung
engmung merged commit a1e96ff into main Aug 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant