A cross-platform command-line suite and C++ client library for the Electra One SysEx protocol:
The library and command-line tool provide programmatic access to all Electra One SysEx commands. They can be used to manage the controller from the command line and to develop software applications that communicate with it without having to implement the underlying MIDI and SysEx communication details.
A small demo application is included to demonstrate how to use the C++ client library.
In order to build the application and the library the following dependencies are required:
- CMake 3.16+
- C++17 compiler
- RtMidi
- CLI11
- ArduinoJson
RtMidi, CLI11, and ArduinoJson are picked up via find_package if already
installed, otherwise fetched from source and built (as a static library)
The binaries can be build on all platforms (Lunix, MacOs, Windows) using:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build buildThis produces:
build/electraone- the command line application (CLI)build/libelectraone.a- the C++ client library, see C++ API library belowbuild/basic_usage- the example - set-DELECTRAONE_BUILD_EXAMPLES=OFFto skip it
Exact output paths / names differ slightly per platform.
A C++17 compiler and CMake are required. RtMidi's Linux backend needs ALSA development headers to build a working MIDI backend:
sudo apt install libasound2-dev (Debian/Ubuntu) or
sudo dnf install alsa-lib-devel (Fedora/RHEL).
Without these, CMake's own configure step will print Could NOT find ALSA and
RtMidi will build without a real MIDI backend. Install them before running
cmake -B build. Delete build/ and reconfigure if you installed them
afterward, since FetchContent's RtMidi detection runs once at configure time.
Xcode Command Line Tools (xcode-select --install) for a C++17 compiler,
CMake (brew install cmake, or the cmake.org
installer). RtMidi builds against CoreMIDI/CoreAudio/CoreFoundation, which
already is part of macOS.
Visual Studio 2022 (the free Community edition works) with the
"Desktop development with C++", which provides MSVC, the Windows
SDK, and CMake integration. Also install and configure Git if the VS installer
didn't already put it on PATH.
Build from a "Developer Command Prompt for VS" or via VS's built-in CMake support:
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake --build build also builds electraone_tests
(it can be skipped with -DELECTRAONE_BUILD_TESTS=OFF), covering the
electraone_api library's hardware-independent logic with
doctest.
To run the tests issue:
ctest --test-dir build --output-on-failureor run ./build/electraone_tests directly for doctest's own output (--help
for its filtering/reporting options).
Source style is enforced with clang-format (see .clang-format). Two
targets are provided via the top-level Makefile:
make format # reformat every source file in place
make format-check # fail (nonzero exit) if any file isn't already formattedCI runs make format-check on every push and pull request (see
.github/workflows/ci.yml, job format-check) - reformat with make format
and commit the result if it fails. Configure this as a required status check
under the repo's branch protection settings to block merging until it passes.
Both targets need make, find, and clang-format on PATH. This works out
of the box on Linux and macOS. On native Windows (a Developer Command Prompt),
none of those three are available by default - use Git Bash, WSL, or
MSYS2/MinGW instead, or just rely on the CI check catching formatting issues
after pushing.
Once the command line tool is built, you can use it to interact wih the Electra One hardware controller. The first step is to review the MIDI ports:
electraone list-portsThe command lists all MIDI input/output ports available on the host computer.
By default, every command connects to the port whose name contains CTRL
(case-insensitive). In general, this will work on Linux and MacOs.
If the default CTRL port does not work, specify the Electra One control port
using the command line parameters:
--port <substring>to match a different substring--port-index <n>to use a specific port index (same one for both output and input) fromlist-portsdirectly, bypassing name matching--out-port-index <n>/--in-port-index <n>for situations when MIDI input and output have different index number, see bellow.
Windows note: WinMM's port names may not contain CTRL (they're generic,
e.g. MIDIOUT3 (Electra Controller)), so name matching always fails there and
--port-index (or the pair above) is required. Windows also commonly lists an
extra software-only output port (e.g. Microsoft GS Wavetable Synth) ahead of
any real devices, with no matching input port. That shifts every subsequent
output index one higher than the same device's input index. Run list-ports
and compare: if the Electra's output and input entries land at different
positions, use --out-port-index/--in-port-index instead of --port-index
to address them independently.
Besides the options to specify connection ports, there is a handful of other global options. The global options must be given before the subcommand name. They are:
--timeout <ms>— reply wait timeout, default 3000--txn-id <n>— attach an optional 14-bit transaction ID to the request (firmware 4.0+)-o, --output <file>— write the response to a file instead of stdout--raw— print the raw response bytes as hex instead of decoding--pretty— pretty-print JSON responses with indentation for human reading (non-JSON payloads, e.g. Lua source, are printed unchanged; has no effect together with--raw)--human— render JSON responses for human reading rather than as JSON. It akes precedence over--pretty; no-hshort form since that's already--helpon every subcommand.
The command line tool completes the its task it returns an exit code depending on the result of the task.
Exit codes:
0ACK (success)1NACK (failure)2timeout or transport/protocol error3CLI usage error
Any command that changes the state or data on the controller can be associated with a transaction ID. The primary purpose of transaction IDs is to link an ACK/NACK response to the original request.
Transaction IDs are 14-bit numbers. The host is responsible for generating them and ensuring their uniqueness.
Data requests, such as list and information queries, do not support transaction IDs.
Run electraone <group> --help or electraone <group> <subcommand> --help for
full details about groups and their sub-commands.
| Group | Subcommands |
|---|---|
list-ports |
|
info |
|
runtime-info |
|
reboot |
|
preset |
get, upload <file>, remove, list, slot-info, switch, set-slot, reload, load-preloaded |
lua |
get, upload <file>, remove, exec <code>|--file |
overrides |
get, upload <file> |
persisted |
get, upload <file> |
performance |
get, upload <file> |
config |
get, upload <file>, remove |
snapshot |
list, get, update, remove, swap, set-slot |
capture |
list, get, update, remove, swap, set-slot |
control |
update, override-text |
parameter-map |
list |
midi-learn |
enable, disable, listen |
usb-devices |
list |
ui |
page-switch, control-set-switch, bottom-bar-text |
events |
set-port, subscribe, listen |
logger |
enable, disable, set-port, listen |
window |
stop, resume |
debug |
enable, disable, set-breakpoints |
files |
open, register, send-chunk, commit, list, remove, upload <file> (composite) — see below |
screenshot |
electraone info
electraone --pretty preset list
electraone --human preset list
electraone preset get --bank 0 --slot 1
electraone preset upload my-preset.json
electraone snapshot list --project-id nljaziUjglOuD1fe15Eq
electraone lua exec "print('hello')"
electraone control update --id 42 --value-text "hello"
electraone events listen --pots --touch --duration 30File arguments accept - to read from stdin,
e.g. cat preset.json | electraone preset upload -.
The command line not only sends commands to the Electra One controller. It can also listen to events and log messages that the controller emits.
The commands to listen are:
events listenlogger listenmidi-learn listen
All listen commands print incoming messages as [HH:MM:SS.mmm] <description>,
where the timestamp is a host-side wall-clock timestamp on every line and
the <description> is the message or event sent by the controller.
logger listen subscribes to controller events in addition to enabling the
logger (same --page --control-set --usb-host --pots --touch
--button --window --all flags as events listen, defaulting to all event
types), so you see Pot Touch / Page Switch / etc. interleaved with log messages
in one stream instead of having to run two terminals:
electraone logger listen --duration 30
electraone logger listen --pots --touch # log messages + just these event types--timeout option does not apply to listen commands. listen is inherently
open-ended. With no --duration option specified, it waits indefinitely
until Ctrl+C, and with --duration N it gives up once N seconds have passed.
The File Transfer API, available in firmware 4.0 and later, supports uploading, listing, and removing files stored on the controller.
File uploads use an atomic transaction:
- Open a cache.
- Register one or more files and specify their sizes.
- Transfer each file in chunks.
- Commit the transaction with an MD5 checksum for each file.
During the commit step, the controller verifies the checksums of all transferred files. It then either applies every file in the transaction or rejects the entire transaction.
For the common case of uploading a single file, use the composite upload
command. It performs the entire transaction automatically:
electraone files upload firmware-mk2-v4.1.4.srec \
--location updates \
--type firmware \
--chunk-size 32768
electraone files upload my-preset.json \
--location slots \
--type preset \
--bank 0 \
--slot 1The --location option specifies where the file will be stored:
slots— preset slots that users can navigate from the controller UIupdates— staging area for updates applied during the next rebootassets— UI assets, fonts, and related filesmodules— preloaded Lua modulespresets— preloaded presets
The --type option specifies the type of file being uploaded:
firmware— Electra One firmwarebootloader— Electra One bootloader; use this type with cautionpreset— preset JSON filelua— preset Lua source fileluaModule— shared Lua module that can be used by preset Lua scriptsui— UI asset fileconfig— controller configuration JSON filedeviceList— JSON file containing device override definitionsdatafile— JSON file containing persistent data generated by a Lua scriptperformance— performance JSON file
Different locations use different destination addressing schemes:
- Preset
slotsare identified by--bankand--slot, using 0-based index. - Preloaded
modulesandpresetsare identified by--namespaceand--path. The namespace is the top-level directory, typically named after the maintainer. The path identifies a file or subdirectory within that namespace.
When transferring multiple files in a single transaction, each file must be
assigned an identifier with --id.
The identifier defaults to 1 and only needs to be unique within the current
transaction.
Files are transferred in smaller chunks. Use --chunk-size to control the
amount of file data included in each transfer-chunk message. The default chunk
size is 256 bytes.
While sending chunks, files upload shows a progress bar that updates in
place:
[================> ] 52% 14000/26682 bytes
The bar only appears when stdout is an actual terminal - if the output is
redirected or piped, nothing is printed per chunk (the MD5/step/summary lines
around the transfer are still shown either way). Pass --no-progress to
suppress the bar entirely, even on a terminal.
During the commit step, the controller validates the MD5 checksum of every file before applying any changes. For large files, this step can take significantly longer than other commands.
The --commit-timeout option controls the timeout for the commit step only. Its
default value is 60,000 ms, and it is independent of the general --timeout
option.
The following low-level commands are available:
files openfiles registerfiles send-chunkfiles commitfiles listfiles remove
are exposed individually, e.g. for scripting a multi-file transaction.
The electraone-cli repository also builds libelectraone.a, a static C++
library that provides programmatic access to the same Electra One SysEx protocol
used by the CLI.
The public headers are located in include/electraone/:
ElectraOneClient.hpp— high-level client APIsysex.hpp— lower-level SysEx types and utilities
The library is intended for embedding Electra One support directly into third-party applications.
The public API depends only on the C++ Standard Library. RtMidi is required when linking the application, but RtMidi types and implementation details are not exposed through the public API.
#include <electraone/ElectraOneClient.hpp>
electraone::Client client;
client.connect(); // Throws std::runtime_error on failure.
auto info = client.getElectraInfo();
if (!info) {
// The controller did not reply before the timeout.
} else if (info->isNack) {
// The controller rejected the request.
} else {
std::cout << info->payloadAsText(); // JSON response.
}
client.switchPage(2);
electraone::UploadFileOptions options;
options.location = "slots";
options.type = "preset";
options.bank = 0;
options.slot = 1;
client.uploadFile(fileBytes, options); // Throws on failure.Command methods return std::optional<Response>, where Response is an alias
for sysex::ParsedResponse.
The return value should be interpreted as follows:
std::nullopt— the controller did not reply before the timeoutresponse.isAck— the controller accepted the requestresponse.isNack— the controller rejected the requestresponse.payloadAsText()— returns the response payload as textresponse.payload— provides access to the raw payload bytes
Use Client::poll(timeoutMs) to wait for the next incoming message without
sending a request.
This is useful after enabling event streams with methods such as
subscribeEvents() or enableLogger().
The function describeEvent
electraone::describeEvent(response)converts an event response into a human-readable description.
Client::uploadFile() performs the complete File Transfer API transaction:
- Open the transfer cache.
- Register the file.
- Send the file in chunks.
- Commit the transaction.
If any step fails, the method throws std::runtime_error. The exception message
identifies the failed step.
During the commit step, the controller validates the file's MD5 checksum before applying it. This can take significantly longer than other commands, particularly for large files.
For that reason, file uploads have a separate commit timeout:
UploadFileOptions::commitTimeoutMsThe default value is 60 seconds, and it is independent of
ConnectOptions::timeoutMs.
When using the low-level transfer API directly, Client::commitTransaction()
also accepts its own timeoutMs parameter.
The C++ API mirrors the command groups provided by the CLI.
See the following files for more information:
- ElectraOneClient.hpp — complete API signatures
- basic_usage.cpp — runnable example
- examples/Makefile — standalone example build
Build the library and run the example with:
cmake -B build
cmake --build build
cd examples
make
./basic_usageThe recommended way to use the library from another CMake project is to include
this repository with add_subdirectory() or FetchContent, and then link
against the electraone_api target:
target_link_libraries(your_app PRIVATE electraone_api)The target automatically propagates the required RtMidi dependency and the platform-specific MIDI libraries:
- macOS — CoreMIDI, CoreAudio, and CoreFoundation
- Linux — ALSA and pthreads
- Windows — WinMM
No additional link configuration is normally required.
The basic_usage example uses the same approach. See
CMakeLists.txt for the corresponding add_executable() and
target_link_libraries() calls.
When linking manually, include:
libelectraone.aorelectraone.lib- the RtMidi static library
- the platform-specific MIDI libraries
c++ -std=c++17 \
-I include \
your_app.cpp \
build/libelectraone.a \
build/_deps/rtmidi-build/librtmidi.a \
-framework CoreMIDI \
-framework CoreAudio \
-framework CoreFoundation \
-o your_appc++ -std=c++17 \
-I include \
your_app.cpp \
build/libelectraone.a \
build/_deps/rtmidi-build/librtmidi.a \
-lasound \
-lpthread \
-o your_appRun the following command from a Visual Studio Developer Command Prompt:
cl /std:c++17 /I include ^
your_app.cpp ^
build\Release\electraone.lib ^
build\_deps\rtmidi-build\Release\rtmidi.lib ^
winmm.lib ^
/Fe:your_app.exeThe paths above assume a multi-configuration generator such as Visual Studio
and a Release build. Single-configuration generators, such as Ninja, may place
the libraries directly in the build directories without a Release
subdirectory.
The manual Windows command is provided as a best-effort example and has not been verified on a Windows system. The CMake approach is recommended because it does not depend on generator-specific output paths.
When RtMidi is provided by find_package() instead of being downloaded by the
project, replace the explicit RtMidi archive path with the system library:
- macOS or Linux:
-lrtmidi - Windows:
rtmidi.lib
A system package may provide RtMidi as a shared library. In that case, the resulting application may no longer be a fully self-contained binary. The library type is determined by the installed RtMidi package and is outside this project's control.