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
-
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.
-
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
What CircuitPython version are you using?
Latest
main(verified against currentadafruit/circuitpythonsource).Code/REPL
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_connectedreturnsFalseand there is noboot.pyoverride, yet writes are blocked as if the FS were read-only.This was discovered while testing
circuitpython/web-editorover BLE. The web-editor calls into@adafruit/ble-file-transfer-js, which dispatches aMOVE_COMMAND/WRITE_COMMAND/etc., and the firmware reliably returns0x05. The previous JS-side label for0x05wasSTATUS_ERROR_USB_MOUNTED(since corrected toSTATUS_ERROR_READONLYinadafruit/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:So at boot, every USB-capable build sets
MP_BLOCKDEV_FLAG_CONCURRENT_WRITE_PROTECTEDon andMP_BLOCKDEV_FLAG_USB_WRITABLEon.In
supervisor/shared/filesystem.c:With CWP=on, USB_WRITABLE=on, IGNORE=off → returns false.
In
extmod/vfs_fat_diskio.c:So FAT reports the volume as write-protected →
FR_WRITE_PROTECTED→ BLE FT returnsSTATUS_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 duringmain()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:…which calls
usb_drive_set_enabled(false)inshared-module/storage/__init__.cand clearsMP_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-132insupervisor_workflow_move:filesystem_lock()is a mutex onMP_BLOCKDEV_FLAG_LOCKED, not a write-permission check. When it fails it means another op (or stale state) holds the lock. ReturningFR_WRITE_PROTECTEDcascades intoSTATUS_ERROR_READONLYat the BLE protocol layer. Lock-busy and write-protected are conflated.Same conflation happens for
_process_write's directfilesystem_lockfailure path insupervisor/shared/bluetooth/file_transfer.c:268-272.Suggested fixes
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 onCIRCUITPY_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 viaboot.py.Distinguish busy from write-protected. Introduce
STATUS_ERROR_BUSY = 0x06(or similar) insupervisor/shared/bluetooth/file_transfer_protocol.hand stop returningFR_WRITE_PROTECTEDfromsupervisor_workflow_move/mkdir/deletewhen the underlying failure isfilesystem_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
boot.pyoverrides.supervisor.runtime.usb_connected == Falsein this state.circuitpython/web-editor(PRs Lint all the libraries! #475, Support of the Feather M0 WiFi ATSAMD21 + ATWINC1500 #478) and JS libadafruit/ble-file-transfer-js(PR atmel-samd: Add modules directory with frozen bytecode support (like ESP8266 port), and NeoPixel python wrapper module. #10, released as v1.0.5).boot.pywithimport storage; storage.disable_usb_drive()— but this is a workaround, not a fix.