roomi-fields/electra-one-mcp/dev-electra-one
Claude Code and agent skill by roomi-fields/electra-one-mcp
- community
Checking that the SKILL.md file is still in the repository.
Use this skill BEFORE writing or modifying any Lua/preset code targeting an Electra One MIDI controller (MK2/Mini), or pushing a preset via Firestore PATCH. Activate whenever the user mentions Electra One, MK2, electraone-widgets, a .lua file under widgets/, lib/theme.lua, the preset JSON, app.electra.one, setPaintCallback, setTouchCallback, setPotCallback, or anything about pushing/uploading widgets to the device. Reading the project's HOWTO.md is mandatory before touching code — multiple sessions have re-paid hours of debug time rediscovering things already documented (graphics.print not drawText, 0xRRGGBB colours, WIDTH/HEIGHT constants, integer-coord requirement, etc.).
Install
npx vskill@latest install roomi-fields/electra-one-mcp/dev-electra-oneSKILL.md
dev-electra-one
You are about to work on the Electra One MK2 MIDI controller — a programmable device whose Lua API and firmware behave in several non-obvious ways. This skill exists because we have lost hours, multiple times, rediscovering things that are already documented in HOWTO.md or in this skill. Do not start typing Lua before you've read the relevant material.
Trust the documented quirks. Most of them are silent failures: the widget renders as a default blue fader fallback, with no error in the browser console because the logger is off by default. By the time you "see" the problem, your only feedback is a bug-report-shaped silence.
Always do this first
- Read the official doc via RTFM, not via WebFetch. The
electraone-widgets project workspace has a local mirror of docs.electra.one at .electra-docs/md/ (79 pages, including the entire Lua crash course) indexed by RTFM. Use mcp__rtfm__rtfm_search "<query>" to find relevant sections, mcp__rtfm__rtfm_expand to read them. The official docs ARE the source of truth — most behaviours we previously thought were "undocumented gotchas" turn out to be explicitly described once you grep the right page.
- Open the project's `HOWTO.md` sections 1 and 2 for the
workflow we use (custom-paint pattern, Firestore PATCH push).
- Check `MEMORY.md` entry
project_e1_device_gotchasfor any
firmware-specific quirks learned that aren't in the docs.
- Only after doing the above, write code.
If a piece of behaviour isn't covered in those documents and you have to figure it out from scratch, add it back to `HOWTO.md` before considering the work done. Don't invent gotchas from intuition — grep the local doc mirror first.
The Lua API on device (firmware 4.1.4, verified 2026-05-25)
These are the things the official docs at docs.electra.one/developers/luaext.html either don't say or don't make obvious. Treat every one of these as a hard constraint, not a recommendation — most cause silent fallback to the default blue fader rendering.
Text drawing
graphics.drawText does not exist. Calling it raises attempt to call a nil value (field 'drawText') at paint time. For natural LEFT-aligned text where you've already computed x, pass a large width (e.g. 9999) so it never truncates.
Colour values
Pass 0xRRGGBB 24-bit integers. The firmware (4.1.4+) handles the RGB565 conversion internally. Do not pre-convert to RGB565 — the v4.1.4 release notes specifically fixed RGB888→RGB565 translation, so what worked before might surprise you now.
Integer coordinates
Every graphics.fillRect / drawRect / drawLine / fillCircle / drawCircle / print argument must be an integer. A float coordinate raises number has no integer representation and aborts the paint callback. Any expression involving a / N division produces a float — wrap it in math.floor(...). The Theme.rect / outline / line helpers in lib/theme.lua already do this internally; coordinates passed direct to graphics.* do not.
Bounds and event types
Touch event types are global constants: `DOWN`, `MOVE`, `UP`, `CLICK`, `DOUBLECLICK`. Yes, CLICK and DOUBLECLICK are dispatched natively by the firmware — you don't need to implement a click-detection state machine in Lua. Touch coordinates event.x / event.y arrive in tile-local coordinates (origin at the top-left of the tile's bounds).
Pots / encoders — the empirical truth (firmware 4.1.4)
`inputs[]` is IGNORED for `type:"custom"` tiles. Tested 2026-05-25 on firmware 4.1.4: declaring 2 entries {potId: 1, valueId: "step"} + {potId: 2, valueId: "edit"} on a custom tile produces NO Lua callback events AND no USB MIDI traffic when the user rotates those extra pots. The multi-input mechanism documented for ADSR/fader controls (see presetformat.md "an ADSR control with multiple values assigned") does NOT apply to custom controls. This is the exact blocker for the NewIgnis spec — feature request #4172.
Each custom tile receives exactly one pot. Which pot it is depends on the tile's slotId. Observed mapping on a default-configured MK2:
- tile in
slotId: 0(top row, full-width) → receivesev.id = 6
(= leftmost pot of the bottom physical row)
- tile in
slotId: 6(second row, full-width) → receivesev.id = 0
(= leftmost pot of the top physical row)
So the firmware delivers each tile's pot from the OPPOSITE physical row. The other 10 pots produce nothing for this tile (no callback, no MIDI). If "top pot controls top lane" matters for UX, route the action manually in your potCallback (cross-dispatch):
Encoder press not distinguishable from rotation start. events.onPotTouchChange(potId, controlId, touched) fires on capacitive touch but cannot tell "pressed without turning" from "started to turn". Forum threads #4116 and #4185 confirm: no onPotClick. For a click-without-rotation gesture, track a rotatedDuringTouch flag (set true on MOVE, reset on DOWN) and treat UP-without-MOVE as click. Even this is fiddly — the user has to be quite gentle to avoid an accidental tiny rotation.
Observing parameter changes from Lua
parameterMap.onChange is a global function you DEFINE (like preset.onLoad), not one you call:
Trying to call parameterMap.onChange(vo, callback) raises "attempt to call a nil value (field 'onChange')" — there is no such method. The dispatch is automatic: the firmware invokes the global when any ParameterMap entry changes (from MIDI in, from a pot mapped via inputs, from parameterMap.set, etc.).
origin is one of the documented enum values (ORIGIN_LUA, ORIGIN_MIDI, etc. — see .electra-docs/md/developers/luaext.md for the full list). Use it to skip echoing your own writes.
Touch on multi-tile presets
setTouchCallback fires reliably when the tile occupies the slot the firmware expects. If you reposition a tile via setBounds to a different visual area, the firmware still dispatches touches by slotId, not by the new bounds. event.x / event.y will then arrive in the original slot's coordinate space, hit-tests calculated against ctrl:getBounds() (which returns your overridden bounds) will miss, and tap-to-select silently does nothing. Resolution: pick slotId values that match where you want the tile, and only use setBounds to enlarge (e.g. to stretch a 1-slot tile to fullpage), not to relocate.
Bundling theme + primitives + widget
The project's scripts/bundle-preset.js concatenates lib/theme.lua + selected lib/primitives/*.lua + the widget's widget.lua into the preset's lua field for device upload. Key rules learned the hard way:
- No IIFE wrappers.
Theme = (function() local Theme = {} ... return Theme end)()
fails silently on device: closures inside the IIFE lose their upvalues after the load chunk completes, callbacks fall back, blue bar appears. Use flat concatenation: theme.lua declares Theme = Theme or {} at the top, drops return Theme, and primitives rewrite local function name to function Theme.name.
- No `local g = graphics` at module top level. Same closure problem.
Reference graphics directly inside each helper.
- No bitwise operators in code that runs at load time (top-level
expressions, module constants). The Lua runtime on some firmware revisions doesn't parse >> << & | (Lua < 5.3). If you need the RGB565 conversion, precompute the values in the bundler in Python/JS.
Callbacks must be global
paintLane, touchLane, potLane must be declared as top-level globals (function paintLane(ctrl) ... end), not as local closures captured by a factory or IIFE. A control:setPaintCallback(localFn) where localFn is a closure with upvalues falls back to default fader rendering after the chunk loads. Per-tile state lives in global tables keyed by ctrl:getId():
Preset JSON structure (schemaVersion 2 works for custom tiles)
Clone XT envelopes (GK6wmbgvwM6S3GanpoN7) as a template — its single custom tile renders correctly on device. Minimum tile fields:
Device JSON needs instrumentId: "generic-controls":
- `slotId` is 0-indexed. Top-left =
slotId: 0; second row left =
slotId: 6 on the MK2 6×6 grid.
- `span:"6"` (string) only reliably enlarges the FIRST custom tile in
the JSON. A second tile with span:"6" often renders as a single slot. Workaround: call ctrl:setBounds({...}) for every tile in preset.onLoad.
Logger
print(...) is the only way to write to the Lua tab's log window. The logger is OFF by default ("for performance reasons", per the docs) — the user has to enable it in the Lua tab UI before any print or fatal- error stack trace will appear. Firmware 4.1.4 added a fatal-error handler that prints debug + stack trace to the logger; harvest those when diagnosing silent failures.
controller.uptime() returns ms since boot — the only time API. Use it for double-click detection between events, debouncing, etc.
Firestore PATCH workflow (push to device without going through the UI)
This is the fast path to test changes on hardware. The full step-by-step is in HOWTO.md section 2; the short version:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/dev-electra-one/SKILL.md
Popularity
- Repository stars
- 5
- Repository forks
- 2
- 7-day trend score
- 9
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/roomi-fields/electra-one-mcp/dev-electra-one)