roomi-fields/electra-one-mcp/dev-electra-one

Claude Code and agent skill by roomi-fields/electra-one-mcp

Trust tier 3
  • community
Source check pending

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-one

SKILL.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

  1. 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.

  1. Open the project's `HOWTO.md` sections 1 and 2 for the

workflow we use (custom-paint pattern, Firestore PATCH push).

  1. Check `MEMORY.md` entry project_e1_device_gotchas for any

firmware-specific quirks learned that aren't in the docs.

  1. 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
lua
graphics.print(x, y, text, width, alignment)
-- alignment ∈ { LEFT, CENTER, RIGHT } (global constants)

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.

lua
Theme.ACCENT = 0xE5823E -- correct
-- Theme.ACCENT = 0xE407 -- WRONG, this is RGB565 reinterpreted as RGB888 → blue-ish
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
lua
local b = ctrl:getBounds()
local w, h = b[WIDTH], b[HEIGHT] -- WIDTH=3, HEIGHT=4, X=1, Y=2

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)
lua
ctrl:setPotCallback(function(ctrl, ev)
-- ev.id = 0-indexed pot number IN THE CALLBACK (0..11), even though
-- the doc and the JSON `inputs[].potId` use 1..12. This
-- off-by-one between doc and runtime is confirmed.
-- ev.type = DOWN | MOVE | UP (same constants as touch)
-- ev.delta = signed integer; 0 when no rotation (DOWN/UP frames)
end)

`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) → receives ev.id = 6

(= leftmost pot of the bottom physical row)

  • tile in slotId: 6 (second row, full-width) → receives ev.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):

lua
local sourceId = ctrl:getId()
local targetId = (sourceId == 1) and 2 or 1
-- operate on lanes[targetId] / selectedStep[targetId]

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:

lua
function parameterMap.onChange(valueObjects, origin, midiValue)
for i, vo in ipairs(valueObjects) do
local ctrl = vo:getControl()
print(string.format("changed: %s.%s = %d",
ctrl:getName(), vo:getId(), midiValue))
end
end

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():

lua
lanes = { [1] = {...}, [2] = {...} }
selectedStep = { [1] = 1, [2] = 1 }
dragging = { [1] = nil, [2] = nil }
muted = { [1] = {}, [2] = {} }
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:

json
{
"id": "<uuid>",
"reference": 1,
"slotId": 0,
"type": "custom",
"deviceId": 1,
"color": "F45C51",
"name": "MyTile",
"categoryId": "control",
"values": [{"message": {"type":"virtual","deviceId":1,"parameterNumber":99}}],
"visible": true,
"span": "6",
"vspan": 1
}

Device JSON needs instrumentId: "generic-controls":

json
"devices": [{"id":1, "name":"Generic MIDI", "instrumentId":"generic-controls", "port":1, "channel":1}]
  • `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:

bash
# 1. Extract the public Firebase API key from a fresh app.electra.one bundle
FIREBASE_API_KEY=$(for js in $(curl -s https://app.electra.one/presets/ | grep -oE '/_nuxt/[a-z0-9]+\.js' | sort -u); do
curl -s "https://app.electra.one$js" | grep -oE 'AIza[0-9A-Za-z_-]{35}'
done | sort -u | head -1)
# 2. Auth (use $ELECTRA_PASSWORD env var, do NOT commit the password)
TOKEN=$(curl -s -X POST \
"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=$FIREBASE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw "{\"email\":\"$ELECTRA_EMAIL\",\"password\":\"$ELECTRA_PASSWORD\",\"returnSecureToken\":true}" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['idToken'])")

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

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
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/roomi-fields/electra-one-mcp/dev-electra-one/badge)](https://verified-skill.com/skills/roomi-fields/electra-one-mcp/dev-electra-one)

Something wrong with this skill or its page? Tell us.