Skip to content

Apps Bluetooth support#67

Draft
microbit-matt-hillsdon wants to merge 50 commits intomainfrom
apps
Draft

Apps Bluetooth support#67
microbit-matt-hillsdon wants to merge 50 commits intomainfrom
apps

Conversation

@microbit-matt-hillsdon
Copy link
Contributor

@microbit-matt-hillsdon microbit-matt-hillsdon commented Jan 5, 2026

This contains breaking changes (e.g. flash progress interface, connect
signature, device error codes) and will form part of a v1 at some point.
Migrating USB-only code should be trivial though.

Changes:

  • Switch to capacitor-ble for bluetooth

    • Quite a simplication as the service/characteristic lookups are deferred to
      point of use and the interactions are internally queued.
  • Support DFU and partial flashing on iOS/Android platforms

  • Drop a bunch of workarounds that need reevaluating after the switch

  • Don't try to start notifications on absent services. Prevents issues when not
    in application mode for a flash.

  • Improve connect interface (which had a misleading return value) and
    connect/flash progress.

Design issues:

  • Should connecting and connecting for flashing both be the same flow? Or are
    the ideas different enough that we split them? It's nice in that it matches
    USB but it's also quite different because of pairing.

TODO:

  • Test in web, reinstate workarounds as needed, decide whether to use capacitor implementation for web (check bundles size)
  • Upgrade PR for Python
  • Upgrade PR for hex flashing tool/website

Fixes #20
Fixes #57
Fixes #71

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 5, 2026

Deploying microbit-connection with  Cloudflare Pages  Cloudflare Pages

Latest commit: dca3fd7
Status:🚫  Build failed.

View logs

This contains breaking changes (e.g. flash progress interface, connect
signature, device error codes) and will form part of a v1 at some point.
Migrating USB-only code should be trivial though.

Changes:
- Switch to capacitor-ble for bluetooth
  - Quite a simplication as the service/characteristic lookups are deferred to
    point of use and the interactions are internally queued.

- Support DFU and partial flashing on iOS/Android platforms
- Drop a bunch of workarounds that need reevaluating after the switch
- Temporarily drop uBit name support due to capacitor-ble limitation
- Don't try to start notifications on absent services. Prevents issues when not
  in application mode for a flash.
- Improve connect interface (which had a misleading return value) and
  connect/flash progress.

This branch is going to be long lived for a month or two during apps work, then
we'll loop back around and see what it means for Web Bluetooth - does it
replace it or do we have both implementations.

Design issues:

- Should connecting and connecting for flashing both be the same flow? Or are
  the ideas different enough that we split them? It's nice in that it matches
  USB but it's also quite different because of pairing.
microbit-matt-hillsdon and others added 19 commits January 8, 2026 15:35
- Use .js extension for imports consistently
- Fix typo in exports for types (also on main, but at least VS Code seems to cope)
This might be a behaviour change for bluetooth but is consistent with USB.
For USB there's not really anything to do but let's keep it consistent
This has caused app-level issues because disconnected doesn't let the app understand that
a reconnect will automatically happen.
You can want to pause due to a hidden tab, but defer it due to flashing then
become visible before you ever did pause. But we went ahead with the pause
incorrectly.
Remove them from the connection status.

Check them before connect but also provide pre-flight API for UX flows.

