Roland FP-E50 BLE MIDI on Linux
TL;DR โ to make a Roland FP-E50 (and likely GO:KEYS and similar Roland boards) talk BLE MIDI with Linux:
1. BlueZ can't discover the MIDI characteristic โ Roland violates the ATT spec by mixing 16-bit and 128-bit UUIDs in one Read-By-Type response, and BlueZ mis-parses it (bluez#604). Patch below.
2. BlueZ's multi-event MIDI packets choke the piano's parser โ it voices one note then goes silent. Flush one event per packet (cf. bluez#471).
3. The FP-E50 only voices incoming notes on MIDI channel 4. Notes on the other 15 channels are acknowledged at the ATT layer and silently discarded. This is documented nowhere.
Symptoms (for the search engines)
| What you see | Cause |
|---|---|
BlueZ journal: profiles/midi/midi.c: Failed to obtain characteristic data; GATT shows a bogus characteristic UUID 00000318-0000-1000-8000-00805f9b34fb instead of the BLE-MIDI UUID 7772E5DB-3868-4112-A1A9-F2669D106BF3; the ALSA port appears but no data flows | Fix 1 (discovery) |
Connection drops at exactly ~2 s with HCI reason Connection Timeout (0x08) | Weak signal, not software โ see "red herrings" |
| Notes reach the piano (acknowledged writes return success!) but it makes no sound | Fix 3 (channel 4) |
aplaymidi plays one note then silence, while a slow note-by-note sender works | Fix 2 (packet framing) |
Fix 1: GATT discovery
The FP-E50 returns its characteristic declarations as a single ATT Read-By-Type response mixing 7-byte records (16-bit UUIDs) and 21-byte records (128-bit UUIDs). The spec requires uniform record sizes, so BlueZ slices the response into 7-byte chunks โ and because the total happens to divide evenly, it doesn't even error. The 128-bit MIDI characteristic gets shredded into three garbage records, which is where the phantom 0x0318 UUID comes from.
The patch (in src/shared/gatt-helpers.c, discover_chrcs_cb) detects non-uniform responses using a GATT invariant โ a characteristic's value handle is always its declaration handle + 1 โ and walks the records individually. Spec-compliant devices keep the original fast path. BlueZ upstream closed this as Roland's bug, which is fair, but Roland isn't going to firmware-patch your piano.
Fix 2: one MIDI event per BLE packet
BlueZ coalesces simultaneous MIDI events into multi-event BLE packets with running status. The FP-E50's parser cannot handle these: it voices the first event and then ignores everything for the rest of the connection. Anything kernel-timed (aplaymidi) delivers chords in bursts and triggers this instantly. The fix is a flush after every event in profiles/midi/midi.c, so each BLE packet carries one complete header + timestamp + message.
Fix 3: the piano only listens on MIDI channel 4
This was the maddening one. With discovery fixed, every diagnostic said our notes were arriving: link-layer ACKs, and โ decisively โ ATT Write Requests returned clean Write Responses. The piano was formally accepting the bytes and voicing nothing.
We tested timestamps (BLE-MIDI carries a 13-bit millisecond clock; we even synced our outgoing stamps to the piano's own clock domain), acknowledged vs unacknowledged writes, raw vs framed payloads, and the suspicious Microchip/ISSC transparent-UART characteristics that sit alongside the MIDI one in its GATT table (it's an RN4870-class module inside). All silent.
The break came from sweeping a single note across all 16 MIDI channels: exactly one landed, every time. The FP-E50 transmits on channel 1 but only voices incoming notes on channel 4 โ presumably an artifact of its ZEN-Core scene/part layout. The owner's manual documents a transmit-channel setting and nothing about receive filtering. Remap your stream to channel 4 and everything plays.
Red herrings worth naming
"The dongle is bad." Early on, every connection died at exactly the 2-second supervision timeout, and a cheap Realtek RTL8761B USB dongle made an easy suspect. The actual cause was signal: ~โ80 dBm with the dongle behind a metal case across the room from the piano. A USB extension cable to the desk fixed it completely. Check RSSI before blaming silicon.
"The writes must not be arriving." The piano acknowledges โ at two protocol layers โ data it has no intention of using. On a device whose only real output is sound, protocol-level "success" means nothing. The only ground truth is your ears.
One trick that earned its keep: when you have many hypotheses and your only output channel is audio, encode the experiment in pitch โ route each delivery variant to a different note, play them in sequence, and whichever pitch sounds names the mechanism that works. One listening pass, five hypotheses.
Recipe
# build deps (Ubuntu 24.04)
sudo apt install autoconf automake libtool libdbus-1-dev libglib2.0-dev \
libreadline-dev libudev-dev libical-dev libjson-c-dev libell-dev libasound2-dev
git clone -b 5.72 https://github.com/bluez/bluez.git && cd bluez
curl -O https://jarrahbloomfield.com/fp-e50-bluez-5.72.patch.txt
git apply fp-e50-bluez-5.72.patch.txt
ln -s lib bluetooth # tree expects this include path
./bootstrap && ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var \
--enable-midi --disable-manpages --disable-datafiles
make src/builtin.h && make src/bluetoothd
# run it (in place of the system daemon)
sudo systemctl mask bluetooth.service && sudo pkill bluetoothd
sudo ./src/bluetoothd -n -d &
bluetoothctl # agent NoInputNoOutput; scan, pair, trust, connect
# play: remap everything to channel 4 first
aplaymidi -l # โ "FP-E50 MIDI 1 Bluetooth"
aplaymidi -p 128:0 song-ch4.mid
Channel remapping in a few lines of Python (pip install mido):
import mido, sys
f = mido.MidiFile(sys.argv[1])
for track in f.tracks:
track[:] = [m for m in track if not (hasattr(m, 'channel') and m.channel == 9)]
for m in track:
if hasattr(m, 'channel'):
m.channel = 3 # MIDI channel 4, zero-indexed
f.save(sys.argv[1].rsplit('.', 1)[0] + '-ch4.mid')
Input (piano โ Linux) needs none of the channel games โ once discovery is patched, aseqdump/arecordmidi just work.
Notes
The timestamp clock-sync change is included in the patch but was never proven necessary โ the channel-4 discovery landed while it was in place and it's harmless, so it stayed. If you're minimizing the diff, try without it.
Tested on Ubuntu 24.04, BlueZ 5.72, FP-E50 firmware 1.23. The GO:KEYS series shares the same BLE module and the same bluez#604 signature, so fixes 1โ2 almost certainly apply; whether its receive-channel gate is also channel 4, I'd love to hear.
This debugging was driven by Claude: it wrote the BlueZ patches, decoded the btmon captures, and designed the experiments (including the pitch-coded test matrix and the channel sweep that cracked it). Claude also wrote this writeup in its entirety.