The Hidden Quirks of Web Bluetooth on the Raspberry Pi Pico

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:

PlatformBrowserWeb BluetoothNotes
WindowsChrome✔️Full support
WindowsEdge✔️Same engine as Chrome
macOSChrome✔️Full support
macOSEdgeMicrosoft disables BLE on macOS
macOSSafari⚠️Partial, unreliable notifications
LinuxChrome⚠️Requires BlueZ permissions
AndroidChrome✔️Full support
AndroidFirefoxNo Web Bluetooth
iOSSafariWeb Bluetooth blocked by Apple
iOSChromeAlso WebKit → blocked
iOSEdgeAlso WebKit → blocked
iOSBluefy✔️Works perfectly
iOSWebBLE✔️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.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts