Skip to content

Commit 8af1084

Browse files
roomi-fieldsclaude
andcommitted
docs(electra-one): update fiche with findings from electra-one-mcp v0.1.0
Following a deep RE pass on the app.electra.one JS bundle + a full hardware validation session on firmware 4.1.4 for the sister electra-one-mcp Claude Code plugin, several gaps in this fiche became visible. This commit fills them. Updates: - §2 Control types: ADD `custom` (Lua-painted tiles). Critical for widget devs — was missing. - §2: ADD the "one pot per custom tile" firmware quirk (forum #4172), with a note explaining why `inputs[]` declaring multiple potIds is silently ignored for `type:"custom"`. - §2: Correct the screen height (1024×575 panel vs 1024×565 drawing area per the bundle's displayHeight constant). - §5 Events table: extend with the events the JSON `replies[]` array already emits but the MD didn't document — control_set/switched, snapshot_bank/switched, capture_list/changed, usb_host/changed — plus map each to its SysEx event code (7E XX / 7F XX). - NEW §7b: Schema gotcha — `tiles` vs `controls`. The web editor's internal schema (used in the electraone-widgets repo and in the Firestore-imported presets under `.cache/electra-import/`) is NOT what the device firmware parses via `/preset/upload`. Documents the symptom (silent ACK then empty screen) and points at the converter in the sister plugin. - NEW §7c: File Transfer API. Brief documentation of the 01 2D / 2E / 2F / 04 2D protocol, the supported type values, and the known commit-rollback gotcha when type="preset". - NEW §11: Full MK2 layout math reference. The slot_to_bounds / slot_to_pot / slot_to_set / slot_to_page_id / bounds_to_slot formulas extracted from the bundle (offsets ~129500 + ~159200 + ~134272), verified against a live MIDI sniff. Lets a Rust port of the converter live in osc-bridge if desired. - NEW §12: USB port routing nuance. The §8 recommendation (Port 1) is correct for osc-bridge's mixed config+CC workload, but pure SysEx admin clients (like electra-one-mcp's push_to_device) target Port 3 (CTRL) — matching app.electra.one's auto-selection regex. Documents both for cross-reference. - NEW §13: Pointer to the sister plugin's structured SysEx catalog (62 commands) so contributors looking to extend the driver have a starting reference. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c2cb2e8 commit 8af1084

1 file changed

Lines changed: 205 additions & 6 deletions

File tree

devices/electra-one/electra-one.md

Lines changed: 205 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Top-level:
5252
```
5353

5454
### Control types (all hardware-validated)
55-
`fader`, `vfader`, `list`, `pad`, `adsr`, `adr`, `dx7envelope`.
55+
`fader`, `vfader`, `list`, `pad`, `adsr`, `adr`, `dx7envelope`, **`custom`**.
5656

5757
- `fader` is the default vertical bar.
5858
- `list` requires an `overlays` entry (id-referenced from `values[].overlayId`)
@@ -61,6 +61,19 @@ Top-level:
6161
`onValue` / `offValue`.
6262
- `adsr` / `adr` / `dx7envelope` are multi-point visual envelopes. See § 6 for
6363
the envelope-segment-editing gesture — it's not what you'd guess.
64+
- **`custom`** is a Lua-painted tile (the canvas under your `setPaintCallback`).
65+
Required for any widget whose visual is implemented in Lua rather than as one
66+
of the firmware's built-in controls (step sequencers, custom meters, note-list
67+
editors, etc.). The `values[]` array still drives the parameter wiring, but
68+
the paint surface is entirely your code's responsibility.
69+
70+
**Custom-tile pot dispatch quirk (firmware 4.1.4)**: when `type:"custom"`, the
71+
firmware dispatches pot events to the tile from **one pot only**, regardless of
72+
how many entries the control's `inputs[]` array declares. Multi-pot custom
73+
controls is forum thread [#4172](https://forum.electra.one/t/multi-pot-custom-control/4172)
74+
— acknowledged as a requested feature, not yet shipped. Design custom widgets
75+
around one-pot interaction (mode toggles, double-click variants, touchscreen
76+
gestures) rather than multiple physical encoders per tile.
6477

6578
### Required field: `inputs`
6679
**Every control must declare `"inputs": [{"potId": N, "valueId": "value"}]`.**
@@ -93,9 +106,13 @@ Each **control set** can contain up to **12 controls**, one per physical pot
93106
KNOB1..KNOB12. `controlSetId` on a control (1, 2, or 3) places it in a specific
94107
set. Only one set is visually active at a time.
95108

96-
X/Y bounds are in pixel coordinates on the 1024×575 screen. Typical column
97-
layout for 6 columns: `x = 14, 181, 348, 515, 682, 849` with width 158. Typical
98-
two-row layout: `y = 40` (top) and `y = 320` (bottom).
109+
X/Y bounds are in pixel coordinates on the **1024×565 drawing area** (the
110+
physical panel is 1024×575 but the firmware's slot-bounds math caps at 565 —
111+
verified against `app.electra.one`'s `displayHeight` constant at offset
112+
~129500 in `0c61af0.js`). Typical column layout for 6 columns:
113+
`x = 14, 181, 348, 515, 682, 849` with width 158. Typical two-row layout:
114+
`y = 40` (top) and `y = 320` (bottom). Full slot-bounds math for any slotId
115+
is in §11 (Layout math reference).
99116

100117
### Upload targets the currently-selected slot
101118
`/preset/upload` writes to whatever slot was last selected on the device
@@ -156,8 +173,13 @@ The bridge forwards device-initiated events as they arrive:
156173
| `/electra1/preset/switched <bank> <slot>` | Loaded preset slot changed |
157174
| `/electra1/preset_bank/switched <bank>` | Bank changed |
158175
| `/electra1/pot/touch <pot_id> <control_lsb> <control_msb> <touched>` | Touch-sensitive pot began/ended contact |
159-
| `/electra1/preset_list/changed` | Presets list modified on device |
160-
| `/electra1/snapshot_list/changed` | Snapshot library modified |
176+
| `/electra1/preset_list/changed` | Presets list modified on device (`7E 05`) |
177+
| `/electra1/snapshot_list/changed` | Snapshot library modified (`7E 03`) |
178+
| `/electra1/snapshot_bank/switched <bank>` | User changed snapshot bank (`7E 04`) |
179+
| `/electra1/control_set/switched <set>` | User pressed SECTION button or tapped a tile of another set (`7E 07`) |
180+
| `/electra1/capture_list/changed` | MIDI capture library modified (`7E 31`) |
181+
| `/electra1/usb_host/changed` | USB Host port: a controller (re)connected on the device's USB-Host jack |
182+
| `/electra1/log <text>` | `print()` output and runtime Lua errors from device (`7F 00`) — *not yet emitted by osc-bridge; the SysEx arrives but the driver doesn't surface it as OSC. Easy add: one entry in `replies[]`* |
161183

162184
---
163185

@@ -271,6 +293,73 @@ live dialogue — is ~1 KB of Lua.
271293

272294
---
273295

296+
## 7b. Schema gotcha — `tiles` vs `controls`
297+
298+
If you're generating preset JSON from a tool that uses the web-editor's
299+
internal "tiles" schema (the format `app.electra.one` saves into Firestore and
300+
that the [electraone-widgets](https://github.com/roomi-fields/electraone-widgets) repo
301+
uses), you must convert it before uploading via SysEx — the firmware does not
302+
parse it.
303+
304+
Symptom: `/preset/upload` ACKs at the transport level, the device fires
305+
`7E 05` preset-list-change and `7E 02` preset-switch events as if everything
306+
worked, but the screen shows "no name - page 1" or stays empty, and
307+
`/preset/get` afterwards returns 0 bytes.
308+
309+
| Schema | Top-level keys | Where it lives |
310+
|---|---|---|
311+
| **`tiles` (web-editor internal)** | `schemaVersion`, `id`, `name`, `targetDevice`, `lua`, `devices`, `tiles[]`, `pages`, `categories`, `firstPageId` | `app.electra.one` Firestore + the electraone-widgets repo + `osc-bridge` `.cache/electra-import/` |
312+
| **`controls` (device firmware)** | `version`, `name`, `projectId`, `pages`, `devices`, `overlays`, `groups`, `controls[]` | The official [Preset format spec](https://docs.electra.one/developers/presetformat.html) — what `/preset/upload` must send. |
313+
314+
The web editor converts tiles → controls in JS (`projectToPreset` function,
315+
offset ~127000–131000 in bundle `0c61af0.js`) before calling its WebMIDI
316+
`output.send()`. If your osc-bridge client is generating presets from
317+
scratch following the `controls` schema described in §2, you're fine. If you're
318+
pulling JSON from Firestore or from the electraone-widgets repo, run it
319+
through the converter first.
320+
321+
A Python port of `projectToPreset` (byte-identical to the web editor, verified
322+
against a live MIDI sniff) lives in the
323+
[electra-one-mcp plugin](https://github.com/roomi-fields/electra-one-mcp) at
324+
`server/preset_converter.py`. Translate to Rust as needed.
325+
326+
The same plugin also ports `presetToProject` (the reverse direction) so you
327+
can pull a preset off the device and write it back in tiles form for
328+
`git diff`-style workflows.
329+
330+
---
331+
332+
## 7c. File Transfer API (for very large files / multi-file deploys)
333+
334+
In addition to the simple uploads in §2 (`01 01 Upload Preset`, `01 0C Upload
335+
Lua`, `01 0F` devices, `01 12` data, `01 11` performance), the device exposes
336+
a transactional file-transfer API for large or multi-file deploys:
337+
338+
| Step | SysEx | Purpose |
339+
|---|---|---|
340+
| 1 | `F0 00 21 45 01 2D F7` | Open cache (begin transaction) |
341+
| 2 | `F0 00 21 45 01 2E <fileId> <s0> <s1> <s2> <s3> F7` | Register a file + its size (4×7-bit LE) |
342+
| 3 | `F0 00 21 45 01 2F <fileId> <data> F7` | Send chunk (one SysEx per chunk; data must stay 7-bit) |
343+
| 4 | `F0 00 21 45 04 2D <commit-json> F7` | Commit + verify MD5 + move to final location |
344+
345+
`type:` values supported: `firmware`, `bootloader`, `preset`, `lua`,
346+
`luaModule`, `ui`, `config`, `deviceList`, `datafile`, `performance`.
347+
`location:` values: `slots`, `updates`, `assets`, `modules`, `presets`, `root`.
348+
349+
**Known limitation on firmware 4.1.4**: commit silently rolls back when
350+
`type:"preset"` is in the commit JSON. The web editor never uses FT for
351+
presets (only for `firmware`, `luaModule`, multi-file `slots` deploys with
352+
Lua + Devices + Performance + Persisted at once). Stick with the simple
353+
`01 01` + `01 0C` path for preset+lua and reserve FT for `luaModule`
354+
uploads (the only way to write to `/ctrlv2/lua/<namespace>/<file>.lua`
355+
without a host filesystem mount).
356+
357+
Reference: [filetransfer.html](https://docs.electra.one/developers/filetransfer.html) +
358+
[forum #592](https://forum.electra.one/t/command-line-preset-file-upload-tool/592)
359+
(staff posts #9, #16 on chunk sizes and reliability).
360+
361+
---
362+
274363
## 8. Hardware constraints
275364

276365
- **Max preset size**: ~100 KB of JSON. Larger layouts must be split.
@@ -327,3 +416,113 @@ When writing new client code against this device:
327416
5. When generating a preset JSON, prefer existing template-mutation over
328417
building from scratch — the format has many cross-references (pageId,
329418
deviceId) that are easy to get subtly wrong.
419+
6. If you have a `tiles`-schema JSON (web editor's internal format), convert
420+
to `controls` schema BEFORE upload (see §7b). The firmware silently
421+
rejects tiles JSON.
422+
423+
---
424+
425+
## 11. Layout math reference (MK2)
426+
427+
Extracted from `app.electra.one`'s bundle `0c61af0.js` (offsets ~129500
428+
defaults + ~159200 slot helpers + ~134272 reverse math). Verified against a
429+
live MIDI sniff: `slotId=1, span=1, vspan=0``bounds=[181, 6, 158, 16]`,
430+
`potId=8`, byte-identical to web editor output.
431+
432+
### Constants
433+
434+
```
435+
mk2: numPages=12, slotsPerPage=72, slotsPerRow=6, rowsOnPage=6
436+
slotsInSection=24, controlsInSection=12
437+
displayWidth=1024, displayHeight=565
438+
controlWidth=168, controlHeight=60
439+
groupWidth=168, groupHeight=30, groupSpanHeight=97
440+
maxVspan=6
441+
rowY=[0, 30, 90, 120, 180, 210, 270, 300, 360, 390, 450, 480]
442+
```
443+
444+
The 6×12 grid alternates rows: even-y rows (y=0,2,4,…) are label/group rows
445+
(h=30); odd-y rows are control rows (h=60). A "section" is one label + one
446+
control row pair; the device has 6 sections per page.
447+
448+
### Forward (slotId → bounds + pot)
449+
450+
```python
451+
def slot_to_bounds(slot, span=1, vspan=0):
452+
x = slot % 6
453+
y = (slot // 6) % 12
454+
if y % 2 == 0: # label/group row
455+
w = 146 * span + 21 * (span - 1) + 12
456+
h = (90 * vspan - 9) if vspan > 0 else 16
457+
px = 20 + 167 * x - 6
458+
else: # control row
459+
w, h = 146, 56
460+
px = 20 + 167 * x
461+
py = 6 + 22 * ((y // 2) + (y % 2)) + 68 * (y // 2)
462+
return [px, py, w, h]
463+
464+
def slot_to_pot(slot):
465+
x = slot % 6
466+
return x + 1 if (slot // 6) % 4 == 1 else x + 7
467+
468+
def slot_to_page_id(slot): return slot // 72 + 1
469+
def slot_to_set(slot): return ((slot // 6) % 12) // 4 + 1
470+
```
471+
472+
### Reverse (bounds → slotId)
473+
474+
```python
475+
def bounds_to_slot(bounds, page_id=1):
476+
col = bounds[0] // 170
477+
row_section = (bounds[1] - 6) // 90
478+
row_offset = 0 if (bounds[1] - 6) % 90 == 0 else 1
479+
return 72 * (page_id - 1) + 6 * (2 * row_section + row_offset) + col
480+
```
481+
482+
For the Mini variant (4 cols × 6 rows, 24 slots/page, different constants) see
483+
the bundle around offset 157783, or the matching `_MiniLayout` class in the
484+
[electra-one-mcp plugin](https://github.com/roomi-fields/electra-one-mcp/blob/main/server/preset_converter.py).
485+
486+
---
487+
488+
## 12. Note on USB port routing
489+
490+
§8 recommends Port 1 (`Electra Controller`) as the osc-bridge target for both
491+
SysEx config AND CC traffic. This holds for osc-bridge's mixed
492+
config-plus-runtime workflow, but for SysEx-only admin work (preset upload,
493+
file transfer, event subscription, executing Lua, querying state), the **CTRL
494+
port** is the conventional target:
495+
496+
- **Port 1**`Electra Controller` (no suffix). Carries CC/Note traffic to/from
497+
the active preset's MIDI device assignments. The bundle routes `app.electra.one`'s
498+
default MIDI here. Per-channel routing applies.
499+
- **Port 2**`MIDIOUT2 / MIDIIN2`. Same as Port 1 but for the second MIDI
500+
cable; pass-through to externally-connected gear via the device's MIDI
501+
passthrough.
502+
- **Port 3 (CTRL)**`MIDIOUT3 / MIDIIN3`. Admin / SysEx / events. The
503+
`app.electra.one` bundle's auto-selection regex prefers names matching
504+
`/Electra.*(CTRL|Port 3|MIDI 3)/i`. Every documented SysEx admin command in
505+
§2 + §6 + §7 works here. All `7E XX` unsolicited events come out of this
506+
port. CC sent here may light the device's USB LED but does not route to
507+
controls.
508+
509+
Practical guidance:
510+
- **osc-bridge default**: Port 1 (the existing recommendation). Carries both
511+
CC and SysEx; the device's command interpreter accepts most SysEx admin
512+
on any port.
513+
- **Pure SysEx admin / scripting clients** (preset push, Lua REPL, snapshot
514+
management, file transfer): Port 3 (CTRL) is the conventional target —
515+
guarantees no collision with active-preset CC traffic.
516+
517+
---
518+
519+
## 13. Cross-reference: complete SysEx catalog
520+
521+
For the full 62-command SysEx vocabulary (every host→device command + every
522+
`7E XX` / `7F XX` event), see:
523+
524+
- `electra-one.json` in this folder — what osc-bridge wraps as OSC routes
525+
- The [electra-one-mcp plugin](https://github.com/roomi-fields/electra-one-mcp)'s
526+
`docs/structured/sysex_commands.json` — same catalog with payload-shape
527+
metadata, queryable via its `get_sysex_command` MCP tool
528+
- [Official protocol doc](https://docs.electra.one/developers/midiimplementation.html)

0 commit comments

Comments
 (0)