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.
- 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.
- 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
captures/_TODO-global-settings.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.
- [~] Backup — BUILT (2026-07-07, offline+mock verified; live pass pending):
Session:: backup_setlistsweeps the setlist (goto + raw read per slot, op-23 identity cross-check, cursor restored) into afretwire-backupJSON file (fretwire_core::backup, hex-encoded raw streams). Reads only. CLIbackup/backup-show; GUI Backup… with a progress overlay. (Stores our own format, not.hlx— the wire↔.hlxkey mapping isn't needed for round-tripping.) - [~] Restore / preset write — BUILT (2026-07-07; [hypothesis] until the first live test):
Session::restore_preset= goto slot → op-21 edit-buffer write of the stored blob → op-71 save. The op-21 chunked write is live-proven for mutated current blobs (node moves); a foreign blob is the same mechanism, unverified. CLIrestore <file> <index> [slot]; GUI Restore… with source + target pickers (overwrite always visible). Also gives duplicate/copy. - IR management — upload user impulse responses to the device's IR slots, rename/reorder/
delete. Transaction shape PARTIALLY DECODED (2026-06-28,
captures/_TODO-ir.md): PRIMARY channel, session op 255/254; upload = op 9{112:slot, 113:u32 checksum, 109:name, 110:8192B blob (2048×f32), …flags}+ op 13 commit; export = op 12/11. Before implementing: reassemble the blob, confirm the113checksum algorithm, and decode the format flags (more captures). - Global / I/O settings — extend op 25 (
{118:id, 119:value}); map the id space (only id 134 known; the preset-load pushes expose more118/119ids to catalog). - 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) - TODO — publish to the AUR.
packaging/PKGBUILDis written but not published or even built once. Needs, in order: 1. Local validation: copy it out of the tree (makepkglitterssrc//pkg/),updpkgsums(needspacman-contrib), thenmakepkg -si. This is the one package an Arch box can test end to end — the tag tarball,npm ci, both binaries,check(), the udev rule. 2. Replacesha256sums=('SKIP')with the real hash (updpkgsumsrewrites it). Publishing withSKIPmeans a tampered tarball can't be detected. 3. Publish: AUR account + SSH key →git clone ssh://aur@aur.archlinux.org/fretwire.git→ copy the PKGBUILD →makepkg --printsrcinfo > .SRCINFO(mandatory, push is rejected without it) → 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) — a matrix entry on the existing musl job inrelease.yml, ~10 lines.fretwire-clihas no C dependencies (nusbis pure Rust), so it cross-compiles to a static binary cleanly. Why: a Raspberry Pi wired into a pedalboard doing preset switching / backup / restore with no screen is the real use case; Asahi Linux on Apple Silicon is a smaller second one. Caveat: untested — no arm64 hardware here, so label the asset as such in the release notes until someone confirms it runs. - arm64 for the GUI — not planned. Feasible now that public repos get free
ubuntu-24.04-armrunners (native build, no cross-compiling WebKitGTK), but it doubles the bundle matrix for a thin slice of users. Wait for someone to ask. - 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, fw0x03800000. 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 is listed asUntestedwith its unknown fields honestlyNone— we have no capture, preset or backup from one — and opening it logs a warning. Tests pin the invariants, including that every table entry has a matching udev rule. - 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 — independently useful, needs no device and no new captures. Would give the Stomp backup-file interop too. Format is documented well enough to build against.
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.