Skip to content

BLE File Transfer writes blocked as STATUS_ERROR_READONLY when no USB host attached #10972

@makermelissa-piclaw

Description

@makermelissa-piclaw

What CircuitPython version are you using?

Latest main (verified against current adafruit/circuitpython source).

Code/REPL

import supervisor
print(supervisor.runtime.usb_connected)   # False (no host attached)

import storage
print(storage.getmount("/").readonly)     # (see note below)

Behavior

On a board powered only from a wall adapter (no USB host anywhere on the line), any BLE File Transfer write op (write/move/mkdir/delete) is rejected by the firmware with status 0x05 (STATUS_ERROR_READONLY). Reads succeed.

supervisor.runtime.usb_connected returns False and there is no boot.py override, yet writes are blocked as if the FS were read-only.

This was discovered while testing circuitpython/web-editor over BLE. The web-editor calls into @adafruit/ble-file-transfer-js, which dispatches a MOVE_COMMAND/WRITE_COMMAND/etc., and the firmware reliably returns 0x05. The previous JS-side label for 0x05 was STATUS_ERROR_USB_MOUNTED (since corrected to STATUS_ERROR_READONLY in adafruit/ble-file-transfer-js#10 / v1.0.5), so this was originally surfaced as a misleading "Unable to write while USB connected" message — but the underlying firmware behavior is the real bug.

Root cause (traced through main)

There are two distinct issues conflated in this code path.

1. Boot defaults make the CIRCUITPY filesystem unconditionally write-protected to Python/BLE.

main.c:1111-1112:

filesystem_set_internal_concurrent_write_protection(true);
filesystem_set_internal_writable_by_usb(CIRCUITPY_USB_DEVICE == 1);

So at boot, every USB-capable build sets MP_BLOCKDEV_FLAG_CONCURRENT_WRITE_PROTECTED on and MP_BLOCKDEV_FLAG_USB_WRITABLE on.

In supervisor/shared/filesystem.c:

bool filesystem_is_writable_by_python(fs_user_mount_t *vfs) {
    return ((vfs->blockdev.flags & MP_BLOCKDEV_FLAG_CONCURRENT_WRITE_PROTECTED) == 0) ||
           ((vfs->blockdev.flags & MP_BLOCKDEV_FLAG_USB_WRITABLE) == 0) ||
           ((vfs->blockdev.flags & MP_BLOCKDEV_FLAG_IGNORE_WRITE_PROTECTION) != 0);
}

With CWP=on, USB_WRITABLE=on, IGNORE=off → returns false.

In extmod/vfs_fat_diskio.c:

case IOCTL_INIT:
case IOCTL_STATUS: {
    DSTATUS stat;
    if (ret != mp_const_none && MP_OBJ_SMALL_INT_VALUE(ret) != 0) {
        stat = STA_NOINIT;
    } else if (!filesystem_is_writable_by_python(vfs)) {
        stat = STA_PROTECT;
    } else {
        stat = 0;
    }

So FAT reports the volume as write-protected → FR_WRITE_PROTECTED → BLE FT returns STATUS_ERROR_READONLY.

Crucially, this check never consults tud_mounted(), tud_connected(), or whether a host has actually claimed the MSC LUN. The decision is made purely from build-time configuration flags set during main() startup. As a result, a board powered from a wall adapter with no host anywhere on the line still reports its filesystem as USB-owned/write-protected to BLE.

The only way to escape this is to override in boot.py:

import storage
storage.disable_usb_drive()

…which calls usb_drive_set_enabled(false) in shared-module/storage/__init__.c and clears MP_BLOCKDEV_FLAG_USB_WRITABLE. But this is a hard binary choice — you lose USB MSC entirely — when the truthful answer should be runtime-conditional.

2. Lock contention is reported as "write protected".

supervisor/shared/workflow.c:122-132 in supervisor_workflow_move:

if (!filesystem_lock(active_mount)) {
    return FR_WRITE_PROTECTED;
}

filesystem_lock() is a mutex on MP_BLOCKDEV_FLAG_LOCKED, not a write-permission check. When it fails it means another op (or stale state) holds the lock. Returning FR_WRITE_PROTECTED cascades into STATUS_ERROR_READONLY at the BLE protocol layer. Lock-busy and write-protected are conflated.

Same conflation happens for _process_write's direct filesystem_lock failure path in supervisor/shared/bluetooth/file_transfer.c:268-272.

Suggested fixes

  1. Make USB-writable runtime-conditional. Only treat USB as the writer when a host has actually mounted MSC (e.g. gate on tud_mounted() and active LUN state) rather than solely on CIRCUITPY_USB_DEVICE == 1. Falling back to Python-writable when no host is present would let BLE FT (and other workflows) write without forcing users to disable USB MSC entirely via boot.py.

  2. Distinguish busy from write-protected. Introduce STATUS_ERROR_BUSY = 0x06 (or similar) in supervisor/shared/bluetooth/file_transfer_protocol.h and stop returning FR_WRITE_PROTECTED from supervisor_workflow_move/mkdir/delete when the underlying failure is filesystem_lock() contention.

Behavior

Writes via BLE File Transfer are blocked even when no USB host is present, returning status 0x05 (STATUS_ERROR_READONLY). The firmware's read-only determination is based on build-time flags rather than actual host attachment state.

Additional information

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions