Skip to content

feat: let apps mark their params changed for the configurator to poll - #691

Merged
ArthurGibert merged 1 commit into
mainfrom
feat/app-param-writeback
Sep 17, 2026
Merged

ArthurGibert merged 1 commit into
mainfrom
feat/app-param-writeback

Conversation

@ArthurGibert

Copy link
Copy Markdown
Member

What

Replaces #640: apps can now change their own parameters (a panel gesture that
picks a value the user could otherwise only set from the configurator), and
the change reaches the configurator without a reconnect.

Why not #640 as written

Full review is on the PR, summarized here. Three blocking problems:

  • No path for installed apps. #640 only added update() to the
    firmware's own ParamStore; the fpapp-sdk mirror never got one, and
    community apps ship exclusively as installed apps. As written, it had no
    possible caller.
  • Breaks an open configurator. The device would push an unsolicited
    AppState message. The configurator has no path for a message it isn't
    waiting for — any frame that arrives between requests gets handed to
    whichever request comes next as that request's reply. useConnectionHealthCheck
    polls GetGlobalConfig every 2s; a push landing in that window gets read
    as the wrong response and disconnects the session.
  • A push is indistinguishable from a reply. It reused AppState, the
    same variant GetAppParams/SetAppParams use, so even a smarter client
    couldn't tell a push apart from an answer to something it just asked.

Design

The device never sends anything unprompted. The configurator asks.

  • ParamStore::update<F>(&self, modifier: F) — mutates, persists to
    flash, and marks the app's channel dirty. No MIDI I/O. On both the
    firmware store and, this time, its fpapp-sdk mirror, so installed apps
    get it too.
  • One dirty bit per channel (APP_PARAM_DIRTY: AtomicU16). A firmware
    app sets its own bit directly; an installed app sets it through a new
    write-only, empty-payload blob_kind::PARAM_DIRTY — the only new item.
    FPAPP_ABI_MINOR 4 -> 5.
  • ConfigMsgIn::GetChangedAppParams — swaps the bitmask to 0, filters to
    channels still in the layout, and answers with only the apps that actually
    changed (usually none). Values are read fresh at answer time, the same
    signal + wait primitives GetAppParams already uses — never a value
    snapshotted when the change happened, so there's no stale-value or
    reused-channel case to get wrong.
  • GetAllAppParams and the new handler share one send_app_param_batch
    helper.
    The "don't announce a count before that many messages exist"
    invariant (FPApps: parameter batch can end short of its announced count, wedging the config connection #688) now lives in one place instead of two copies that could
    drift.
  • Configurator: polls GetChangedAppParams alongside its existing
    connection check, every 2s, and merges results into the params store.
    Deliberately its own try/catch, never allowed to cause a disconnect — an
    older firmware that predates this message must not read as a dropped
    connection.
  • Does not restart the app on a device-driven change (a host-driven
    SetAppParams still does). A gesture already runs inside the app's own
    code; restarting it out from under itself would be wrong.

Also fixed

The configurator never displayed a live update at all, found while
testing this. react-hook-form's fields are uncontrolled — defaultValue
seeds a field once, when it registers, and isn't revisited on a later prop
change. A poll-driven update reached the store correctly but no on-screen
field ever reflected it. Fixed by splitting ActiveApp's params form into
its own component, keyed by the params' content, so it's useForm() itself
that gets recreated when a value changes — not just the DOM under it, and not
the collapsible card around it (which uses native, not React, state, so it
needs to stay outside the remount or every live update closes the card).

Verified on hardware

A throwaway single-param probe app confirmed the wire mechanism first,
isolated from any real app's complexity. Then on Manifold (migrated in a
companion community-apps PR): the gesture reaches the app, the new value
shows up in the configurator within ~2s without the card closing, it survives
a power cycle, and a configurator save of an unrelated param no longer
reverts it.

  • Put an app with a ParamStore::update caller in a layout, trigger its
    gesture, confirm the value updates in the configurator within ~2s
  • Confirm the card stays open while the value updates
  • Power-cycle and confirm the value persisted
  • Save an unrelated param from the configurator, confirm the
    gesture-set value isn't reverted
  • Regression: edit and save a built-in app's params; add a built-in app
    with params to a layout

🤖 Generated with Claude Code

@ArthurGibert
ArthurGibert merged commit 5485122 into main Sep 17, 2026
4 checks passed
ArthurGibert added a commit to ATOVproject/faderpunk-community-apps that referenced this pull request Sep 17, 2026
Both apps now call ParamStore::update() from their Mode/Range panel gesture instead of keeping a private working copy outside run(). Replaces the workaround from #59, whose two limitations are gone: a panel edit is now saved, and a Configurator save of another parameter no longer reverts it. Needs ATOVproject/faderpunk#691.
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