Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# fretwire

An independent, from-scratch **Linux editor for the Line 6 HX Stomp and Helix Floor**, written in Rust.
An independent, from-scratch **Linux editor for the Line 6 HX Stomp, Helix Floor and Helix LT**, written in Rust.

fretwire talks to the pedal over its `MI_00` USB control interface (VID `0x0E41` / PID `0x4246`).
The wire protocol was recovered by **observing USB traffic to and from the device**; the model,
Expand Down Expand Up @@ -155,7 +155,7 @@ cargo run -p fretwire-cli -- detect # HX Stomp: present
cargo run -p fretwire-cli -- pull # read the loaded preset (non-destructive)
```

The rule covers both the HX Stomp (`0x4246`) and HX Stomp XL (`0x4253`).
The rule covers the HX Stomp (`0x4246`), the Helix Floor (`0x4248`), the Helix LT (`0x424a`) and the HX Stomp XL (`0x4253`).

## The reference data

Expand Down
43 changes: 43 additions & 0 deletions crates/fretwire-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ pub const PID_HX_STOMP_XL: u16 = 0x4253;
/// blocks on the second DSP (addressed by the same bare slot integer: `slot = dsp * 20 + index`).
/// See `docs/helix-floor.md` and [`DEVICES`].
pub const PID_HELIX_FLOOR: u16 = 0x4248;
/// USB Product ID for the Helix LT, read off a physical unit on Linux (2026-08-18):
/// USB product string `HELIX`, `bcdDevice 0x0200`, and the same six-interface layout the
/// Floor has, interface 0 being the vendor control channel.
///
/// The unit identifies itself as `P21` — the Floor's model code — and every read path
/// reconciles against it unchanged. See `docs/helix-lt.md`.
pub const PID_HELIX_LT: u16 = 0x424A;
/// Interface number of the vendor-specific control channel.
pub const CONTROL_INTERFACE: u8 = 0x00;
/// Bulk OUT endpoint (host → device).
Expand Down Expand Up @@ -128,6 +135,42 @@ pub const DEVICES: &[Device] = &[
setlist_size: Some(128),
support: Support::Verified,
},
Device {
pid: PID_HELIX_LT,
name: "Helix LT",
// The LT stamps the Floor's code: the handshake identity reply reports "P21" and a
// pulled preset carries key `7 → 36` = "P21\0". `by_model_code("P21")` therefore
// resolves to the Floor, which is listed first — they are one data class.
model_code: Some("P21"),
// Unknown, not copied across: the handshake carries no `0x0021xxxx` device id and
// the wire preset stream has no such field. The Floor's value came from a `.hxb`,
// and we have no backup from an LT.
preset_device_id: None,
// Both DSPs — a pulled preset populates key `1` and holds blocks in slots 21..28
// (the unit reported DSP1 71.0% / DSP2 43.0%).
dsps: Some(2),
// The pulled preset carries SNAPSHOT 1..SNAPSHOT 8.
snapshots: Some(8),
// Banks 0..7 each list 128 presets and bank 8 is refused (code -3). Bank 0 holds the
// factory amp presets and bank 7 the templates ("Quick Start", "Parallel Spans",
// "SNP:4-Amp Spill") — the Floor's layout, so the Floor's names are used. Unlike the
// Floor's, these names are not corroborated by a backup; only the arity and the two
// end banks were observed.
setlists: Some(&[
"FACTORY 1",
"FACTORY 2",
"USER 1",
"USER 2",
"USER 3",
"USER 4",
"USER 5",
"TEMPLATES",
]),
setlist_size: Some(128),
// Handshake, preset read, setlist and preset-list browse are all reconciled against a
// physical LT, but no edit has ever been sent to one.
support: Support::Untested,
},
Device {
pid: PID_HX_STOMP_XL,
name: "HX Stomp XL",
Expand Down
33 changes: 31 additions & 2 deletions crates/fretwire-protocol/tests/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
//! Everything asserted here is either a USB ID from a real descriptor or a value read out of a real
//! preset — nothing is inferred from another device in the family. See `docs/helix-floor.md`.

use fretwire_protocol::{DEVICES, Device, PID_HELIX_FLOOR, PID_HX_STOMP, PID_HX_STOMP_XL, Support};
use fretwire_protocol::{
DEVICES, Device, PID_HELIX_FLOOR, PID_HELIX_LT, PID_HX_STOMP, PID_HX_STOMP_XL, Support,
};

#[test]
fn every_device_has_a_distinct_pid() {
Expand All @@ -28,6 +30,10 @@ fn lookup_by_pid() {
Device::by_pid(PID_HX_STOMP_XL).map(|d| d.name),
Some("HX Stomp XL")
);
assert_eq!(
Device::by_pid(PID_HELIX_LT).map(|d| d.name),
Some("Helix LT")
);
assert!(Device::by_pid(0xFFFF).is_none());
}

Expand Down Expand Up @@ -74,7 +80,7 @@ fn verified_devices_are_fully_described() {
}

#[test]
fn the_untested_device_claims_nothing_it_hasnt_shown_us() {
fn the_stomp_xl_claims_nothing_it_hasnt_shown_us() {
let xl = Device::by_pid(PID_HX_STOMP_XL).unwrap();
assert_eq!(xl.support, Support::Untested);
// We have no capture, preset or backup from an XL — so none of this may be assumed to match
Expand Down Expand Up @@ -136,3 +142,26 @@ fn the_two_verified_devices_differ_where_we_measured_them() {
assert_eq!(floor.preset_device_id, Some(0x0021_0001));
assert_ne!(stomp.model_code, floor.model_code);
}

/// The LT reports the Floor's model code and the same setlist geometry, but its own preset
/// device id was never observed — it must stay unknown rather than inherit the Floor's.
#[test]
fn the_lt_shares_the_floors_data_class_without_inheriting_its_device_id() {
let floor = Device::by_pid(PID_HELIX_FLOOR).unwrap();
let lt = Device::by_pid(PID_HELIX_LT).unwrap();

// Measured on a physical LT: see docs/helix-lt.md.
assert_eq!(lt.model_code, Some("P21"));
assert_eq!((lt.dsps, lt.snapshots), (Some(2), Some(8)));
assert_eq!(lt.setlist_size, floor.setlist_size);
assert_eq!(lt.setlists, floor.setlists);

// Never seen on the wire, so never guessed.
assert_eq!(lt.preset_device_id, None);

// Both stamp "P21"; the lookup keeps resolving it to the Floor, which is listed first.
assert_eq!(
Device::by_model_code("P21").map(|d| d.pid),
Some(PID_HELIX_FLOOR)
);
}
103 changes: 103 additions & 0 deletions docs/helix-lt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Helix LT — device survey

What we know about the **Helix LT**, from a user's unit on Linux (2026-08-18, firmware string
`a7e2585`). Unlike the Floor survey, this one rests on **no USB captures and no device backup**:
everything below was read by fretwire itself over `MI_00`, with this PR's device entry applied.

**Bottom line: the LT needs no protocol change.** The handshake, the preset read, the snapshot
decode, and the setlist and preset-list browses all work unmodified once the PID is in the table.
The catalogue resolved every block and parameter of the loaded preset by name.

**Nothing has been written to this device** — no edit, no save, no backup, no restore. That is why
the entry is `Support::Untested` despite the reads reconciling cleanly.

## USB identity [solid]

| | Helix Floor | Helix LT |
|---|---|---|
| VID / PID | `0x0E41` / `0x4248` | `0x0E41` / **`0x424A`** |
| `bcdDevice` | `0x0200` | `0x0200` |
| product string | — | `HELIX` |

Interface layout, read from Linux sysfs — the same six-interface shape the Floor has, with the
vendor control channel at interface 0:

| iface | class | kernel driver |
|---|---|---|
| 0 | **Vendor (`0xFF`)** | none — this is `MI_00` |
| 1 | Audio control | `snd-usb-audio` |
| 2 | Audio streaming | `snd-usb-audio` |
| 3 | Audio streaming | `snd-usb-audio` |
| 4 | Audio / MIDI | `snd-usb-audio` |
| 5 | HID | `usbhid` |

`CONTROL_INTERFACE`, `EP_IN` and `EP_OUT` needed no change, and `claim_interface(0)` succeeded on
the first try.

## Identity and firmware [solid]

The handshake identity reply reports **`P21`** — the Floor's model code, not a code of its own. The
pulled preset agrees, at key `7`:

| key | value |
|---|---|
| `36` | `"P21\0"` |
| `37` | `"a7e2585\0"` |
| `35` | `57737248` = `0x03710020` |

Key `35` is recorded raw on purpose. `docs/helix-floor.md` notes `device_version = 0x03800000` for
firmware 3.82, which would make `0x03710020` read as ~3.71 — but that encoding has never been
pinned down against two known firmware versions, so this is data, not a conclusion.

## Preset model [solid]

Reading the loaded preset (8326 bytes, declared length matched):

- **Both DSPs.** Preset key `1` is populated and blocks came back in slots 21–28 as well as 3–6,
i.e. the global `slot = dsp * 20 + index` numbering the Floor established. The unit reported
DSP1 71.0% used and DSP2 43.0%.
- **8 snapshots**, `SNAPSHOT 1`…`SNAPSHOT 8`, and the stored active index agreed with the live
scene.
- Every one of the 8 blocks resolved to a `.models` definition by name, with device-ordered named
parameters — no unmatched model, no unmatched parameter key.

## Setlists [arity solid; names not corroborated]

Browsing each bank in turn:

| bank | result |
|---|---|
| 0 | 128 presets — `US Double Nrm`, `Essex A30`, `Brit Plexi Brt` (factory) |
| 1 | 128 presets |
| 2 | 128 presets — the user's own presets; `read-info` on the loaded preset reported `bank: 2, index: 0, name: "WIP"` |
| 6 | 128 presets |
| 7 | 128 presets — `Quick Start`, `Parallel Spans`, `SNP:4-Amp Spill` (templates) |
| 8 | refused, code `-3` |

So: **eight banks of 128**, bank 0 factory and bank 7 templates — the Floor's layout, and
`setlist_stride()`'s 128 fallback was already correct for this device.

The names in `DEVICES` are therefore the Floor's. Worth being explicit about the difference in
evidence: the Floor's names came from the eight `L6Setlist` streams of a real `.hxb`, whereas here
only the *arity* and the *character of the two end banks* were observed. If an LT backup ever turns
up and disagrees, this is the field to fix.

## Not observed

- **`preset_device_id`.** The handshake identity reply carries no `0x0021xxxx` device id, and the
wire preset stream has no such field — the Floor's `0x210001` came from a `.hxb`. Left `None`
rather than copied across.
- **Every write path.** Nothing was sent to this unit beyond reads and the handshake.

## Reproducing

With the LT connected and the udev rule installed:

```
fretwire detect # Helix LT: present (untested device)
fretwire connect # handshake OK — device reports "P21"
fretwire pull # the preset above, blocks and params resolved
fretwire setlists # 8 setlists
fretwire presets 7 # the templates bank
fretwire dump-raw lt.raw # then: fretwire tree lt.raw
```
2 changes: 2 additions & 0 deletions packaging/70-hxstomp.rules
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
SUBSYSTEM=="usb", ATTR{idVendor}=="0e41", ATTR{idProduct}=="4246", MODE="0660", TAG+="uaccess"
# Helix Floor — verified: byte-identical handshake and edit path (see docs/helix-floor.md).
SUBSYSTEM=="usb", ATTR{idVendor}=="0e41", ATTR{idProduct}=="4248", MODE="0660", TAG+="uaccess"
# Helix LT — reads verified against a physical unit; no edit has been sent to one.
SUBSYSTEM=="usb", ATTR{idVendor}=="0e41", ATTR{idProduct}=="424a", MODE="0660", TAG+="uaccess"
# HX Stomp XL — untested; we have no capture from one. Same family, so the rule is here, but
# fretwire will warn when it opens it.
SUBSYSTEM=="usb", ATTR{idVendor}=="0e41", ATTR{idProduct}=="4253", MODE="0660", TAG+="uaccess"