Skip to content

[MERGED STACK] Single source-of-truth#167

Merged
sastraxi merged 33 commits into
TreeFallSound:pistomp-v3from
sastraxi:feat/ws-reconnect-reseed
Jun 10, 2026
Merged

[MERGED STACK] Single source-of-truth#167
sastraxi merged 33 commits into
TreeFallSound:pistomp-v3from
sastraxi:feat/ws-reconnect-reseed

Conversation

@sastraxi

@sastraxi sastraxi commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Collapse pi-Stomp's dual ownership of live state into a single source of truth — mod-ui — and make the WebSocket the read-side that keeps piStomp's mirror current in real time.

Previously, we held bypass/parameter state locally and re-derived it by polling REST after every change. Two writers, racing, reconciled by timing. This stack removes pi-Stomp as a writer of live values: it emits intent over the WS, and treats mod-ui's echo as the authoritative update. We're a controller: command, then mirror what the server actually did.

Sources of truth (LLM-generated)

Concretely, the websocket handler only acts on three things:

  1. Plugin bypass → plugin.set_bypass() (PluginBypassMessage / AddPluginMessage, line 374)
  2. Control-port param values → param.value (ParamSetMessage, line 388)
  3. Snapshot/preset index → current.preset_index (PedalSnapshotMessage / LoadingEndMessage)

Everything else is either dropped or flows through a different channel:

  • Pedalboard structure — which plugins exist, routing/connections, plugin add/remove — is not mirrored. Critically, AddPluginMessage does not add a plugin; it loops existing current.pedalboard.plugins and only updates bypass on a match (modhandler.py:373). A plugin added in MOD-UI is invisible to piStomp until a full pedalboard reload (last.json mtime → LILV re-parse). The WS carries values, the file-watch carries topology.
  • Pedalboard identity / selection — driven entirely by the last.json file watch, not WS.
  • Parameter metadata (min/max/type/labels) — from the LILV TTL parse, never the WS.
  • Parsed-but-inert messages: SizeMessage, AddHwPortMessage/RemoveHwPortMessage (JACK ports), TrueBypassMessage, LoadingStartMessage — all parsed in ws_protocol.py but have no arm in the dispatch. They're decoded and discarded.
  • Audio meters / output_set — dropped at the bridge before queueing (websocket_bridge.py:172).
  • Tempo/BPM — piStomp sends transport-bpm but there's no inbound parse for it, so the displayed tempo isn't mirrored back.
  • Banks — separate banks.json mtime poll.

A deeper dive

  1. Read order is critical: WS must be drained before the file-watch path each tick (loading_end/snapshot must land first) — see the ordering comments in poll_modui_changes.
  2. pi-Stomp's own sends come back as echoes ~10ms later. Anything that previously assumed a local write took effect immediately now depends on the round-trip. Check that emit sites don't also mutate local state (that would resurrect the dual-writer bug the stack exists to kill).
  3. MIDI-driven mod-ui mutations (like footswitches), though they do not use WS to apply their changes, wait for the broadcast state to come back as well: mod-ui remains the single-source-of-truth.
  4. Mono LCD (lcd128x64/lcdgfx) gains self.plugins tracking so refresh_plugins() can redraw bypass indicators on inbound updates — the v1/v2 equivalent of the v3 LCD's existing path.

The payoff is correctness with minimal impact to responsiveness: indicators update from any source (footswitch, MOD-UI, snapshot, external) in ~10ms with no polling, and no state divergence. In practice, the WS bridge has been stable in production. This PR series depends on that continuing to be the case. If a disconnect does happen, we automatically reconnect with exponential backoff. sastraxi#36 documents a subtle correctness concern that probably doesn't matter in practice.

Individual PRs

This PR is a merged stack of all the PRs below. 99% of the UX is solved by the first, as it's very hard to hit footswitches fast enough to trigger the "echo" bug at 10ms. The rest simplify the architecture.

sastraxi and others added 30 commits June 6, 2026 02:16
Makes WS-driven bypass changes (snapshot/web/MIDI) visible on v1/v2.
refresh_plugins only repushed zone bitmaps; the bypass line is now
redrawn from a tracked plugin list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mod-ui broadcasts param_set :bypass during snapshot_load, drained on the
10ms tick, so the per-plugin pi_stomp_get poll in preset_change_plugin_update
is redundant. Removed it from the snapshot path (modhandler + mod); pedalboard
load still seeds bypass from the parsed TTL. Authoritative reseed (reconnect
add-dump) is branch 5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	pistomp/handler.py

@rreichenbach rreichenbach left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems great. I love seeing MOD-UI and the LCD in perfect sync. Finally!!!

I tested:

  • Snapshot and Pedalboard changes via MOD-UI, Nav Encoder and footswitches
  • Plugin Bypass via MOD-UI, footswitches and clicking the encoder block
  • MIDI USB mapping, bypass
  • DIN MIDI mapping, bypass
  • Expression pedal mapping
  • Tweak encoder changes value on MOD-UI (Finally!)

Everything I tested seems rock solid. This Rocks!

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.

2 participants