Opcode Dictionary
Derived from captures/03-feature-sweep.log (49 marked UI actions in HX Edit 3.82
driving an HX Stomp on firmware 3.80), cross-checked against
captures/01-connect-and-sync.log and captures/02-ui-actions.log. Layers 1–4 are
described in PROTOCOL.md; this document covers layer 4’s
vocabulary only.
Nothing here is derived from Line 6 source code.
Confidence key
- [confirmed] — isolated to one marked UI action, or cross-checked against a second independent source (the preset document, or HX Edit’s own data files).
- [inferred] — consistent with everything observed, not isolated.
- [open] — explicitly undetermined. Read these as “do not guess”.
How the attribution was done
tools/hxsniff/attribute.py reassembles each channel’s byte stream while keeping a
byte-offset → USB-transfer index, so every decoded application message can be traced
back to the transfer that carried its first byte and from there to the ### MARK
line in force at that moment.
tools/hxsniff/attribute.py CAPTURE.log --marks # every mark + what it produced
tools/hxsniff/attribute.py CAPTURE.log --mark PARAM- # one group of marks
tools/hxsniff/attribute.py CAPTURE.log --opcode 30 # one opcode and its replies
tools/hxsniff/attribute.py captures/*.log --dict # the inventory below
A framing correction that changes the readings
PROTOCOL.md lists channel message type 0x0c as “start-of-stream”, and
reassemble.py used to keep only type 0x04. That is wrong: bit 0x04 means
“carries stream data”, and it is set on 0x04 (data), 0x0c (data + piggybacked
acknowledgement) and 0x14 (data on a keep-alive slot). Reading only 0x04
silently dropped 26 messages in 03-feature-sweep and 10 in 01-connect-and-sync
— including two thirds of every slider gesture, which made it look as though the
device were pushing unsolicited replies. Both scripts now match typ & 0x04.
[confirmed] — after the fix every request in all three captures has a matching
reply, every reply a matching request, and all three streams decode to the last byte
with no MessagePack desynchronisation.
The layer-3 field at offset 2 (the one PROTOCOL.md flags as usually the service
id) is not reliable: the byte-identical opcode-0 reply carries 0x0000 in
captures 01/02 and 0x28e1 in capture 03. Ignore bytes 2..3; only the u32 length
at offset 4 matters. [confirmed]
Transaction discipline
txn (key 102) is a per-channel counter starting at 1000, allocated by the host
only. Every request gets exactly one reply. [confirmed]
Status (key 103) is a completion mode, not an error code. [confirmed]
| 103 | meaning |
|---|---|
0 |
complete — 104 holds the result |
1 |
accepted, asynchronous — 104 is nil and the real completion arrives later as notification 20 carrying the same 102 |
255 |
refused — 104 is {111: signed error code} |
Both async cases in the captures are unambiguous:
# 02-ui-actions, mark LOAD-PRESET-05A-FX-5th-Then-7th
--> {102: 1009, 100: 20, 101: {107: 0, 108: 12}}
<-- {102: 1009, 103: 1, 104: None}
<-- {105: 20, 106: {102: 1009, 103: 0, 104: None}} # 1 ms later, on 0x1002
# 03-feature-sweep, mark UNDO
--> {102: 1041, 100: 21, 101: {110: <2643-byte preset document>}}
<-- {102: 1041, 103: 1, 104: None} # 83 66 cd 04 11 67 01 68 c0
<-- {105: 20, 106: {102: 1041, 103: 0, 104: None}} # 82 69 14 6a 83 66 cd 04 11 67 00 68 c0
A client must therefore not treat 103: 1 as failure, and must keep a pending-txn
table keyed on 102 so notification 20 can complete it.
HX Edit’s traffic contains only 0 and 1; status 255 surfaced once deliberately
bad requests were sent (a parameter on an empty slot, snapshot 7, model 99999),
answering {111: -3}, {111: -46} and {111: -302} respectively. Note that
the asynchronous acceptances are not validated up front — selecting preset
999 on a 126-preset device answers 103: 1 and then nothing happens — and that
a no-op such as clearing an empty IR slot is an honest 103: 0.
1. Opcode table
Channel column is the device node the request goes to: 0x1001 session control,
0x1080 preset and global data. Nothing is ever sent by the host on 0x1002 beyond
the channel hello.
| Op | Chan | Triggered by | Args (key 101) | Reply (key 104) | Conf |
|---|---|---|---|---|---|
| 0 | 0x1001 |
connect | nil | [{setlist: name}] |
[confirmed] |
| 1 | 0x1001 |
connect | {107: setlist, 101: 2} |
[{index: {109: name, 123: bool, 124: bool, 125: int}}] ×126 |
[confirmed] |
| 13 | 0x1001 |
connect | {101: 2} |
nil | [inferred] |
| 20 | 0x1080 |
double-click a preset in the librarian | {107: setlist, 108: index} |
nil, 103: 1 |
[confirmed] |
| 21 | 0x1080 |
Undo (of a model change) | {110: <preset document>} |
nil, 103: 1 |
[confirmed] |
| 22 | 0x1080 |
connect; after a preset load | nil | preset document (blob) | [confirmed] |
| 23 | 0x1080 |
connect; after a preset load | nil | {107, 108, 109: name, 117: bool, 83: [int,int], 92: int} |
[confirmed] |
| 24 | 0x1080 |
connect; Global Settings gear | {118: object id} |
{118: id, 119: value} |
[confirmed] |
| 30 | 0x1080 |
any parameter edit | {98, 29, 26, 28, 119} — see §4 |
echo of the args | [confirmed] |
| 40 | 0x1080 |
pick a model in the browser; Redo | {98: block, 100: {23: bool, 25: model, 26: model}} |
{13: 1, 24: <slot object>} |
[confirmed] |
| 42 | 0x1080 |
click a routing choice on an Input or Output block | {98: slot, 51: destination} |
nil | [confirmed] |
| 41 | 0x1080 |
click a block’s bypass switch | {98: block, 59: enabled bool} |
nil | [confirmed] |
| 76 | 0x1080 |
connect | {} |
{63: bool, 55: [11 floats]} — Global EQ |
[confirmed] |
| 78 | 0x1080 |
click a block in the signal chain | {98: block, 26: 0} |
nil | [confirmed] |
| 99 | 0x1080 |
connect | {} (arguments ignored) |
{63: bool} — is the tempo driven by external MIDI clock. HX Edit shows [External] instead of the BPM when true. |
[confirmed] |
| 112 | 0x1001 |
connect | nil | nil | [open] |
| 254 | 0x1001 |
connect | {} |
nil | [open] |
Opcodes 6, 25, 59, 61 and 68 listed in PROTOCOL.md come from kempline/helix_usb
and do not appear in any of our captures. They are not corroborated here.
1.1 Worked examples
Each is quoted as the layer-3 body (after the 8-byte stream header), taken verbatim from the capture.
op78 — select block. BLOCK-select-EQ1, transfer 2298:
83 66 cd 03 f2 64 4e 65 82 62 02 1a 00
--> {102: 1010, 100: 78, 101: {98: 2, 26: 0}}
<-- {102: 1010, 103: 0, 104: None}
<-- {105: 39, 106: {82: 1, 68: 3, 121: 19, 106: {98: 2, 26: 0}}}
Block 2 of CT-Sad is HD2_CaliQMono — the EQ block the operator clicked. The four
BLOCK-select-* marks produced 98: 1, 2, 3 and 6, matching preset slots 1
(Scream 808), 2 (Cali Q), 3 (Cali Rectifire + cab) and 6 (LA Studio Comp) exactly.
op41 — bypass. BLOCK-bypass-toggle then BLOCK-bypass-restore, transfers 5264
and 5322:
83 66 cd 04 07 64 29 65 82 62 06 3b c2 --> {102: 1031, 100: 41, 101: {98: 6, 59: False}}
83 66 cd 04 08 64 29 65 82 62 06 3b c3 --> {102: 1032, 100: 41, 101: {98: 6, 59: True}}
<-- {105: 49, 106: {82: 0, 68: 5, 121: 17, 106: {98: 6, 59: False}}}
The block was on before the click, so 59 = enabled (True = active, False =
bypassed), not “bypassed”. [confirmed]
op40 — change a block’s model. MODEL-change-pick-first, transfer 7152:
83 66 cd 04 0f 64 28 65 82 62 01 64 83 17 c2 19 cd 01 84 1a ff
--> {102: 1039, 100: 40, 101: {98: 1, 100: {23: False, 25: 388, 26: -1}}}
<-- {102: 1039, 103: 0,
104: {13: 1, 24: {19: 6, 20: {24: {23: False, 25: 388, 26: -1},
9: 1, 10: True,
11: {2: 3, 3: 3, 4: [0.55, False, False]},
12: {2: 0, 3: 0, 4: []}}}}}
Note key 100 is reused inside 101 as the model descriptor. It is nested, so
there is no ambiguity, but a naive “key 100 means opcode” reader will break.
op24 — read a device object. GLOBAL-settings-gear opened the Global Settings
dialog and read five objects in a row, 52 ms apart:
--> {102: 1033, 100: 24, 101: {118: 95}} <-- {118: 95, 119: False}
--> {102: 1034, 100: 24, 101: {118: 96}} <-- {118: 96, 119: True}
--> {102: 1035, 100: 24, 101: {118: 97}} <-- {118: 97, 119: 0}
--> {102: 1036, 100: 24, 101: {118: 98}} <-- {118: 98, 119: 10}
--> {102: 1037, 100: 24, 101: {118: 99}} <-- {118: 99, 119: 11}
The connect sequence reads ids 128, 14, 73, 136 and 27. Which setting each id names is [open] — the capture only shows values, and no click in the sweep changed one. Two ids are pinned from elsewhere: 16 = tempo in BPM and 28 = current preset index (see §3, notification 22).
No write counterpart to opcode 24 was captured. The three GLOBAL-tab-* marks
and GLOBAL-close produced zero traffic, so the opcode that writes a global setting
is [open].
1.2 The connect sequence
Identical in content and order across all three captures — only 117 in the op23
reply differed (see §2.2) [confirmed]:
0x1001 hello(5) · 0x1080 hello(6) · 0x1002 hello(4) · 0x1001 hello(2)
0x1080 op76 {} -> global EQ
0x1080 op24 {118: 128} -> 0
0x1080 op23 nil -> {107: 0, 108: 7, 109: 'CT-Sad', ...}
0x1080 op22 nil -> 2531-byte preset document
0x1080 op24 {118: 14} -> 1
0x1080 op24 {118: 73} -> 0
0x1080 op24 {118: 136} -> 0
0x1001 op254 {} -> nil
0x1080 op24 {118: 27} -> False
0x1080 op99 {} -> {63: False}
0x1001 op0 nil -> [{0: 'PRESETS'}]
0x1001 op1 {107: 0, 101: 2} -> 126 preset names
0x1001 op112 nil -> nil
0x1001 op13 {101: 2} -> nil
126 entries confirms key 108 is a linear zero-based preset index over the whole
setlist, not a bank number: the librarian’s 05A is 108: 12, and 12/3+1 = 5,
12 mod 3 = 0 → A. [confirmed]
2. Key dictionary
2.1 Envelope keys
| Key | Meaning | Type |
|---|---|---|
| 100 | opcode (request) | int |
| 101 | arguments (request) | map or nil |
| 102 | transaction id | int, from 1000 per channel |
| 103 | completion mode — 0 done, 1 async (see above) | int |
| 104 | result (response) | any |
| 105 | notification id | int |
| 106 | notification payload; also the inner argument key one level down | map or nil |
2.2 Argument and result keys
| Key | Meaning | Type | Conf |
|---|---|---|---|
| 2 | parameter-array capacity | int | [inferred] |
| 3 | parameter-array count actually present | int | [inferred] |
| 4 | parameter value array | array | [confirmed] |
| 9 | model id, repeated alongside 24.25 |
int | [inferred] |
| 10 | block enabled | bool | [inferred] |
| 11 | primary model’s parameter group {2, 3, 4} |
map | [confirmed] |
| 12 | secondary (cab) model’s parameter group | map | [confirmed] |
| 13 | in an op40 result — always 1; inside a split or join body it is the slot the branch attaches before |
int | [confirmed for split/join, open elsewhere] |
| 16 | tempo, BPM (preset-level, key 5) |
float | [confirmed] |
| 19 | slot kind: 0 input, 1 output, 2 split, 3 join, 6 block, 8 empty | int | [confirmed] |
| 20 | slot contents | map or nil | [confirmed] |
| 22 | array of 20 slots | array | [confirmed] |
| 23 | model descriptor has a paired second model (amp+cab) | bool | [inferred] |
| 24 | model descriptor {23, 25, 26}; in an op40 result, the whole slot object |
map | [confirmed] |
| 25 | primary model id — index into Helix.sym |
int | [confirmed] |
| 26 | in a model descriptor: secondary model id, -1 = none |
int | [confirmed] |
| 26 | in op30 / op78 / preset key 6: block sub-address, always 0 here |
int | [open] |
| 28 | parameter index — see §4 | int | [confirmed] |
| 29 | in op30, always True |
bool | [open] |
| 55 | Global EQ: 11 floats, three bands of freq/Q/gain plus low-cut and high-cut | array | [confirmed] |
| 59 | block enabled (op41) | bool | [confirmed] |
| 63 | an enable flag; Global EQ on/off in op76, something else in op99 | bool | [inferred] |
| 68 | notification topic — see §3 | int | [inferred] |
| 82 | notification flag, 0 or 1, tracks 68 — see §3 |
int | [open] |
| 83 | in op23: [int, int], stable per preset, changes with the preset — most likely DSP usage in hundredths of a percent per core |
array | [inferred] |
| 92 | in op23, always 0 | int | [open] |
| 98 | block index — index into the preset’s slot array 0.22[] |
int | [confirmed] |
| 107 | setlist index | int | [confirmed] |
| 108 | linear zero-based preset index (0..125 on HX Stomp) | int | [confirmed] |
| 109 | name | string (C string, NUL included in the msgpack length) | [confirmed] |
| 110 | a whole preset document, as op21’s argument | blob | [confirmed] |
| 117 | in op23 — True in captures 01/02, False in 03 for the same preset; most likely “has unsaved edits” |
bool | [inferred] |
| 118 | device object id (op24, notification 22) | int | [confirmed] |
| 119 | value — of an object (op24) or a parameter (op30) | float / bool / int | [confirmed] |
| 121 | notification sub-type — see §3 | int | [inferred] |
| 123, 124, 125 | per-preset flags in the op1 listing; False, False, 0 for all 126 presets |
bool, bool, int | [open] |
Key 101 is used both as the top-level “arguments” key and, inside op1 and op13’s arguments, as an ordinary key with the value 2. Its inner meaning is [open].
3. Notification events
All notifications arrive on channel 0x1002 (device node 0x1002, host 0x03f0).
Two shapes exist:
{105: 20, 106: {102: txn, 103: status, 104: result}} # deferred completion
{105: id, 106: {82: f, 68: topic, 121: sub, 106: args}} # state change
Dispatch on 105, and on 121 within it. 105 alone is not enough: id 22 covers
two different messages and id 49 covers three. The (82, 68, 121) tuple was
perfectly stable across all three captures. [confirmed]
| 105 | 82 | 68 | 121 | Args | Produced by | Conf |
|---|---|---|---|---|---|---|
| 4 | 1 | 1 | 6 | {107, 108} |
preset load — the last event of the load | [inferred] |
| 8 | 1 | 1 | 5 | {107, 108} |
preset load — the first event of the load | [inferred] |
| 20 | — | — | — | {102, 103, 104} |
deferred completion of an earlier 103: 1 reply |
[confirmed] |
| 21 | — | — | — | nil | after every document-write completion — follows notification 20 in all fourteen captured undos | [confirmed pattern, meaning inferred: post-commit tick] |
| 22 | 0 | 9 | 25 | {118: id, 119: value} |
a device object changed | [confirmed] |
| 22 | 0 | 10 | 27 | nil | see below | [open] |
| 30 | 0 | 6 | 20 | {98, 29, 26, 28, 119} |
a parameter changed | [confirmed] |
| 39 | 1 | 3 | 19 | {98, 26} |
selected block changed | [confirmed] |
| 49 | 0 | 5 | 10 | {98} |
slot rebuilt (op40) | [confirmed] |
| 49 | 0 | 5 | 17 | {98, 59} |
bypass changed | [confirmed] |
| 49 | 0 | 5 | 47 | nil | emitted with 49/10 on every op40 | [confirmed] |
82 and 68 are both functions of the message: 68 ∈ {1,3} always came with
82: 1, 68 ∈ {5,6,9,10} always with 82: 0. What either field means
individually is [open]; the tuple behaves like a (class, interface, method)
tag from whatever RPC framework Line 6 use internally.
3.1 Notifications echo the host’s own writes
Every notification in the sweep was caused by a host request, and the device still broadcast it. A client that also renders the signal chain will see its own edits come back and must not treat them as external changes without comparing values. [confirmed]
3.2 Notification 22 / (0, 9, 25) — object changed
Loading a preset produced three of these:
{105: 22, 106: {82: 0, 68: 9, 121: 25, 106: {118: 16, 119: 120.0}}}
{105: 22, 106: {82: 0, 68: 9, 121: 25, 106: {118: 16, 119: 120.0}}}
{105: 22, 106: {82: 0, 68: 9, 121: 25, 106: {118: 28, 119: 12}}}
Object 28’s value 12 is the preset index just selected. Object 16’s value 120.0 is
the newly loaded preset’s 5.16; the preset being replaced had 5.16 =
75.789474, so the notification is reporting the change rather than echoing the old
state. Both are [confirmed] by that cross-check. This is the same 118 id space
opcode 24 reads, so op24 {118: 16} should read the tempo back — untested, because
the connect sequence never asks for it.
3.3 Notification 22 / (0, 10, 27) — an argument-free tick
479 of these across the captures, always with 106: None, so they carry no
information beyond “something happened”. Every inter-arrival gap is an exact multiple
of 75 ms, and they cluster densely just after the DSP rebuilds (preset load, model
change, undo/redo). What they report is [open]. They are safe to ignore: nothing
in the sweep depends on them.
3.4 The event order for a model change
--> op40 {98: 1, 100: {23: False, 25: 388, 26: -1}}
<-- rsp {13: 1, 24: <new slot object>}
<-- ev49 {82: 0, 68: 5, 121: 47, 106: None}
<-- ev49 {82: 0, 68: 5, 121: 10, 106: {98: 1}}
<-- ev39 {82: 1, 68: 3, 121: 19, 106: {98: 1, 26: 0}}
<-- ev22 x11 (0, 10, 27)
The op40 reply already contains the rebuilt slot including its new default parameter values, so a client does not need to re-read the preset after a model change. [confirmed]
4. Parameter addressing — opcode 30
This is the load-bearing result. Six PARAM-* marks in the sweep each touched a
different control on one block, and one more in capture 02 touched a different block.
The block under edit was preset slot 6 of CT-Sad, model id 120 =
HD2_CompressorLAStudioCompMono — LA Studio Comp, whose parameters in
Helix.sym are, in order:
0 PeakReduction 1 Gain 2 Type 3 Emphasis 4 Mix 5 Level
What each mark sent:
| Mark | args | 28 | resolves to |
|---|---|---|---|
PARAM-PeakReduction-slider |
{98: 6, 29: True, 26: 0, 28: 0, 119: 0.33 → 0.05} |
0 | PeakReduction |
PARAM-Gain-slider |
{98: 6, 29: True, 26: 0, 28: 1, 119: 0.57 → 0.34} |
1 | Gain |
PARAM-Type-toggle-Limit |
{98: 6, 29: True, 26: 0, 28: 2, 119: False → True} |
2 | Type |
PARAM-Type-toggle-Compress |
{98: 6, 29: True, 26: 0, 28: 2, 119: True → False} |
2 | Type |
PARAM-Mix-slider |
{98: 6, 29: True, 26: 0, 28: 4, 119: 1.0 → 0.78} |
4 | Mix |
PARAM-Level-spinner-up |
{98: 6, 29: True, 26: 0, 28: 5, 119: 0.0 → -0.1} |
5 | Level |
Index 3 (Emphasis) was never touched, and no mark ever produced 28: 3 — the gap is
exactly where it should be.
So: 98 is the block index and 28 is the parameter index. 26 is neither.
[confirmed]
4.1 Key 28 indexes Helix.sym, not .models
Helix.sym lists 5 parameters for HD2_Cab4x121960T75; cab.models lists 7 for the
same model, because it adds the structural pseudo-parameters @mic and @enabled.
The preset stores exactly the 5, in Helix.sym order. Index against the Helix.sym
parameter list (or filter the @-prefixed entries out of .models). [confirmed]
4.2 Key 119 is the parameter’s native value, not a normalised 0..1
HelixModelDefs’ entry for LA Studio Comp settles this:
| Param | valueType |
min |
max |
wire value seen |
|---|---|---|---|---|
| PeakReduction | 1 (float) | 0.0 | 1.0 | 0.33, 0.05 |
| Gain | 1 | 0.0 | 1.0 | 0.57, 0.34 |
| Type | 2 (bool) | false | true | False, True |
| Mix | 1 (percent) |
0.0 | 1.0 | 1.0, 0.78 |
| Level | 1 (volume) |
-120.0 | 12.0 | 0.0, -0.099998 |
Level moved by exactly one 0.1 dB spinner step to -0.1 dB — a raw decibel value,
impossible under a 0..1 normalisation. (The mark is named -spinner-up but the value
went down; the click landed on the down arrow. The step size is what matters.) The
same holds elsewhere in the preset: slot 3’s cab stores LowCut 80.0 (Hz),
HighCut 8000.0 (Hz) and Distance 3.0 (inches), while the amp’s twelve knobs are
all 0..1.
Key 119 carries the value in whatever units HelixModelDefs declares via
min/max/valueType for that model’s parameter. Some are 0..1 (knobs shown
0.0–10.0, percentages shown 0–100 %), some are real units (dB, Hz, inches, ms), some
are bools. There is no single scaling. [confirmed]
Booleans are MessagePack true/false (0xc3/0xc2), not floats. For LA Studio
Comp’s Type, False = Compress and True = Limit, from the marks. [confirmed]
Floats are float32, so values round-trip inexactly — -0.1 arrives as -0.099998.
Compare with a tolerance.
4.3 One gesture is three requests
Every slider drag, toggle click and spinner click sent three op30s:
177.369 --> {102: 1013, 100: 30, 101: {98: 6, 29: True, 26: 0, 28: 0, 119: 0.33}} # old value
177.371 <-- {102: 1013, 103: 0, 104: {98: 6, 29: True, 26: 0, 28: 0, 119: 0.33}}
177.419 --> {102: 1014, 100: 30, 101: {98: 6, 29: True, 26: 0, 28: 0, 119: 0.05}} # new value
177.422 <-- {102: 1014, 103: 0, 104: {98: 6, 29: True, 26: 0, 28: 0, 119: 0.05}}
177.423 <-- {105: 30, 106: {82: 0, 68: 6, 121: 20, 106: {98: 6, ..., 119: 0.05}}}
177.471 --> {102: 1015, 100: 30, 101: {98: 6, 29: True, 26: 0, 28: 0, 119: 0.05}} # re-commit
177.474 <-- {102: 1015, 103: 0, 104: {98: 6, 29: True, 26: 0, 28: 0, 119: 0.05}}
The first request always re-sends the value the preset already held (verified against the preset document for all seven parameter marks), the second carries the new value, the third repeats it. Only the value-changing request produces notification 30. This looks like HX Edit’s mouse-down / move / mouse-up handling rather than anything the protocol requires; a client can send one op30 [inferred] — nothing in the capture suggests the first and third are needed.
The wire form of the middle request and the resulting notification, verbatim:
--> 83 66 cd 03 f6 64 1e 65 85 62 06 1d c3 1a 00 1c 00 77 ca 3d 4c cc cd
<-- 82 69 1e 6a 84 52 00 44 06 79 14 6a 85 62 06 1d c3 1a 00 1c 00 77 ca 3d 4c cc cd
4.4 What keys 26 and 29 are — not determined
26 was 0 and 29 was True in all 21 op30 calls across all three captures, so
neither can be pinned. [open]. Two readings of 26 fit everything seen, and the
capture cannot separate them:
- Which of the slot’s two models — a slot carries a primary parameter group
(key
11) and a secondary one (key12) for the cab half of an Amp+Cab.26would select between them, and would have stayed 0 because no cab parameter was ever edited. This is supported by26naming the secondary model id inside a model descriptor, and by op78{98, 26}addressing the same pair. - Path index — HX Stomp has one signal path, so it would always be 0.
The experiment that settles it: select the Amp+Cab block, edit a cab parameter
(Low Cut), and see whether 26 becomes 1 or 28 continues past the amp’s 12
parameters.
29 may be a “commit to the preset” or “this came from the editor” flag; nothing in
the sweep varies it.
4.5 A mark whose label is wrong
PARAM-Drive-set in 02-ui-actions is labelled for a distortion block, but by then
the operator had loaded preset 12 (FX:5th Then 7th), whose slot 1 is an LA Studio
Comp. The request {98: 1, 29: True, 26: 0, 28: 0, 119: 0.78 → 0.19} matches that
preset’s stored PeakReduction 0.78 exactly. The addressing is confirmed; the
label is not. Read that mark as “some parameter 0 of slot 1”.
5. Block and model representation
A block is addressed by its index in the preset document’s slot array
preset[0][22], a fixed 20-entry array. [confirmed] Key 98 is that index
everywhere it appears.
| Index | Contents on HX Stomp |
|---|---|
| 0 | input (19: 0) |
| 1..8 | path A block slots (19: 6, or 19: 8 when empty) |
| 9 | output (19: 1) |
| 10 | split (19: 2) |
| 11..18 | path B block slots |
| 19 | join (19: 3) |
The 1..8 / 11..18 split is [inferred] — preset FX:5th Then 7th has processing
blocks at 1, 3, 4, 5, 6 and at 12, with a split at 10 and a join at 19, which is
exactly one parallel path.
5.1 Model identity on the wire — resolved
The numeric model id in keys 25 and 26 is the zero-based index into
Helix.sym. [confirmed] This closes the “numeric model id ↔ symbolic name”
gap that has since been closed — see PROTOCOL.md on the section table and
Preset::computed_sections.
Validated on ten independent models — both the symbolic name and the parameter count match what the preset serialises:
| id | Helix.sym symbol |
params in .sym |
params in preset | where |
|---|---|---|---|---|
| 18 | HD2_AmpCaliRectifire |
12 | 11: {2: 12, 3: 12} |
CT-Sad slot 3 |
| 64 | HD2_Cab4X12CaliV30 |
5 | 12: {3: 5, 4: [3.0, 80.0, 8000.0, 0.0, 0.0]} |
slot 3’s cab, captures 01/02 |
| 62 | HD2_Cab4x121960T75 |
5 | 12: {3: 5, 4: [8.0, 19.9, 10200.0, 0.24, 0.0]} |
slot 3’s cab, capture 03 |
| 80 | HD2_DelaySimpleDelayMono |
6 | 6 values | FX preset slot 3 |
| 101 | HD2_DistScream808Mono |
3 | 11: {2: 3, 3: 3} |
CT-Sad slot 1 |
| 120 | HD2_CompressorLAStudioCompMono |
6 | 6 values | CT-Sad slot 6 |
| 129 | HD2_EQGraphic10BandMono |
11 | 11 values | CT-Sad slot 5 |
| 151 | HD2_AppDSPFlowJoin |
6 | 6 values | slot 19 |
| 257 | HD2_AppDSPFlowSplitY |
3 | 3 values | slot 10 |
| 323 | HD2_CaliQMono |
6 | 6 values | CT-Sad slots 2, 4 |
| 388 | HD2_DistKinkyBoostMono |
3 | op40 reply [0.55, False, False] |
MODEL-change |
| 510 | HD2_DistDerangedMasterMono |
4 | op40 reply [0.77, 0.522, 0.65, 0] |
MODEL-change |
The cab case is the strongest single check: model 64’s stored
[3.0, 80.0, 8000.0, 0.0, 0.0] lines up with Helix.sym’s
['Distance', 'LowCut', 'HighCut', 'EarlyReflections', 'Level'] and with
cab.models’ HD2_Cab4X12CaliV30 defaults of exactly 3.0 in, 80 Hz and 8000 Hz —
five values, five names, five defaults, all in order. Model 62 in capture 03 has the
same shape with the user’s own values, and its high cut of 10200 Hz sits inside the
declared 500–20100 Hz range.
Helix.sym is Line 6 proprietary data shipped inside HX Edit. Read it from the
user’s own installation at runtime; do not vendor it. See the model catalog.
5.2 Model descriptor
24: {23: <paired>, 25: <primary model id>, 26: <secondary model id or -1>}
For an ordinary block: {23: False, 25: n, 26: -1}. For CT-Sad’s Amp+Cab slot:
{23: True, 25: 18, 26: 62} — amp HD2_AmpCaliRectifire plus cab
HD2_Cab4x121960T75, with parameter group 11 holding the amp’s 12 values and group
12 the cab’s 5. [confirmed] that this is the layout; [inferred] that 23
means “paired”.
Split and join slots put the model id in key 8 instead
(15: {8: 257, ...}, 17: {8: 151, ...}) with parameters under key 7.
[confirmed] from the same cross-check.
5.3 Changing a model
--> {102: t, 100: 40, 101: {98: <slot>, 100: {23: False, 25: <new model>, 26: -1}}}
<-- {102: t, 103: 0, 104: {13: 1, 24: <the rebuilt slot object>}}
The device supplies the new model’s default parameters in the reply. Setting an
Amp+Cab pair presumably means sending {23: True, 25: amp, 26: cab}, but that was
never captured — [inferred].
The device never enumerates models. All fifteen CAT-* marks and CAT-Distortion-open
produced zero USB traffic: HX Edit’s model browser is
served entirely from its local catalog files. A third-party editor must read
HX_ModelCatalog.json / HelixModelDefs.bin / Helix.sym from the user’s HX Edit
installation, or ship its own table. [confirmed]
6. Presets, setlists, snapshots, tempo, globals, undo/redo, copy
6.1 Setlists and preset lists — [confirmed]
op0 returns [{0: 'PRESETS'}]: one setlist, index 0, named PRESETS. op1
{107: 0, 101: 2} returns 126 entries {index: {109: name, 123: False, 124: False,
125: 0}}. Flags 123/124/125 never varied → [open].
6.2 Loading a preset — [confirmed]
Selecting a preset in the librarian list sends nothing; the three
SELECT-PRESET-* marks in 02-ui-actions produced zero traffic. Only opening it
(double-click) does:
op20 {107: 0, 108: 12} -> 103: 1
ev20 {102: <same txn>, 103: 0} # completion
ev8 (1,1,5) {107: 0, 108: 12} # load started
ev22 (0,9,25) {118: 16, 119: 120.0} # tempo of the new preset
ev39 (1,3,19) {98: 4, 26: 0} # its saved cursor position
ev22 (0,9,25) {118: 28, 119: 12} # current preset index
ev4 (1,1,6) {107: 0, 108: 12} # load finished
op23 nil -> metadata incl. the name
op22 nil -> the preset document
The name is not in the preset document; it comes from op23. HX Edit re-reads both after the load rather than trusting a cached copy.
6.3 Undo and redo — [confirmed], and they are client-side
UNDO after two model changes sent opcode 21 with a complete 2643-byte preset
document in key 110 — HX Edit pushed the previous state wholesale rather than
asking the device to undo:
--> {102: 1041, 100: 21, 101: {110: <'l6-helix' + section table + preset map>}}
<-- {102: 1041, 103: 1, 104: None}
<-- {105: 39, ...} # cursor restored to block 1
<-- {105: 20, 106: {102: 1041, 103: 0}} # completion
<-- {105: 21, 106: None}
REDO did not use opcode 21 — it re-issued the original op40 {98: 1, 100:
{23: False, 25: 510, 26: -1}}. So the undo stack lives in the editor, and opcode 21
is really “write this preset document into the edit buffer” — which is also the
opcode a third-party tool would use to upload a preset. [inferred] that it is a
general write rather than an undo-specific call; nothing observed contradicts it, but
it was only ever seen carrying a previously-read document.
Notification 21 follows the completion of every document write — fourteen
consecutive captured undos all show 20 then 21. It reads as a post-commit
tick; nothing acts on it. [confirmed pattern]
6.4 Snapshots — no traffic, no conclusion
SNAPSHOT-selector-open and SNAPSHOT-pick both produced zero USB traffic.
Either the picker is local until a different snapshot is chosen, or the click landed
on the snapshot already active. No opcode for switching snapshots was captured here, but it is opcode 88 —
found later by driving the snapshots from the keyboard rather than the mouse,
since HX Edit sends nothing when a click lands on the already-active snapshot.
See PROTOCOL.md. [confirmed]
What the capture does show is where snapshots live in the preset document: key 10
holds {6, 7, 8, 9: 20, 10: [<3 snapshot objects>], 13: [bool × 20]}, each snapshot
carrying its name (4: 'SNAPSHOT 1'), tempo (5: 120.0) and per-block state.
10.8 was 0 for CT-Sad and 1 for FX:5th Then 7th, so 10.8 is plausibly the
active snapshot index [inferred].
6.5 Tempo
TEMPO-click produced zero traffic — the click opened a local control.
Tempo itself is reachable: it is device object id 16, reported as
{118: 16, 119: 120.0} and stored in the preset as 5.16. [confirmed] for the
identity of the object; the opcode that writes it is [open] (op24 is a read;
no write was captured).
6.6 Global settings
GLOBAL-settings-gear read objects 95–99 via op24. The three GLOBAL-tab-* marks
and GLOBAL-close produced zero traffic — the dialog’s tabs are local and
nothing was changed, so no write opcode was exercised. [open].
6.7 Preset copy
PRESET-copy and PRESET-select-target both produced zero traffic (keep-alives
only) — so copy in the librarian is a client-side clipboard operation, which is an
answer rather than a gap. Paste is a document write (op21): this project implements
copy, paste, import and export that way, and the round trip is verified byte-exact
against hardware. [confirmed]
7. Marks that produced no traffic
Reported honestly rather than guessed at. 31 of the 49 marks are here — only 18 marks in the sweep produced any application traffic at all.
| Mark(s) | Reading |
|---|---|
BLOCK-select-Dist |
The block was already selected — the earlier TEST-MARK click had selected slot 1, and re-clicking the selected block sends nothing. The later BLOCK-select-Dist-again did send op78 {98: 1, 26: 0}. |
CAT-* (15 marks), CAT-Distortion-open |
The model browser is local; the device is not consulted. |
TAB-* (5 marks) |
Editor tabs are local. |
SNAPSHOT-selector-open, SNAPSHOT-pick |
See §6.4 — cannot distinguish “local UI” from “click missed”. |
TEMPO-click |
Local control. |
GLOBAL-tab-* (3), GLOBAL-close |
Local; nothing was changed. |
PRESET-copy, PRESET-select-target |
Client-side clipboard; librarian selection is local. |
Verified with attribute.py --marks, which counts transfers per mark as well as
decoded messages: each of these windows contains only keep-alives and bare
acknowledgements, so the silence is real and not a decoding failure.
8. Open questions worth one more capture
- What key 26 selects in op30 — edit a cab parameter on an Amp+Cab block (§4.4).
- The write side of op24 — change a value in Global Settings while capturing.
- Snapshot switching — switch to a genuinely different snapshot.
- Real error codes — send a malformed request (e.g. a block index of 99) and see
what
103comes back. - Key 29 in op30 — no idea how to vary it from the UI.
- Whether op21 accepts an arbitrary preset document — the only way to know whether a third-party tool can upload presets with it.
- Notification 22/(0,10,27) — 479 argument-free ticks on a 75 ms grid.