Skip to content
Get started

BLE MIDI

The ble_midi component connects to a device that speaks MIDI over Bluetooth Low Energy — a wireless keyboard, a pad controller, a foot switch — and turns the notes, knobs and buttons it sends into automations. It can also send MIDI back to the device.

It does not create any entities of its own. What you do with a note or a knob is up to your automations.

The component needs a BLE Client to hold the connection, which in turn needs the ESP32 BLE Tracker to find the device.

WARNING

The BLE software stack on the ESP32 consumes a significant amount of RAM on the device.

Crashes are likely to occur if you include too many additional components in your device’s configuration. Memory-intensive components such as Voice Assistant and other audio components are most likely to cause issues.

esp32_ble_tracker:
ble_client:
- mac_address: XX:XX:XX:XX:XX:XX
id: midi_keyboard
ble_midi:
id: my_midi
ble_client_id: midi_keyboard
on_note_on:
- logger.log:
format: "Note %u pressed with velocity %u"
args: [note, velocity]
  • ble_client_id (Required, ID): ID of the associated BLE client.
  • id (Optional, ID): The ID to use for code generation, and for reference by the ble_midi.* actions.
  • pair (Optional, boolean): Pair with the device after connecting. Some devices refuse to send anything until they are paired. Defaults to false.
  • max_sysex_size (Optional, int): The size of the buffer used to reassemble System Exclusive messages, in bytes. Longer messages are truncated. Range: 8-2048. Defaults to 256.

Automations:

  • on_connect (Optional, Automation): An automation to perform when the MIDI device is connected and ready. See on_connect.
  • on_disconnect (Optional, Automation): An automation to perform when the MIDI device disconnects. See on_disconnect.
  • on_note_on (Optional, Automation): An automation to perform when a key or pad is pressed. See on_note_on.
  • on_note_off (Optional, Automation): An automation to perform when a key or pad is released. See on_note_off.
  • on_control_change (Optional, Automation): An automation to perform when a knob, fader or switch is moved. See on_control_change.
  • on_program_change (Optional, Automation): An automation to perform when the device selects a program. See on_program_change.
  • on_pitch_bend (Optional, Automation): An automation to perform when the pitch bend wheel moves. See on_pitch_bend.
  • on_sysex (Optional, Automation): An automation to perform when a System Exclusive message arrives. See on_sysex.
  • on_message (Optional, Automation): An automation to perform for every MIDI message, as raw bytes. See on_message.

Channels are 0 to 15, as they are on the wire. What a keyboard labels “channel 1” is channel 0 here.

Notes, velocities, controller numbers, controller values and programs are 0 to 127. Pitch bend is -8192 to 8191, with 0 at rest.

Triggered when the MIDI device is connected and ready to send. This is later than the on_connect of the BLE Client itself: it fires once the MIDI service has been found and the device has been told to start sending.

ble_midi:
ble_client_id: midi_keyboard
on_connect:
- logger.log: "MIDI device ready"

Triggered when the MIDI device disconnects.

ble_midi:
ble_client_id: midi_keyboard
on_disconnect:
- logger.log: "MIDI device gone"

Triggered when a key or a pad is pressed. The variables note, velocity and channel are available.

ble_midi:
ble_client_id: midi_keyboard
on_note_on:
- logger.log:
format: "Note %u on channel %u, velocity %u"
args: [note, channel, velocity]

A note is often used as a button. Pick the one you want and act on it:

ble_midi:
ble_client_id: midi_keyboard
on_note_on:
- if:
condition:
lambda: "return note == 60;"
then:
- light.toggle: desk_light

NOTE

Many devices signal a release by sending a note on with velocity 0 instead of a note off. The component reports those as on_note_off with velocity 0, so you do not have to handle both cases.

Triggered when a key or a pad is released. The variables note, velocity and channel are available.

ble_midi:
ble_client_id: midi_keyboard
on_note_off:
- logger.log:
format: "Note %u released"
args: [note]

Triggered when a knob, fader or switch is moved. The variables control_number, value and channel are available.

ble_midi:
ble_client_id: midi_keyboard
on_control_change:
- if:
condition:
lambda: "return control_number == 7;"
then:
- light.turn_on:
id: desk_light
brightness: !lambda "return value / 127.0f;"

Triggered when the device selects a program. The variables program and channel are available.

ble_midi:
ble_client_id: midi_keyboard
on_program_change:
- logger.log:
format: "Program %u selected"
args: [program]

Triggered when the pitch bend wheel moves. The variables value and channel are available.

ble_midi:
ble_client_id: midi_keyboard
on_pitch_bend:
- logger.log:
format: "Pitch bend %d"
args: [value]

Triggered when a System Exclusive message arrives. The variable data is available, a view of the payload without the surrounding 0xF0 and 0xF7 bytes (data.size(), data[i]).

ble_midi:
ble_client_id: midi_keyboard
on_sysex:
- logger.log:
format: "SysEx of %u bytes"
args: ["data.size()"]

Triggered for every message the device sends, including the ones the triggers above already cover and the ones they do not, such as aftertouch and clock. The variable data is available, a view of the status byte followed by its data bytes (data.size(), data[i]).

ble_midi:
ble_client_id: midi_keyboard
on_message:
- logger.log:
format: "MIDI status 0x%02X"
args: ["data[0]"]

NOTE

In on_sysex and on_message, data is only valid while the automation runs, so these two triggers do not accept actions that wait, such as delay, wait_until or script.wait. Copy what you need into a global variable first, then wait in a separate script.

Sends a note on message.

- ble_midi.send_note_on:
id: my_midi
note: 60
  • id (Required, ID): ID of the ble_midi component.
  • note (Required, int, templatable): The note, 0 to 127.
  • velocity (Optional, int, templatable): The velocity, 0 to 127. Defaults to 64.
  • channel (Optional, int, templatable): The channel, 0 to 15. Defaults to 0.

Sends a note off message. Takes the same options as ble_midi.send_note_on.

- ble_midi.send_note_off:
id: my_midi
note: 60

Sends a control change message. This is what most devices use to set an LED, a motorized fader or a mode.

- ble_midi.send_control_change:
id: my_midi
control_number: 7
value: 100
  • id (Required, ID): ID of the ble_midi component.
  • control_number (Required, int, templatable): The controller number, 0 to 127.
  • value (Required, int, templatable): The value, 0 to 127.
  • channel (Optional, int, templatable): The channel, 0 to 15. Defaults to 0.

Sends a program change message.

- ble_midi.send_program_change:
id: my_midi
program: 3
  • id (Required, ID): ID of the ble_midi component.
  • program (Required, int, templatable): The program, 0 to 127.
  • channel (Optional, int, templatable): The channel, 0 to 15. Defaults to 0.

Sends a pitch bend message.

- ble_midi.send_pitch_bend:
id: my_midi
value: 2048
  • id (Required, ID): ID of the ble_midi component.
  • value (Required, int, templatable): The amount of bend, -8192 to 8191.
  • channel (Optional, int, templatable): The channel, 0 to 15. Defaults to 0.

Sends a System Exclusive message. The 0xF0 and 0xF7 bytes are added for you if the payload does not already carry them. The message is sent in a single BLE packet and is not split, so the payload can be at most 125 bytes, and less if the connection negotiated a small packet size; a longer payload is refused with a warning.

- ble_midi.send_sysex:
id: my_midi
payload: [0x7E, 0x7F, 0x06, 0x01]
  • id (Required, ID): ID of the ble_midi component.
  • payload (Required, list of bytes, templatable): The message body.

Sends MIDI bytes as they are: one complete message, a status byte followed by exactly the data bytes it needs. Use this for messages the other actions do not cover. A payload that is not a single complete message, or a System Exclusive message, is refused with a warning; use ble_midi.send_sysex for those.

- ble_midi.send_raw:
id: my_midi
data: [0xFA]
  • id (Required, ID): ID of the ble_midi component.
  • data (Required, list of bytes, templatable): The bytes to send.

Checks whether the MIDI device is connected and ready.

- if:
condition:
ble_midi.connected:
id: my_midi
then:
- ble_midi.send_note_on:
id: my_midi
note: 60

NOTE

Sending while the device is disconnected does nothing; the message is dropped and a warning is logged. Use this condition when you want to take a different action instead.

Log everything, press every key and turn every knob:

ble_midi:
ble_client_id: midi_keyboard
on_message:
- logger.log:
format: "MIDI status 0x%02X, data 0x%02X 0x%02X"
args: ["data[0]", "data.size() > 1 ? data[1] : 0", "data.size() > 2 ? data[2] : 0"]

The note and controller numbers you see in the log are the ones to use in your automations.