This better matches the iOS/Android permission model and is easier for client
code to manage.
This is trivial for the connections and saves apps from additional state
management as they often need to understand which transition happened.
…arting notifications (#76)

- Attempt to connect four times before throwing error.
- Throw immediately if disconnect occurs whilst connecting instead of waiting for timeout.
- Ensure latest services are found.
Fixes connecting to micro:bit on Android after full flashing. Otherwise there is a "Characteristic not found" error from trying to get board version.
This seems to help significantly with reconnect on Android, where as the retry solution has encountered issues (unexpected disconnects, trouble discovering services that just increasing the delay didn't help with).

Remove the retries for now - we might well reinstate but we need to do so with more care as it's led to very long retries in the case where there is no device due to app-level retries doubling the retry count.

Tested on Android and iOS with a branch of ml-trainer that removes the delay after flashing.
In ml-trainer you can see this if you:
1. connect, pair, DFU
2. trigger connection errors until start-over state
3. edit the pattern away from the correct one
4. note you go on to partial flash

In Web Bluetooth this wasn't an issue as we always did a clearDevice but we should fix it here for other library scenarios.
- Add a new error code for pairing information lost on peripheral.
- Add new native-only progress stages (ProgressStage.CheckingBond and ProgressStage.ResettingDevice).
- Add abort signal to allow for stopping of scanning (and potentially other steps in future)
…g of device on disconnect (#85)

- Remove auto reconnect logic and `ConnectionStatus.Reconnecting` status in favour of the consuming app handling reconnections where necessary.
- Remove discarding of connection/device on disconnect in favour of the consuming app calling `clearDevice`. So unless the app calls `clearDevice`, we assume the same device is used for the connection.
- Extend radio bridge connection timeout to 10_000ms to match Bluetooth.
- Remove no longer needed `ignoreDelegateStatus`
- Keep serial session open when connecting to allow for reconnection after switch tabs
1. Skip 64-byte blocks that are entirely 0xFF during BLE partial
   flashing, matching the iOS native app's approach. The device erases
   each flash page when it receives a write at a page-aligned address,
   so interior 0xFF blocks are redundant. Flash page size is determined
   from the board version (V1: 0x400, V2: 0x1000).

2. Handle MakeCode hex files that nrf-intel-hex rejects. Older thin
   (non-universal) hex files have trailing blank lines or embedded
   source in custom Intel HEX record type 0x0E after the EOF record.
   Added truncateHexAfterEof() in hex-flash-data-source.ts, used by
   both BLE (bluetooth.ts) and USB (usb-partial-flashing.ts) code
   paths.

Region bounds were verified against the iOS app's hexDataToAppRegion
calculation which derives [SoftDevice end, bootloader start).

Added tests covering MakeCode v0–v8 hex files (both universal and thin)
with exact expected region assertions (start, end, hash).
…lative delays (#87)

- Add `deviceBondState` option to Bluetooth connect.
- Set device as bonded when connection is established successfully.
- Set device as not bonded when connection fails.
- Remove speculative delays.
- Ensure partial flashing errors are raised so that the app can handle them.
Simple monorepo with npm workspaces. We'll always build everything.
microbit-grace and others added 3 commits February 13, 2026 12:12
This reduces partial flashing time. The `waitForDisconnect` after partial flashing has been moved to `connect`. Most of the time, the disconnect can happen in the background, except when connecting straight after partial flashing. The disconnect interferes with connecting on Android. 

CreateAI project hex is added as a Capacitor app file option for testing purposes.

The Capacitor app has been updated to include a 'Connect' and 'Disconnect' button. Live accelerometer data is displayed if available and connected.

---------

Co-authored-by: Matt Hillsdon <matt.hillsdon@microbit.org>
Compare the MakeCode application hash from the hex file (SHA-256
truncated to 8 bytes, at magic+24) against the device's MakeCode
region hash. When they match the program is already on the device,
so we reset to application mode and return AlreadyUpToDate instead
of reflashing identical data. This mirrors the optimisation in PXT's
own Web BLE partial flashing code.

Fixes #651
Capacitor BLE, Filesystem, Core, and Nordic DFU are now peer
dependencies so consuming apps control versioning and avoid
duplicates.

This follows the pattern used by Nordic DFU and other similar plugins.
microbit-matt-hillsdon and others added 27 commits February 27, 2026 09:53
Particularly useful to have sibling project references here.
The Nordic DFU library defaults alternativeAdvertisingNameEnabled to true,
causing it to scan for a custom advertising name after triggering the
bootloader. The micro:bit bootloader is built with
NRF_DFU_BLE_REQUIRES_BONDS=1, which compiles out the code path that reads
a stored advertising name — it always advertises as "DfuTarg". The library
scans for the wrong name and times out.

Fix by setting alternativeAdvertisingNameEnabled to false on iOS, matching
the microbit-ios app behaviour. The library scans by DFU service UUID
instead.

Also remove the no-op unsafeExperimentalButtonlessServiceInSecureDfuEnabled
option (controls SDK 12 experimental service 8E400001, not used by micro:bit).

Upgrade @microbit/capacitor-community-nordic-dfu to v7.0.0-microbit.4 which
adds the alternativeAdvertisingNameEnabled option and DFU logging.

Note some known limitations we can't fix here or will tackle in follow up PRs.
- Attempt full flash if PF characteristic is not found. This matches current behaviour in the Android micro:bit app, where full flash is attempted if PF characteristic is not found ([see here](https://github.com/microbit-foundation/android-partial-flashing-lib/blob/075e8e844873f8443dcbe7fca64bb4aa18b0aced/src/main/java/org/microbit/android/partialflashing/PartialFlashingBaseService.java#L419-L430))
- Skip resetting of micro:bit if PF characteristic is not found and try connecting anyway
- Use any viable characteristic to trigger iOS pairing

With these changes we can flash over a CreateAI hex, except the live version on V1 which has neither DFU nor PF so we're out of luck.

---------

Co-authored-by: Matt Hillsdon <matt.hillsdon@microbit.org>
The iOS connect path uses an encrypted characteristic to trigger the
pairing dialog. Previously it only tried the partial flashing
characteristic, which fails on devices without PF support.

Now tries PF notifications, then Secure DFU buttonless notifications
(V2), then DFU control read (V1). Each failure is instant (local GATT
cache lookup) so there is no meaningful delay from retries.

Update known issues.
The hash isn't populated for builds without the MakeCode magic so we don't want
to determine two such builds are the same incorrectly. In practice it's
unlikely these builds will have a PF service but still.
This was supported before the switch to the capacitor-ble plugin but
dropped because RequestBleDeviceOptions only accepts a single
namePrefix. On native, the scan and bonded device filters now match
against either prefix. On web, we call patch navigator.bluetooth.requestDevice
to use multiple filters regardless of what the capacitor plugin passes.

I'll raise an upstream issue.
Separate USB and Bluetooth entrypoints (breaking).

Restructure so USB consumers who don't use createUniversalHexFlashDataSource no
longer pull in @microbit/microbit-universal-hex.

Rename createWebBluetoothConnection to createBluetoothConnection (and
MicrobitWebBluetoothConnection to MicrobitBluetoothConnection) as it now
covers native platforms, not just Web Bluetooth.

Rename createWebUSBConnection to createUSBConnection (and
MicrobitWebUSBConnection to MicrobitUSBConnection) for consistency.

Add `sideEffects: false` to package.json.

Updated README with entrypoints documentation and platform support table.
See microbit-foundation/python-editor-v3#89

Ultimately this needs a fix in DAPLink to clear the queue when the host reconnects but that won't have much impact on existing devices.

Repro steps via the demo:
- Disable the retries in this PR
- Write a MakeCode project that spams serial
- Connect via the demo 
- Start serial via the button
- Reload the page
- Connect and see it fail ~50% of the time

The disconnect on tab visibility has been a long-standing workaround and these disconnects play an important role in letting folks use multiple micro:bit apps. So on the apps branch we now have an explicit PAUSED state as an optional feature of the USB connection (on by default to play nice across apps). Apps can handle PAUSED differently from DISCONNECTED (e.g. Python can not clear serial scrollback for PAUSED).
In Brave we can fail to parse the serial and therefore to tell the board
version (it anonomises the device serial number) and we need this information
to flash the correct hex.

This matches MakeCode's approach.

I confirmed that identical serials are found in Chrome for V1 and V2 via this
and the old method.

I don't think the fallback should come into play but leaving it for now. This
feature has been in DAPLink since WebUSB support has been in DAPLink.

Tweak format command to be less verbose.
Introduce a new code for the purpose.

Previously we returned undefined in scenarios where you call the method either
before connection or when unexpectedly disconnected. This can lead to errors
being ignored or knock-on errors because you "know" you're connected but
disconnects can happen at arbitrary points. Better to require the caller to
handle an error.
Drop EventTarget inheritance and ~220 lines of type gymnastics
(Registration, ValueIsEvent, TrackingEventTarget) in favour of a
simple Map<key, Set<Listener>> implementation. Listeners now receive
plain data payloads directly instead of Event wrapper objects.

Aligning with EventTarget has been nothing but pain especially for users who
mock/stub the connections which is common in our consuming apps for tests.

Removed: ConnectionStatusEvent, BeforeRequestDevice, AfterRequestDevice,
BackgroundErrorEvent, SerialDataEvent, SerialErrorEvent, SerialResetEvent,
FlashEvent, AccelerometerDataEvent, ButtonEvent, MagnetometerDataEvent,
UARTDataEvent classes. Added ButtonData interface.
The flash event was only in SerialConnectionEventMap and only dispatched
by USB connections. Move it to DeviceConnectionEventMap so it's available
on all connection types, and dispatch it from Bluetooth's flash() method
after a successful flash.
…rfaces

Serial is a USB-specific concern — Bluetooth has UART (different data
model) and the Bluetooth implementation was just a stub. Remove
serialWrite from the base DeviceConnection interface and add it to
MicrobitUSBConnection instead. The radio bridge intentionally does not
expose serialWrite as it would conflict with the serial protocol used
to relay radio messages.
Breaking: Switch to throwing DeviceError when not connected
Breaking: Replace EventTarget with custom typed event system
…tion-state

Document post-flash connection state differences
Breaking: Move serialWrite from DeviceConnection to USB only
Document radio bridge as experimental / limited
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants