Building a Web Bluetooth Low Energy (BLE) project on the Raspberry Pi Pico using MicroPython sounds simple — until you try to connect it to a Web Bluetooth app. What should be a straightforward BLE service quickly turns into a maze of silent failures, cryptic errors, and browser‑specific behavior. I ran into this problem while working on the Deadrail Engine Control project which is a python program using the rpi_app_framework that is controlled from a REACT application running on an IPhone.
After debugging the BLE motor‑control app, here are the most important lessons, pitfalls, and fixes. If you’re building anything BLE‑related on the Pico — especially for Web Bluetooth — this guide will save you hours of frustration.
1. UUID Case Sensitivity: Chrome and Edge Are Extremely Strict
Chrome, Edge, Opera, and all Chromium‑based browsers require UUIDs to match exactly, including lowercase vs uppercase.
MicroPython accepts uppercase UUIDs. Chrome does not.
If the UUIDs differ in case, Chrome will:
- connect successfully
- allow reads
- allow writes
- but silently refuse to subscribe to notifications
This leads to the Pico throwing:
[Errno 22] EINVAL
every time it tries to send a notification.
Fix: Use lowercase UUIDs everywhere — Pico, React, and any BLE tools.
2. Notify‑Only Characteristics Don’t Work in Chrome or Edge
Chrome and Edge refuse to subscribe to a characteristic that only has “notify”. Even if the Pico advertises it correctly, startNotifications() will silently fail unless the characteristic also includes “read” or “write”.
This is one of the most common Web Bluetooth pitfalls.
Correct Example (TX characteristic with required flags)
Code
self.tx_char = BLECharacteristic(
uuid="6e400003-b5a3-f393-e0a9-e50e24dcca9e",
flags=["read", "notify"]
)
Adding “read” does not change how you use the characteristic — it simply satisfies Chrome’s requirement that a characteristic must have at least two properties before it will allow notification subscriptions.
Without this, Chrome will connect, read, and write just fine, but startNotifications() will never activate, and the Pico will throw EINVAL every time it tries to notify.
3. MicroPython Sometimes Fails to Create CCCDs (Client Characteristic Configuration Descriptors)
This one is undocumented and extremely confusing.
MicroPython does not always create a CCCD unless the characteristic includes "write" in addition to "read" and "notify".
Without a CCCD, Chrome cannot enable notifications — even if everything else is correct.
Correct Example (TX and STATUS characteristics)
Code
self.tx_char = BLECharacteristic(
uuid="6e400003-b5a3-f393-e0a9-e50e24dcca9e",
flags=["read", "write", "notify"]
)
self.status_char = BLECharacteristic(
uuid="6e400004-b5a3-f393-e0a9-e50e24dcca9e",
flags=["read", "write", "notify"],
on_read=self.on_status_read
)
This ensures MicroPython creates a proper CCCD so Chrome/Edge can subscribe.
4. The Biggest Bug: Using the Wrong Connection Handle
MicroPython assigns a connection handle when a central connects. It is almost never 0.
But many BLE examples online (and some libraries) call:
Code
gatts_notify(0, handle, data)
This always fails with EINVAL because the real connection handle might be 1, 2, or higher.
Correct Example (store and use the real handle)
Inside your IRQ handler:
Code
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, addr_type, addr = data
self._conn_handle = conn_handle
Then use it:
Code
self.ble.gatts_notify(self._conn_handle, characteristic.handle, data)
This single fix eliminates nearly all EINVAL errors.
5. MicroPython Does Not Advertise 128‑bit UUIDs Automatically
If you want Web Bluetooth to discover your service, you must manually add the 128‑bit UUID to the advertisement packet using AD type 0x07.
Correct Example (advertising 128‑bit UUID)
Code
adv = bytearray()
adv += b"\x02\x01\x06" # Flags
adv += bytes([len(name) + 1, 0x09]) + name # Complete Local Name
adv += bytes([len(uuid_bytes) + 1, 0x07]) + uuid_bytes # 128‑bit UUID
Without this, some centrals (especially Web Bluetooth) won’t see your service.
6. Browser Compatibility: What Works and What Doesn’t
Web Bluetooth is the best choice for Pico‑based BLE projects because it removes the need for native apps entirely. Instead of compiling, signing, and distributing mobile or desktop software, you can deliver a full BLE control interface through a simple website that updates instantly. It works across major platforms like Windows, macOS, Linux, and Android, and gives you direct access to BLE services using nothing more than JavaScript. For makers and developers, that means faster iteration, easier sharing, and a dramatically simpler user experience — just open a browser and connect.
Here’s the compatibility matrix for Web Bluetooth:
| Platform | Browser | Web Bluetooth | Notes |
|---|---|---|---|
| Windows | Chrome | ✔️ | Full support |
| Windows | Edge | ✔️ | Same engine as Chrome |
| macOS | Chrome | ✔️ | Full support |
| macOS | Edge | ❌ | Microsoft disables BLE on macOS |
| macOS | Safari | ⚠️ | Partial, unreliable notifications |
| Linux | Chrome | ⚠️ | Requires BlueZ permissions |
| Android | Chrome | ✔️ | Full support |
| Android | Firefox | ❌ | No Web Bluetooth |
| iOS | Safari | ❌ | Web Bluetooth blocked by Apple |
| iOS | Chrome | ❌ | Also WebKit → blocked |
| iOS | Edge | ❌ | Also WebKit → blocked |
| iOS | Bluefy | ✔️ | Works perfectly |
| iOS | WebBLE | ✔️ | Works perfectly |
Important iPhone Note
Apple blocks Web Bluetooth in all browsers on iOS because all browsers must use WebKit, and WebKit does not implement Web Bluetooth.
The only way to use Web Bluetooth on iPhone is through:
- Bluefy
- WebBLE
These apps embed a native CoreBluetooth bridge.
7. Silent Failures Are Normal — Logging Is Essential
Chrome will not tell you:
- when
startNotifications()fails - when UUIDs don’t match
- when CCCDs don’t exist
- when the Pico rejects a notify call
The Pico will only show:
Code
[Errno 22] EINVAL
Logging on both sides is the only way to see what’s happening.
And Finally…
Bluetooth Low Energy on the Raspberry Pi Pico is powerful, but it comes with a long list of quirks:
- UUIDs must match exactly
- Characteristics must include the right flags
- CCCDs must exist
- Connection handles must be tracked
- 128‑bit UUIDs must be advertised manually
- Browser support varies wildly
- iPhone requires Bluefy
Once you understand these pitfalls, the Pico becomes a rock‑solid BLE peripheral — perfect for Web Bluetooth dashboards, motor controllers, sensor hubs, and more.