Target: HX Stomp (VID 0x0E41 / PID 0x4246). Goal: full GUI editor on Linux. Strategy: recover the MI_00 USB control protocol by observing traffic to and from the device; read the model/preset/control data from the user's own installed copy at runtime (nothing is redistributed); build a libusb transport + greenfield GUI.
Implementation language: Rust. USB via nusb (pure-Rust, cross-platform, async) — no
libusb C dependency, clean on Linux; falls back fine for dev on Windows. Workspace layout:
| crate | role |
|---|---|
fretwire-data |
parse shipped JSON (.models, catalog, controls, .hlx presets) into typed structs |
fretwire-protocol |
MI_00 wire message types + encode/decode codec (filled in during Phase 2/3) |
fretwire-usb |
nusb transport: enumerate, claim MI_00, bulk/interrupt I/O |
fretwire-core |
device session API (connect, sync, param set, preset load/save, snapshots, tuner) |
fretwire-cli |
command-line validator/driver |
fretwire-tauri |
GUI — Tauri 2 (WebKitGTK) + Svelte |
- Locate the device; set up USBPcap + Wireshark.
- Inventory
res/— found complete model/preset/UI data as JSON/XML. - Identify device VID/PID and the MI_00 vendor control interface.
- Set up project dir + notes.
- Scaffold the Rust workspace (5 crates) — compiles clean.
-
fretwire-data: parse every shipped.models+.hlx; presets round-trip losslessly (3 tests green). -
fretwire-usb:nusbenumeration works —fretwire detectreports the unit present.
- Determine which USBPcap root hub the HX Stomp is on; build a capture filter.
- Establish a repeatable capture procedure (start cap → do ONE known action → stop).
- Capture the startup handshake (launch HX Edit with device connected).
- Capture a set of labeled single-action sessions: - one parameter tweak (e.g. amp Drive) — note before/after value - block on/off (bypass) - model swap in one block - preset change (select another preset) - snapshot change - tuner on/off - receive a full preset (open editor on a preset) - send/save a preset to the device
- Store each as
captures/NN-<action>.pcapng+ a.mddescribing the exact action.
Mostly answered without the captures (2026-08-22). This list asked for five footswitch and
controller assign captures. Four were settled by construction instead — op 56 assigns and
unassigns a block's bypass to a switch, op 37 assigns a parameter to a source ordinal, both
byte-exact and verified live, and the CLI has assign-bypass / unassign-bypass / assign-param.
Worth remembering next time a list like this reads as capture-blocked.
What is left of it:
assign_param_to_exp1.pcapng— one parameter assigned to EXP1. Source ordinals 3..=7 are the footswitches; 1 and 2 are believed to be the two expression inputs and that is [unverified] for want of an expression pedal to try it with. This is the only one of the five still worth a capture, and it needs a pedal plugged in more than it needs Windows.- A layout reorder (a bound block moved between switches) is separate from binding and has no op of its own yet; whether the device treats it as unassign-plus-assign is unchecked.
Still wanted for the footswitch ring colour / custom label write (ops 58-62, the one area where
guessing has already cost a power cycle): see section B of captures/_RUNBOOK-hx-edit-session.md.
- Identify endpoints — interrupt EP 0x01 OUT / 0x81 IN, 16-byte base frames, addr 8.
- Reusable extractor
tools/dump-control.ps1; living specdocs/protocol.md. - First framing pass: 3 channels w/ swapping ids + per-channel seq; edit channel
ed03/8010; inner opcode 0x0006 + u32 length prefix; reverb-block handle83 66 cd 03. - Disambiguate bypass: differing bytes were a transaction counter → bypass is toggle-class.
- Handle != block address; block id is the 3rd byte of
8X 62 [id] NN(reverb 07, tremolo 04). - Parameter values = big-endian f32 (Mix 100%→
3f800000, 0%→00000000). - Op class byte after
83 66 cd:03toggle,04set-value. - Analyze
startup.pcapng— HX Edit opens multiple channels (ef03/ed03/f003) each with SESSION_OPEN resource ids; the handshake is byte-stable across runs. - Transport confirmed bulk, not interrupt.
argfield (bytes 12–15) is a u32 offset, not a checksum → likely no per-packet checksum. - Handle discovery resolved: opening a preset streams full state as MessagePack on
the edit channel (cmd 0x04→0x0c→0x08, 272-byte chunks). Block/param handles come from there
— no need to compute them. Blob saved:
captures/preset1_stream.msgpack.bin. Seedocs/protocol.md. - Parse the preset stream with
rmpv—fretwire_data::stream::PresetStreamdecodes envelope →l6-helixblob → integer-keyed preset map (device info, 20 block slots, paths, snapshots). Tests +docs/preset-format.md. (codec also fixed:lenis u16, handles 272-byte chunks.) - Typed device-preset model (
PresetStream::{device_model, firmware, blocks, path_blocks}) + device blocks resolve to.modelsdefs by name (4/4 in test preset).docs/preset-format.md. - Path↔slot pairing verified (path key 11→8 = slot index); param vector aligns with
.modelsparam order (confirmed by default matches). Reading params by (block, index) works end-to-end. - Model identity →
symbolicID: there is no numeric model id (path 11→6 = category, slot 24→25 = runtime handle; neither indexes the 681-modelHelixModelDefs.bin). Canonical id issymbolicID(unique 681/681); resolve a block by name(+category) viaModelDefs::resolve. Effect blocks unambiguous by name; amp/cab variants need category, which is the only undecoded bit (path 11→6 → category; needs more presets). Seedocs/preset-format.md, tests/correlate_modelid.rs. - Edit body is MessagePack (
fretwire_protocol::edit::EditBody):{102: counter, 100: op, 101: {98: slot, 28: param_index, 119: value}}. Block slot = key 98; bypass = bool at key 59; param selected by its index (key 28) in the model'sHelix.symorder → editing is computable from shipped data (verified 4 models/6 params,captures/param_map_findings.md). Buildersedit::bypass/set_valuegenerate byte-exact commands. Switch/transport params (key 28=0) TBD.
-
fretwire-protocolcodec built + tested:Frameencode/decode (exact bytes),Tlvbody, BE-f32 value helpers, channel/cmd/op constants. 7 golden tests against real captured frames (round-trip byte-exact) + byte-exact generation of the validated 5-packet handshake. - libusb prototype: claim MI_00, replay the handshake, read a reply.
- Implement parameter read/write; verify against the physical unit.
- Implement preset get/set; round-trip a
.hlx.
- Data layer: parse
.models/ catalog / controls / presets + device preset stream into a state model (fretwire-data). - [~] Editor model (
fretwire_core::editor): preset stream → typedEditorPreset/EditorBlockwith resolved model id + category, device-ordered named params + values, and byte-exact edit command generation (bypass). Live session (connect/sync overfretwire-usb, param read/write, preset get/set, snapshots, tuner) still to come. - Graceful disconnect /
Session::close()— DONE (verified live 2026-06-25). Connecting put the device into host-owned "edit mode" (front-panel page/preset arrows dead); dropping the interface — even a USB port reset — did not release it (it's firmware RAM, not USB state). The fix: send HX Edit's shutdown session-close (cmd 0x02, empty body) on each channel (status → edit → primary), request/response with a ~150 ms settle before releasing the interface (firing blind doesn't work). Runs onDropso even Ctrl-C cleans up;fretwire disconnectexercises it. primary never acks (handshake diverges there) and the panel releases regardless, so the ack wait is capped (300 ms) to keep teardown fast. Decoded fromlaunch_hx_*_close*.pcapng. - CLI for validation (the GUI rides on top of this).
- [~] DSP-fit / model availability. HX Edit greys out models that won't fit the remaining DSP.
(1) per-model cost — SOLVED: the
.modelsfiles carryload(mono) andload_stereo(% of DSP budget); range 0.35–40.0 (amps ~28%, poly pitch/sustain cap at 40). Wired intoCatalog(bundled_loads) →EditorBlock.dsp_load(incl. paired cab, by Mono/Stereo variant) →EditorPreset.dsp_load;pullshows "DSP X% used". (3) current usage — SOLVED: sum of block loads (computed locally — factory preset reads 71.8%, self-consistent). (2) budget — likely 100% per DSP (the 40-cap + busy-preset sums fit); still to confirm the exact HX Stomp cap and validate our % against HX Edit's meter on the same preset. Availability: thedevicesfield (per-model device-id+firmware list;None= universal) gates which models a given unit even offers — but on the HX Stomp this is moot: the Stomp runs the full HX model library; its only constraint is DSP + the 8-block cap, so HX Edit's grey-out is DSP-fit driven, which we've built. Thedevicesfield is cross-product/firmware versioning (Helix Native + old-firmware gating), not a Stomp model restriction.swapwarns when the projected load exceedseditor::DSP_BUDGET(~100%) viaCatalog::model_load_by_index. Live probe (2026-06-25): the handshake identity reply carries the model string"P33Main"+ ~12 trailing bytes (serial/HW-rev/firmware/flags, undecoded) — no0x0021xxxxdevice id, so thedevicesids can't be matched from the wire either. Only loose end: confirm the budget value vs HX Edit's own DSP meter (our two-amp preset reads 73.5% — does HX Edit agree?). Treat this thread as essentially resolved otherwise.
-
Toolkit chosen: iced 0.13 (tiny-skia software renderer).
-
Connect/disconnect, preset list + switching, current-preset identity (op 23).
-
Signal-chain view (boxes + wires, serial/parallel rows), click-to-select.
-
Live bypass + param sliders (release-to-commit), DSP meter.
-
Model picker with category selector + DSP-fit grey-out + live swap (same- and cross-category); split-type rides
swap_model. -
Snapshots (switch), save-to-device (two-click confirm), ⟳ Refresh.
-
Model icons (2026-08-21): every block and picker row draws the hardware it models, as a generated SVG silhouette (
ui/src/lib/icons/). Cabs derive their speaker grid from the driver array in the name; amps match on symbolic-id prefix; unlisted models fall back to their effect family, then the category. Follow-up: ~30 models are placeholders where the original wasn't identified — see the "Known guesses" table indocs/icons.md, correct opportunistically (one line each inmodels.js). Also open: the picker's category<select>is text-only (a native option can't hold an icon). -
Cab mic view (2026-08-22): a cab's params are drawn — speaker in cross-section, the mic at its distance/position/angle, ticked
Edge/Cap edge/Center. Drag or arrow-key the mic to set Distance and Position. Mic silhouettes generated fromicons/mics.js; see the "cab mic view" section ofdocs/icons.md. Follow-ups: (a) the radial scale assumes Position is linear in radius and pins "Cap edge" to Position's default — neither is stated by the reference data; (b) a legacy cab (category 2) reaches its mic through the extras table and the meta lookup misses it —cab.modelsnames it@micwhilename_paramslooks it up asMic— so it renders read-only with no mic names and the view falls back to a generic silhouette. Untested either way: no legacy cab appears in any capture we hold. -
Live-follow of panel changes (footswitch bypass / snapshot / preset) via the status-channel state-push (
Session::poll_events). -
Move (op 43) + add (op 39) block — protocol + CLI verified live.
-
Drag-and-drop to reorder (serial chain) — DONE (2026-06-28): drop a block into a gap (insert, not replace). op 43 only relocates into an empty slot, so a reorder bubbles the block through a spare empty slot via single moves, each preceded by op 78 begin-structural (
edit::begin_structural, byte-exact). Pureplan_reorderplanner (unit-tested) +Session::reorder_block; GUI drop-into-gap with gap highlight. Serial presets only (errors on split). Verified live. Next: parallel-path drag (Step 2, needs op-21 / split-node handling). -
Add-block — DONE (2026-06-29): "+ Add block" picker appends a model via the surgical op-39 (
Session::add_block_append, FS-safe); drag it into place. Verified live. -
Move to/from parallel (B) row — DONE (2026-06-29): header button moves a block between series/parallel via
move_blockto a row-B slot (Session::move_block_to_row), creating/ retiring the split; row-B derivation fixed (split-node divider). Verified live. -
Cross-row drag — drop a block directly onto row B (and reorder within B), reusing the gap-drag infra now that row B is modeled. Polish on the move-to-row buttons above.
-
Delete block — DECODED (2026-06-30): op 28
{98:slot}is a surgical delete (HX Edit optionally prefixes op 78; we mirror it) that preserves the footswitch layout — the old op-21 approach that wiped FS is no longer used.edit::delete_block,Session::delete_block, CLIdelete-block, GUI ✕ Delete on the selected block. Byte-exact-tested. Pending live test. -
Split/combine nodes & routing — DECODED (2026-06-30): the split node's type is a
swap_model(op 40) on the split slot to Helix.sym 256 (A/B) / 258 (Crossover) / 563 (Dynamic) —cycle_through_split_types.pcapng; the split's own params and the mixer/join node's A/B level/pan/polarity params (model 151HD2_AppDSPFlowJoin) are ordinary set-values on the node's slot (10 / 19) —adjust_A_B_level_and_pan_of_join.pcapng. Surgical/FS-safe.EditorPreset. {split_node,mixer_node}+PresetStream::structural_node,editor::SPLIT_TYPES,Session::set_split_type, CLIsplit-type, GUI: split/mixer render as selectable chips in the chain (bracketing path B), selecting one edits its type/params in the normal param panel. Lane decode[solid]: bottom slots (11–18) = path B; the node's holder key13= its signal-flow column, so a top block at slotsis common-before (s<split_pos), path A (split_pos≤s<mixer_pos), or common-after (s≥mixer_pos). Fixturessplit_preset_stream,dual_amp_stream. Verified live. -
[~] Split-preset drag-routing — position-aware cross-row/same-row moves with slot bubbling (
plan_row_insertright-anchor for B,plan_insert_right_endleft-anchor for common-before), an op-43 overwrite guard (apply_row_movesrefuses moving onto an occupied slot — this had deleted a block), CLImove-to-row/before-split. Works for most regions but the linear chain-with-gaps model is the wrong abstraction for 2D routing: each region needs bespoke placement and some positions (e.g. end of path A, before the mixer) have no gap/slot. NEXT: rewrite the chain view as the device's real 2-row × 8-col grid (top slot = column, bottom slot = column + 9; split/mixer are derived markers). Every cell → one exact slot, one placement path, complete drop coverage — matches HX Edit and the hardware. This replaces the 5 special-case move functions. Superseded 2026-07-05 by the Tauri migration (below): the grid will be rebuilt in the webview, where SVG/CSS handle the wires+branches natively. -
[~] GUI renderer → migrating to Tauri (WebKitGTK webview) — DECIDED 2026-07-05 after a spike. Why: iced is stuck on the tiny-skia software renderer (wgpu is ruled out by EGL/dmabuf driver issues on this box), and tiny-skia can't stroke paths — so the routing UI can't draw wires/branches, the whole reason the chain uses plain widgets. The routing grid is inherently a drawing problem iced can't solve here. Spike (
crates/fretwire-tauri): a minimal Tauri 2 app (static HTML frontend, no bundler) reusingfretwire-coreunchanged via two#[command]s (detect,pull). Result — thetauri/webkit2gtk/taotree links against system WebKitGTK 4.1; the default dmabuf renderer hits the same fatal Wayland path as wgpu, butWEBKIT_DISABLE_DMABUF_RENDERER=1(now baked intomain()) runs stably — the fallback wgpu never had. The webview renders SVG stroked wires + the split/rejoin branch cleanly (verified live 2026-07-05), laid out on the real 2-row × N-column grid (split/mixer get their own columns). The Rust core (protocol/transport/decode/Session/Catalog) is untouched and re-exposed as Tauri commands. NEXT: incremental port of the ~1900-line iced GUI to the webview — routing grid first (the motivation), then the param/model/save/rename panels.fretwire-tauriis a workspace member but excluded fromdefault-memberssocargo test/buildstay off the WebKitGTK tree. Progress: - [x] Svelte + Vite frontend scaffold; SVG chain render on the real 2-row × N-col grid. - [x] Command layer (commands.rs) over the fullSessionsurface + serde DTOs (dto.rs); session held in managed state (async +spawn_blocking; clean teardown on window close). - [x] Block/node selection + live param editing (bypass, sliders, enum dropdowns, on/off switches, paired cab params). Verified live. - [x] Model picker (swap) + add/delete block. - [x] Preset browser (list/switch) + save/Save-As/rename; snapshot switcher; split-type dropdown. - [x] Keepalive heartbeat + live-follow (footswitch bypass / panel snapshot+preset changes pushed to the UI via adevice-pushesevent; needs thecore:eventcapability). Bypassed blocks render greyed in the chain. - [x] Interactive routing grid — the chain is a 2-row × N-col grid of draggable HTML cells (SVG wires behind); every slot is a drop target.PresetStream::grid()+place_block(one guarded op-43 to an exact slot; device recomputes split/mixer). Fixed-slot model, so drops target visible empty slots. Tauri GUI is now at feature parity with iced. - [x] Serial→split creation via the grid — DONE (2026-07-06, verified live): the split/ mixer node slots exist even on serial presets [solid, preset1 fixture], so the empty B row is revealed while a drag is in flight (ghost bracket hint); oneplace_blockinto it and the device activates the split; last B block dragged back retires it. - [x] Movable split/join nodes — DONE (2026-07-06, verified live): drag ⋔/⋉ to a valid gap (drop zones show only the legal range). No surgical op exists — the node holder's key 13 is written via the op-21 whole-preset write (PresetStream::set_node_pos+Session::set_node_pos, guarded: bracket must enclose the occupied B row, split < mixer). First live op-21 write of a mutated blob — device honors it verbatim. - [x] Insert-on-occupied-drop — DONE (2026-07-06, verified live): dropping a block onto another inserts it before/after (by which half of the cell you drop on — glowing insertion bar), shifting neighbors. Same-row =plan_reorderbubble through a scratch slot; cross-row =plan_row_insertsuffix shift.Session::insert_block+insert_blockcommand. (Swap semantics were built first, then replaced per user feedback — insert matches HX Edit.) Off-by-one in the pos→final-index mapping caught by the mock smoke test and pinned withinsert_pos_tests. - [x] Trim trailing empty columns — DONE (2026-07-06): at rest the grid ends one spare column past the last block (kept through the mixer column when split); every column reveals while a drag is in flight. -
Multiple split points — still future.
-
Cab/IR param editing — DECODED (2026-06-28): paired-cab params use sub-model selector
26:1(main =26:0); index is positional in the cab namespace.edit::{set_paired_value, set_value_on},Session::set_paired_param, CLIset-cab, GUI cab grid live-editable (float knobs). Seecaptures/_TODO-cab-params.md. Pending live test. -
Enum param dropdowns (incl. cab mic-select) — DONE (2026-06-28):
valueType:0params get apick_listof their labels (fromHelixControls.json[displayType].format,isDiscrete); the selected index is sent as an int viaSession::set_param_enum/edit::set_value_on. Generic — works for any discrete enum, not just mics. Pending live test. -
Absolute chain positions — DONE by the Tauri routing grid (every cell = one exact slot, empty slots visible and droppable/clickable-to-add). Superseded the 2026-06-29 note.
-
Undo/redo — DONE (2026-07-06): exactly the op-21 blob-snapshot design — the command layer brackets every edit with
edit_begin(label)/edit_commit(), snapshots come from the read cache (no extra USB), undo/redo write the prior blob back (edit buffer only). Header buttons + Ctrl+Z/Ctrl+Shift+Z/Ctrl+Y. -
Scrollable edit history with A/B compare — DONE (2026-07-06): the undo stacks grew into a labeled timeline with a cursor (
Session::history_jump= op-21 write of any entry; labels name real blocks/params, e.g. "Set Drive — Amp A"). HistoryPane: collapsible list, click to jump, mark two entries A/B, toggle between them by ear. Seeded with the "Loaded" state. -
Input/output node editing — DONE (2026-07-06): slots 0/9 (gate/threshold/decay, level/pan) decoded [solid — io fixtures + input-gate capture: plain op-30 on the node slot]; io.models meta bundled; IN/OUT glyphs in the grid open the param panel. Global settings (Input Z/impedance, pad, output level switches) still need a capture round — see
docs/protocol.md. -
2026-07-08 editor round (mock verified; cab paths live-verified 2026-07-09): segmented floats (cab mic Angle → 0°/45° buttons,
ParamMeta::stopsfromHelixControls.jsonscale), Change cab on amp+cab combos ([solid]: same-model op-40 swap keeps amp params, new cab gets factory defaults), Amp+Cab picker category (synthetic id 100,amp.modelsircablinkdefaults), dirty/edited indicator (Session::saved_cursor), snapshot rename (op 89, double-click the tab, undoable), Spacebar bypass, live-follow bypass overlay fix. Root-caused live: wire key 23 = paired-model-active flag — was hardcoded false, so paired swaps/adds stored the cab index without instantiating the cab; builders now mirrorpaired_index >= 0[solid]. Plus smooth param ramping:preview_param/preview_paired_paramstream mid-drag values (no history/re-read); commit on release unchanged.delete_cab.pcapngdecoded → op 28{98:slot}= remove cab (not yet exposed in the GUI). -
Knob widget option, keyboard nav (beyond Space/Ctrl+Z), polish.
-
A cleared footswitch label no longer comes back (2026-08-22): key
14keeps the last string written and key13is the has-label flag; we read 14 alone, so a Simple Delay bound to FS2 displayed as"Tremolo"— a name a different block had held on that switch. Found by smoke-testing the newassign-bypass. -
Show footswitch bindings — DONE (2026-08-21): every block already carried
footswitch(preset key3 → 8, layout position + 1,0= unbound) all the way to the DTO, and the GUI dropped it on the floor. Chain cells and the param panel now show anFS<n>badge. Read-only. -
Assign a block's bypass to a footswitch — DONE (2026-08-22, verified live). It needed no captures in the end and no op-21 rewrite: op 56
{98: slot, 102: switch}binds it and op 57 unbinds, both zero-based, both surgical. Sent on a preset with nothing bound, op 56 added exactly one entry at3 → 8[0]and op 57 restored the document byte-for-byte. The opcodes came fromtonepush's macOS capture; the verification is ours.edit::{assign_bypass_to_switch, unassign_bypass_from_switch},Sessionmethods of the same name, CLIassign-bypass/unassign-bypass. -
Parameter controllers — reading (EXP pedal / a switch driving a param — preset key
4). Unblocked 2026-08-21. The diff experiment was run on a Stomp: assign a param to FS1, then a second to FS2, and diff the document each time. Key4is indexed by source ordinal (FS1 = 3, FS2 = 4;tonepushputs EXP1 at 1), the parameter index is6 → 29not6 → 28, and the travel is keys2/3not4/7— all three were being read wrong, so every assignment reported "param 0, 0 → 0". Fixed inPresetStream::assignments, pinned byfretwire-data/tests/assignments.rs, written up indocs/preset-format.md.pullnow printsFS1 -> slot 16 param 0 [0 -> 8]. -
Parameter controllers — writing — DONE (2026-08-22, verified live). Op 37
{98: slot, 26: paired, 28: param, 29: true, 74: source, 71: 4, 129: false}puts a parameter under a controller, and the same op with74: 0removes it — there is no separate unassign. Ops 65/66 move the Min/Max ends, in the parameter's own units. AssigningMixto FS1 landed the entry at/4[3], confirming the source-ordinal indexing a second time and by a different route.edit::{assign_param, set_assign_travel}, CLIassign-param/assign-travel. -
Reading a footswitch and an assignment from the device — op 33
{102: switch}(one-based in, zero-based out) answers what a switch carries, its label, LED colour and latching type; op 36 answers one parameter's assignment, or104: nil. Both verified live 2026-08-22. Cross-checks rather than new capability — the document already carries both — but op 36's reply is byte-identical to the document's own entry, which makes it a cheap way to confirm a write landed. -
Assignments in the GUI — DONE (2026-08-22). The two mechanisms get two controls, because confusing them is the whole trap: the block header's
FSbadge became a picker for which footswitch toggles that block's bypass (one select, since re-sending op 56 moves a binding), and every parameter row grew a quiet⇢that opens a Controlled by source picker with Min/Max travel sliders in the parameter's own units.PresetDtocarriesassignments(source, resolved parameter name, travel) andfootswitch_count; four commands wrap the session methods. The source list is built fromfootswitch_count, which comes off the preset's own layout, so a Floor will offer its own number without the UI being told about Floors. MIDI is left out — it needs a CC number, which is a separate opcode. Checked rather than assumed: these use the ordinary immediate re-read, notread_preset_settled— assign, remove and unassign all read back correctly on the next read, three rounds in a row, so the ACK-before-rewrite hazard that model swaps have does not apply. -
Parameter controllers — what is left. - Confirm EXP1 = 1 and the ordinals past FS2. Needs an expression pedal; a Stomp's three switches leave most of the ID space unsampled, so a Helix Floor (8 switches, 2 pedals) remains the better instrument for the full map. - Decide key
1positively. "4 a parameter, 0 a bypass" stays refuted.tonepushreads it as the MIDI CC number, a constant 4 under any source with no CC to give, and the op-37 write agrees (assigning to FS1 stored1: 4) — but that is corroboration, not proof, and telling it apart from a "value type" reading needs a MIDI-sourced sample, which a Stomp cannot make alone. - Ops 58-62 (momentary/latching, custom switch label, LED colour) and op 64 (a parameter's MIDI CC) are documented bytonepushand untried here. Not needed for the assignment itself. -
Tempo-sync as one control (issue #5) — HX Edit and the pedal both fold
TempoSync{n}/SyncSelect{n}into the time knob: switch sync on and the knob becomes a note-value selector (1/4,1/8 Dotted, …). We list all three as separate rows instead. Everything needed to render it is already in hand —sync_noteis a discrete control with its 19 labels, and the dropdown works today. Blocked on evidence, not effort: nothing in the shipped data says which param a sync pair governs. Checked and refuted 2026-08-21 — position doesn't encode it (Levelimmediately precedesSyncSelect1in 57 models), andassignis amp-knob ordering, not this (Dual Delay assigns 3/4/5/6 against syncs 1/2; several sync-bearing models have noassignat all). 107 models carry a sync pair and 14 carry two, so a name heuristic would be guessing on ~14 models where guessing wrong silently reassigns a control. Look inHelixModelDefs.binorHX_ModelCatalog.jsonfor a stated grouping before writing UI. Available now without any of that: hideNote SyncwhileTempo Syncis off — that pairing is unambiguous, being the same ordinal. (The note values were off by one until 2026-08-21 — a discrete control's labels span the param'smin..=max, andsync_notestarts at 1. Fixed for every enum, issue #8; see STATUS "thirty-first round". Unrelated to the grouping question above.)
- Setlist export — BUILT (2026-07-07; multi-setlist, cancellable and renamed 2026-08-20;
live-verified on an HX Stomp).
Session::export_setlistswalks each requested setlist (goto + raw read per slot, op-23 identity cross-check viaread_preset_confirmed, cursor restored) into afretwire-backupJSON file (fretwire_core::backupv2, hex-encoded raw streams). Reads only. CLIexport-setlist [--bank N|--all](aliasbackup) /backup-show; GUI Export presets… under the sidebar's ⋯ menu, with a scope choice, a whole-job progress bar and a Cancel — a Floor's eight setlists is 1024 presets and the better part of an hour, and a cancelled sweep still writes what it read. (Stores our own format, not.hlx— the wire↔.hlxkey mapping isn't needed for round-tripping.) Deliberately not called a backup: see "Full device backup" below. - Restore / preset write — BUILT (2026-07-07; bank-aware 2026-08-20), VERIFIED LIVE
2026-08-26: export →
restoreto the same slot on an HX Stomp; the op-4 flash read-back matches the written document. The first attempt found a real, deterministic bug: an op-21 write straight aftergotostalls at its first chunk boundary (same offset twice running) because no read has re-opened the edit buffer after the select — every previously-proven op-21 write had run after a read.restore_presetnow reads between the goto and the write; seedocs/protocol.md"A write straight after agotostalls deterministically". CLIrestore <file> <index> [slot] [--bank N]; GUI Restore… with source + target pickers (overwrite always visible). Also gives duplicate/copy. - Fast setlist export (2026-08-21).
export-setlistused togotoeach slot and read it back — loading, settling and confirming 128 presets, which took tens of minutes and walked the user's pedal through every one of them. Op 4 reads a slot's document in place, byte-identical to the loaded read and without moving the panel: 126 presets in 10.7 s on a Stomp. Falls back to the old sweep if the device refuses op 4 (untried on a Floor), and for the odd slot that answers104: nil. Seedocs/protocol.md. - Full device backup — the thing "Backup" used to imply and does not deliver. A restore that
makes a wiped pedal whole needs three parts, and we have one:
presets (done — setlist export above), global / I/O settings (op 25's id space is
barely mapped — see below), and IRs (op 9/12 transaction only partly decoded — see below).
Gated on those two, in that order; the naming stays honest until all three land, because a file
called a backup gets trusted as one.
fretwire_data::hxbalready reads HX Edit's own.hxb, which is the reference for what a real one contains — and a plausible import path once thetoneJSON → wire blob conversion exists. > Done (2026-08-22). Decoding op 25 bought one small thing early: preset numbering. > Whether the pedal writes01Aor000is a global, and it is confirmed absent from every > stream we already read — flipping it on a live Stomp left the browse listing and the > preset stream byte-identical (2026-08-21, [solid]). The GUI's manual toggle is now a view of > setting 27 rather than a guess beside it: op 24 supplies the form at connect and op 25 > writes it when the toggle is used. - IR management — read and write (2026-08-22, verified live). The one device capability
HX Edit had entirely to itself, and the reason a Linux user still needed a Windows box.
Session::{ir_info,ir_directory,ir_export,ir_upload}; CLIir-list,ir-info,ir-export,ir-export-all,ir-upload; builders byte-exact againstcaptures/{import,export}_ir.pcapng. An IR round-trips bit-exact — a blob read off the pedal matches the one the June capture recorded HX Edit uploading, and a slot written back from that file matches again. The113checksum (a little-endian word sum, not a CRC) was solved 2026-07-22 and this line went on saying it was the blocker; it was not. Seedocs/protocol.md"The user IR store". - IR delete and rename (2026-08-22, verified live) — op 15
{112:slot}empties a slot (afterwards it reads field-for-field like one never written), op 10{112:slot, 109:name}renames. Both fromtonepush'sPROTOCOL.md, not from a capture. - IR management — what is left. Reorder is undecoded and may not exist as an opcode (delete + upload expresses it). Also unfinished: how a preset's IR block references a user slot vs a built-in cab IR.
- GUI IR panel (2026-08-22) — toolbar IRs… opens an overlay: per-slot export/rename/
delete, upload with a native picker and a target-slot picker that says what each slot holds,
an optional empty-slot view (128 requests vs the directory's one), and confirmations that name
what is lost. Slots are shown one-based, as the pedal's own menus number them. Mock backend +
npm testcontract check; not yet clicked through by hand. - [~] Global / I/O settings — the read side is decoded (2026-08-22, live): op 24
{118:id}answers with the value at key119, and 166 of ids 0..=260 answer on a Stomp. Named: 16 tempo BPM (f32), 28 current preset index, 192/201-203 global EQ. Settings are typed and a wrong-typed write is refused-3, soset_setting_numreads before writing. Op 24 was already in the tree misnamedOP_READ_PREP— the handshake had been calling it since day one. Mapping the rest needs no capture:settings-dump, change one thing on the pedal,settings-dumpagain,settings-diff. Verified — a tempo move showed up as exactly one id out of 166. Priorities: Input Z, guitar pad, main out level, and the preset-numbering flag. - Save As (GUI, 2026-06-29) — write the edit buffer to a chosen slot under a new name (op 71); sidebar slot-pick + overwrite confirm. Verified live.
- Preset rename (name-only) — DECODED (2026-06-30): op 6
{107:bank,108:slot,109:name\0}on the primary channel. Unlike save (op 71) it does not commit the edit buffer — the capture (change_amp_drive_rename_..._name_sticks_change_doesnt) proved a pending param edit didn't persist.edit::rename_preset,Session::rename_preset, CLIrename, GUI Rename… field (no confirm, HX Edit semantics). Byte-exact-tested. Pending live test. - Copy/paste/duplicate blocks (read a block's content,
add_block+set_values). -
.hxbbackup reading (2026-07-26) —fretwire_data::hxbparses HX Edit's own backup container (AF6L header + concatenated raw zlib streams): globals, 128 IR slots, the model-usage table and the 8 setlists. CLIshow-backup <file.hxb> [--presets]. Reading only — the presets inside aretoneJSON, not wire blobs, so restoring from a.hxbstill needs a JSON→blob conversion. Its setlist order is what promotedDevice::setliststo [solid].
- Move the CLI to
clap. (2026-07-29)fretwire-clihand-rolled amatchoverstd::env::argsfor ~35 subcommands, with hand-maintainedeprintln!help. Both motivating problems were observed rather than theoretical, and both are now structurally impossible: 1. The help drifted. Editing oneeprintln!during the 2026-07-26 session silently droppedsetandsnapshotfrom the listing. The migration found three more commands missing from it —tree,move-to-row,before-split— which nobody had noticed.--helpis now generated from the command definitions, so it cannot disagree with them. 2. Silent bad-argument fallbacks.args.next().map(|s| s.parse().unwrap_or(0)).unwrap_or(0)meantfretwire goto 5 bananaquietly targeted bank 0 — and onsave/rename, that was a persistent write to the wrong setlist. Every numeric argument now errors instead.bypass/move-to-rowbecameValueEnums: the old parser read any unrecognised word, and a missing argument, asoff/series — so a typo silently did the opposite of the request. Every documented invocation was smoke-tested unchanged (parsing happens beforeconnect, so argument acceptance is checkable without hardware).
-
fretwire import-data <installer>— extract Line 6's reference data from the user's own HX Edit install (verified byte-identical vs the bundled copies). - The data flip:
Catalog::bundled()(include_bytes!) →Catalog::from_data_dir()+ no-data fallback;git rmthe bundled data +res-extracted/; update tests. (2026-07-18) - First-run import in the GUI —
fretwire_core::import+FirstRun.svelte, so a fresh install doesn't dead-end at "runfretwire import-data". (2026-07-21) - Packaging:
.deb/.rpm/AppImage viatauri build(deb/rpm install the udev rule and ship the CLI), static musl CLI, GitHub Release on av*tag. README + license/trademark notes. (2026-07-21) - Update check (
fretwire_core::update, 2026-09-02) — opt-in, once a day, oneHEADtoreleases/latest(the redirect names the tag; no API, no version in the User-Agent). Badge in the GUI header + an About dialog, the first-run checkbox, a one-time ask bar for installs that predate the question,fretwire check-update [--auto on|off], andFRETWIRE_NO_UPDATE_CHECK=1. Runs on the daemon under serve mode. Deliberately not an auto-updater: only the AppImage could replace itself, a self-rewriting.deb/.rpmfights apt/dnf, and signed-artifact plumbing is a trust surface this project does not need. The native answer to "automatic" is the AUR / Flathub items below. - Publish to the AUR — the package now builds and runs (2026-09-02, first time):
makepkgfrom a copy ofpackaging/PKGBUILDon CachyOS against thev0.4.0tag tarball —cargo fetch --locked,npm ci, the frontend build, both binaries in release, the offlinecheck()suite, and a 4.6 MBfretwire-0.4.0-1-x86_64.pkg.tar.zstholdingfretwire,fretwire-gui, the udev rule, a desktop entry, the icon and both licences. The packaged CLI detected the HX Stomp on the bench; the GUI links against the system WebKitGTK with nothing missing.sha256sumsis the real hash (updpkgsums), and the generatedpackaging/.SRCINFOis tracked beside it. Caveats: the dependency check was skipped here (makepkg -d) because this box's rustup is from rustup.rs, which pacman cannot see providingcargo; a real Arch user'srustuppackage does.check()lists the crates the tagged tree has — addfretwire-commandswhen the pkgver moves to 0.5.0. CI builds it now (same day): anarchjob inrelease.ymlruns makepkg in thearchlinux:base-develcontainer from agit archiveof the checkout (the tag tarball does not exist yet when the job runs, so the checksum is skipped there and the AUR copy keeps the real one), refuses a tag whose version differs frompkgver, installs the result withpacman -U, checks the udev rule, desktop entry and the GUI's shared libraries, and attachesfretwire-<ver>-1-x86_64.pkg.tar.zstto the release. Rehearsed in podman on the stock image before the first tag: the first run failed to linkring— makepkg'sltooption puts-flto=autointo CFLAGS and rust-lld cannot read GCC LTO bitcode — so the PKGBUILD carriesoptions=('!lto'); the second run passed end to end (a 6.9 MB package with generic x86-64 flags — a CachyOS-built one carries-march=nativeand must not be shipped). AUR registration is closed to new accounts (bot spam, 2026-09), so the release asset is the Arch channel until it reopens. Left to do: 1. Watch the first real run, on thev0.5.0tag — which is also the first run of the musl job withfretwire-serveandfretwire-mcpadded to it (2026-09-02: the daemon embeds the frontend, so that job now builds it too; rehearsed in anubuntu:22.04container). 2. Publish when registration reopens: AUR account + SSH key →git clone ssh://aur@aur.archlinux.org/fretwire.git→ copyPKGBUILD+.SRCINFO→ commit → push. Check the name is free first. Per release afterwards: bumppkgver, resetpkgrel=1,updpkgsums, regenerate.SRCINFO. - Flathub — deferred. Needs a broad
--device=allfor USB, can't install a udev rule, and the sandbox complicates pointing at an HX Edit installer on the host. Revisit once there are users. - arm64 for the CLI (
aarch64-unknown-linux-musl) — DONE (2026-08-26), lands with the nextv*tag. Theclijob inrelease.ymlis now a matrix; the arm64 leg is a native build on GitHub's freeubuntu-24.04-armrunner (public repos), so no cross toolchain — apt'smusl-toolsprovides the rightmusl-gccon each arch. Asset:fretwire-cli-aarch64-linux-musl.tar.gz. Why: a Raspberry Pi wired into a pedalboard doing preset switching / backup / restore with no screen (asked for 2026-08-23, Pi 5 alongside PiPedal); Asahi Linux is a smaller second. Caveat: the binary has never been run on ARM hardware here — say so in the release notes until someone confirms it runs. The udev blocker went with it (same day): a Pi reached over SSH has no seat, so the rule'sTAG+="uaccess"granted nothing headless and the CLI gotEACCESwith no hint why. Every rule line now also grantsGROUP="plugdev"(ships on Pi OS/Debian;install-udevrunsgroupadd -felsewhere and prints theusermodstep), the CLI test asserts both grants per line, and README/serve-mode.md document the headless path. Verified on a plugdev-less box: udev warns and still appliesMODE+uaccess. - arm64 for the GUI — still not planned, and the request that would have triggered it turned
out to be for something else. Feasible (public repos get free
ubuntu-24.04-armrunners, so it's a native build with no cross-compiled WebKitGTK), but the person who asked runs headless — an arm64 bundle of a windowed app does not serve them. What they want is Phase 10. Revisit only if someone with a Pi desktop asks. - Other architectures — deliberately not doing: i686 (dead on the desktop), armv7 / 32-bit Pi (won't run the editor usefully), RISC-V (no users). Untestable binaries are a support burden.
Opened 2026-07-22 by a contributor's Helix Floor captures + backup. Survey: docs/helix-floor.md.
The data layer already covers the Floor (355/355 models, 19,377/19,377 param keys resolve) and its
USB control interface is identical to the Stomp's, so this is mostly plumbing — once we can see a
real session.
-
Identify the device: PID
0x4248, presetdeviceID0x210001, version word0x03800000(not the firmware version — a 3.80 Stomp reports it too). Constant + udev rule landed; no code matches on the PID yet. -
Decode the
.hxbbackup container (header + concatenated raw zlib streams). -
Get a capture with HX Edit connected (captures 3 & 4, 2026-07-22).
-
Verify the handshake — it is byte-identical to the Stomp's. All ten
device_handshake()frames appear verbatim in both Floor captures. No change needed. Floor model code isP21. -
Confirm our preset parser reads Floor streams — it does, unmodified, including all 8 snapshots. Cross-checked against the
.hxbbackup as ground truth. -
Handle slot
type 7(Looper) infretwire_data::streamenumeration. Different content shape: model index at key8, params at7 → 4, enabled at10. Fixed the Stomp too — a Floor capture's serial preset went from 8 blocks to 9 once its Looper stopped being skipped. -
Walk preset key
1(the second DSP's slot array) alongside key0. Blocks now carry(dsp, index)and flatten to the wire slot withdsp * 20 + index.fretwire_protocol::editneeded no change.EditorPresetgained aDspViewper DSP (its own split/mixer/input/output nodes, grid and load); the flat accessors now mean DSP 0, which is what a one-DSP device has. Verified against a real Floor capture: "Pull Me Under" decodes all 15 blocks across both DSPs with the right rows and footswitch bindings (it used to show 7). -
Verify the write path — byte-exact, 9/9 ops. Captures 3 & 4 are HX Edit-driven and carry the full write path; our existing
editbuilders reproduce the Floor's bytes exactly forset_value,bypass,begin_structural,swap_model(incl. a paired amp+cab swap),save_presetand select-preset. Envelope shapes are identical to the Stomp's. No protocol change is needed for the Floor in either direction. -
DSP2 addressing — solved (
WinCap5, 2026-07-23). Wire slot numbers are global:slot = dsp * 20 + index, so DSP1 is 0–19 and DSP2 is 20–39. There is no DSP field and none is needed. Confirmed by five DSP2 blocks edited in HX Edit onFACTORY 112B"Pull Me Under", each sweep's first wire value landing one UI increment from that block's stored value — and consistent with every earlier capture (all slots < 20, all DSP1). The same capture also gives the read side of a parallel, dual-DSP preset. No further Floor captures are needed. -
Replaced the scattered PID constants with a device-descriptor type.
fretwire_protocol::Device+DEVICEScarry PID, model code, presetdeviceID, DSP count, snapshot count and aSupportflag;Device::by_pid/by_model_codedo the lookups.Transport::opennow matches any known device (verified ones first) and exposes which it opened viaTransport::device()/Session::device();present_devices()lists everything plugged in. The HX Stomp XL isReported(2026-08-20) — an owner has the editor working against one, and both bugs they filed reproduced on an HX Stomp — with its unknown fields still honestlyNone, because "a user says it works" fills in none of them. We have no capture, preset or backup from one, so opening it still logs a caveat. Tests pin the invariants, including that every table entry has a matching udev rule. Still open on the XL: how many setlists it has. Its banking was answered on 2026-08-21 by the owner reading01A-32Doff the panel, and its model codeP36by their handshake log. -
Helix LT — PID
0x424A,Reported(2026-08-22, PR #3). Surveyed on a contributor's physical unit: handshake, preset read, snapshot decode and the setlist/preset browses all work unmodified once the PID is in the table, so the LT needs no protocol change. It stamps the Floor'sP21and carries the Floor's geometry (2 DSPs, 8 snapshots, 8 banks of 128), soby_model_code("P21")keeps resolving to the Floor — they are one data class. Survey:docs/helix-lt.md. Still open on the LT: itspreset_device_id(never on the wire; the Floor's came from a.hxb, and we have no LT backup), how its screen banks presets, and every write path — no edit has ever been sent to one, which is why it is notVerified. -
HX Effects — PID
0x4245,Untested(2026-08-22, issue #10), thenReported(2026-08-24). A contributor ranlsusband sent the line:ID 0e41:4245 Line6, Inc. HX Effects, which made it findable and nothing more — the table's firstUntestedentry. An owner has since reported it working, which moves it toReportedby outcome:detectfinds one, the udev rule covers it, opening it still warns, and every other field isNone. It is the family member least like the rest (effects only, no amps or cabs), so nothing is inherited from the Stomp. Still open: everything else — the report carries no capture and no panel readings. Onepullfrom an owner would settle its model code and preset geometry. -
Helix Rack — PID
0x4249,Untested(2026-09-01). The table's only entry added without a person behind it. The id is the Linux kernel's Line 6 rate quirk, which labels it "Helix Rack >= fw 2.82"; the same table's0x4248"Helix >= fw 2.82" and0x424A"Helix LT >= fw 2.82" are the two ids we did measure on hardware, so the value between them is corroborated at both ends. Every field isNone— not even the Floor'sP21, though the Rack is a Floor in a rack box, because a guessed code would makeby_model_codeambiguous. The pre-2.82 ids are deliberately absent (0x4241Helix,0x4242Helix Rack,0x4244Helix LT): this protocol was recovered from firmware 3.x. Still open: everything.fretwire detectnaming one is the entire report needed to move it toReported. -
Global settings — op 24 reads, op 25 writes, 27 ids named (2026-08-22, and eight Ins/Outs ids from an HX Stomp XL owner on 2026-08-23). The namespace is flat and numbered; a 601-id sweep costs 1.4 s, so
settings-dump/settings-diffmaps it with no capture at all.fretwire_protocol::settingsis the shared table, the CLI hassetting-get/setting-set, and the GUI has a Globals panel. Id 27 (preset numbering) turned the preset sidebar's manual toggle into a view of the pedal's own setting — it reads the form at connect and writes it when switched, so the sidebar item and the Globals panel's "Preset numbering" row are two views of one value (2026-08-22; verified live 2026-08-23, switching in the GUI changes the form on the pedal's own screen). The global EQ is fully mapped:190-200, three bands of frequency/Q/gain then the two cuts, and the GUI draws it as a response curve. Still open: 138 answering ids are unidentified;127(Auto In-Z, renamed from a mis-transcribed "Guitar In-Z" on 2026-08-23) has two observed values and neither is named;201-203are unknown (an earlier "global EQ" gloss was withdrawn). Writes are gated to identified ids only. -
The footswitch record is decoded (2026-08-22). Op 33 returns
{102: switch (zero-based), 65: ?, 109: label, 66: ?, 67: [assignments]}, and an assignment is{59: enabled, 68: ?, 66: colour, 69: {109: name, 98: slot, 28: param, …}}. Key 66 is the LED ring colour,0xRRGGBB— proved by binding two blocks of different categories, a delay coming back0x06FF00(green) and an amp0xFF0003(red). The same key in the type-41 status push is the ring's current colour, bright when engaged and ~1/16 brightness when bypassed, which refutes the "state bitmask" reading that section carried. -
Custom footswitch colours and labels — SHIPPED (2026-08-27). Pick a ring colour and a name per footswitch, the way HX Edit does. Storage decoded the same day (label = layout key
14gated by13, colour =16gated by15;docs/preset-format.md), written through the op-21 document path since no surgical op is known:Session::{set_switch_label, set_switch_color}, CLIswitch-label/switch-color, and a ✎ mini-editor on the GUI block panel with the ten palette swatches. The palette was mapped live by sweeping indices 1-10 and reading the ring, then matched to Line 6's ownfootswitchLEDenum inHelixControls.json: 0 Auto Color, then White, Red, Dark Orange, Light Orange, Yellow, Green, Turquoise, Blue, Violet, Pink, and 11 Off — all twelve observed live. The ring and scribble repaint immediately on the write.hxb-convertcarries@fs_customlabeland@fs_customcolor.Still open, low priority: ops 58-62 (HX Edit's incremental route — **do not probe by guessing**, one power cycle per guess, see `docs/safety.md`); whether record keys `65`/`68`/`26`/`120` matter; the LEDs' exact RGB (the GUI's swatch colours are by-eye approximations). -
assign-bypassleaves the switch label unset where the pedal sets it — SETTLED (2026-08-27). Op 33's109mirrors the layout entry's key14gated by13, and the pedal never backfills them — not with time, not on re-read, not on a snapshot change, not across a save and flash reload (all tried live). The panel's bind gesture writes13: true+14: <name>itself; op 56 writes the virgin pair. So op 56 is not missing a follow-up op — the difference is two document keys, writable on purpose. Seedocs/protocol.md. -
Session grid/routing planning is still DSP-0 only.
add_block_at,place_block,insert_block,reorder_blockandset_node_posplan slot moves inside one 20-slot array and read it viadsp_blocks(0)/dsp_grid(0)— complete for the Stomp, needs adspargument for the Floor. Reading and per-block edits are already DSP-agnostic; only this layer is not. -
Grid/UI: the routing view assumes one DSP × 2 rows. The Floor needs 2 DSPs × 2 paths. The backend is ready —
PresetDto.dsps[]carries each DSP's grid/nodes/load, and every cell and block is tagged with itsdsp; the flat fields mirrordsps[0]so the current UI is unaffected until it's rewritten. -
[~]
.hxbimport/restore. The conversion is built and measured (2026-08-25):fretwire_data::toneturns atoneobject — what an.hxbslot and an.hlxfile both carry — into the wire preset, andfretwire hxb-convertwrites an export file the existingbackup-show/restorepath already reads. Blocks, split topology and snapshots all come across, and so do the footswitch bindings with their custom labels and LED ring colours; checked against one preset held in both forms (a Floor backup and a wire dump of the same slot off the same unit), where all 15 blocks, 106 parameter values, 320 snapshot-matrix cells and all 8 bypass bindings match the device's own bytes. This gives.hlximport too — the format is the same tree — which is the more useful half, since.hlxis what people share. > 2026-08-26: the block-type gaps are closed. The amp+cab refusal dissolved on a live > measurement: both cab families are ordinaryHelix.symentries and a paired cab stores > whichever the preset uses — there was never a family mapping to find. A swap sweep on the > Stomp measured every missing class (dual cab 16/32, IR 19/21, looper 22, synth 23, plus > the family split 18/33 that refuted the old unconditional 33), the IR block's key-27 UUID > reference fell out of the same dumps, and the looper had a device-written oracle in the > Sultans stream all along. Every non-empty preset in the sample backup now converts — > 363/363, from 39. Fixtures:captures/pairing_sweep.md; oracle tests: >fretwire-data/tests/paired_blocks.rs. > 2026-08-26, later: controller assignments carry across too. The oracle preset turned > out to describe its whole table — the earlier "extra rows from the capture session" > reading was a scan hitting a different preset of the same name — so key4, each > snapshot's per-controller values (2), and the type-2 footswitch-layout row a > switch-sourced assignment also owns are all written now, byte-identical to the device's > own across all four source kinds the oracle holds (EXP ×2, footswitch, snapshots). All > 608 controller entries across the sample backup's 363 presets carry; the one source kind > still skipped is MIDI (its row carries a CC number no tone we hold shows). Bonus decode: > the Floor's switch ordinals run from 6 (switch = ordinal − 5), not the Stomp/XL's 3. A converted preset has still never been sent to a pedal. What is left: - TopologyAB(a DSP the paths merely pass through, Floor-only) still has no wire evidence and stays a refusal. - The four input/output nodes store a ragged prefix of their symbol's parameter list and the rule is unknown; they keep the target's values. - MIDI-sourced controller rows (the CC number, key1under a MIDI source) — needs one tone+wire pair with a MIDI assignment.
Opened 2026-08-23 by a Helix Floor owner running a Pi 5 with PiPedal, who asked for a Raspberry Pi
build and described wanting something else: the editor served over the network from a machine with
no screen. Survey and full breakdown: docs/serve-mode.md.
The pieces are further along than the size suggests — ui/src/lib/ipc.js is already a single
transport seam with two implementations behind it (Tauri, and the browser mock), the UI already
runs in a plain remote browser via npm run dev, and of 61 commands only 2 touch AppHandle,
for 3 event names total.
- Lift
commands.rs+dto.rsout offretwire-tauriinto a transport-neutral crate — DONE (2026-08-31):fretwire-commands, indefault-membersso the offline suite covers it.fretwire-taurikeeps 65 one-line#[tauri::command]wrappers (the surface had grown from 61) plus aTauriSink;spawn_heartbeatandexport_setliststake anevents::EventSink, and each event's name + JSON payload live once inevents::Event. -
fretwire-serve— DONE (2026-09-01): axum + embeddeddist/(rust-embed; one static binary in release),POST /invoke/{command}throughfretwire_commands::dispatch(offline- tested, camelCase args like Tauri's), a WebSocket at/eventsfor the three events, clean SIGINT/SIGTERM session teardown, and a single-editor lease (second browser → close 4409 / HTTP 409; released on disconnect). Out ofdefault-memberslikefretwire-tauri. The UI'sipc.jspicked up the third transport (serve.js), selected by a marker the daemon injects intoindex.html— the same dist runs under Tauri, serve, and the mock. - Client-side files — DONE (2026-09-02): five
_inlinecommand variants (ir_upload/ir_export,export_setlists/backup_show/restore_preset) carry the file in the invoke — base64 for the WAV, the text for an export — sharing their bodies with the path pair; the UI picks byINLINE_FILES(serve or the mock) vialib/files.js, so in a browser IRs and exports are uploads/downloads and the export dialog can still target the daemon's disk. Data import stays server-side permanently (fretwire import-dataover SSH — the installer is ~1 GB); that is the one flow still reachingpickPath()under serve, and the server-side directory browser is a nice-to-have for it. Seedocs/serve-mode.md§3. - Auth for non-loopback — DONE (2026-09-02). Loopback needs nothing (only local
processes reach it); any wider
--bindrequires a bearer token, generated once into~/.local/share/fretwire/serve-token(0600; or--token/FRETWIRE_SERVE_TOKEN) and printed at startup inside the link to open —#token=…, a fragment, so it never reaches logs or a Referer. The page keeps it per origin, sends it asAuthorization: Beareron invokes and as a query parameter on the event socket (browser JS can't set handshake headers), and asks for it on a 401 / close 4401. With a token theHostrule relaxes to "our port" andOriginmust equalHost(a rebinding page lands on its own origin with no token); without one the 2026-09-01 loopback rule stands. No TLS to start, by decision: a LAN bind assumes a trusted network, and the SSH tunnel, a VPN, or a TLS proxy cover the rest. Seedocs/serve-mode.md§4. - A
GROUP=udev rule — DONE (2026-08-26), with the arm64 CLI item in Phase 8: every rule line now grantsGROUP="plugdev"alongsideuaccess,install-udevcreates the group and prints theusermodstep, and the test asserts both grants per line. - Verify PiPedal coexistence on hardware. Different USB interface from the audio one, and
fretwire-usbalready falls back todetach_and_claim_interface, so it should be fine — but detaching an interface out from under a live audio path is not a friendly failure, and this has never been tested here. - The arm64 serve artifact, once the crate exists (the arm64 CLI shipped independently — Phase 8).
- MCP server — a third consumer of the same lift. DONE (2026-09-02):
fretwire-mcp, a stdio server on the officialrmcpSDK — 14 read tools (offline export-file and catalog tools, live reads), +10 edit-buffer tools behind--allow-writes, +preset_savebehind--allow-save; ungated tools are unlisted. Text results in HX Edit's display units, set the same way. Left open: the in-daemon HTTP transport (needs a second seat on the lease),model_params,.hxbinput. Seedocs/serve-mode.md. The original case: asked for independently on 2026-08-23, the same day as serve mode, which is the strongest argument for doing the lift at all. The requester's guess that the CLI is a poor fit is correct and measurable: ~60 live subcommands each callSession::connect(), so it's one handshake and teardown per invocation with no cursor or edit buffer carried across; a long-lived MCP process fitsSession+ the heartbeat better. Offline-first, and after the lift. Most of the value (explain / generate / tone-match / batch-rename / diff a preset) needs no pedal — it runs on backup JSON plus the catalog, all of which is already implemented offline. Don't translate 61 GUI-shaped commands into 61 tools. Read-only by default; edit buffer before persistent save; firmware/DFU never in the tool surface (docs/safety.md). Available today with no code:fretwire backup+ point an agent at the JSON. Seedocs/serve-mode.md.
See docs/safety.md. TL;DR: captures + offline work are zero-risk; live control is low-risk
(worst case = power cycle); firmware/flash/bootloader/DFU is the only brick risk and is out of
scope — never transmit it. Back up the device before any write experiments.
- Auth: HX Edit has
auth_*.xml+ an authentication log → confirm the device link itself doesn't require online auth (editing should be local; account is for licenses/marketplace). - The wire preset format may be binary (not the JSON
.hlx); captures will tell. - Firmware update path is explicitly out of scope (risk of bricking) until late, if ever.