From 65516d11c166d6d228e21a400d0e08d6a3c303b5 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 15 Jul 2026 22:51:40 -0400 Subject: [PATCH 1/2] allow storage.disable_usb_drive() and .enable_usb_drive() after boot.py --- shared-bindings/storage/__init__.c | 39 ++++++++++++++++++++++----- shared-module/storage/__init__.c | 13 ++++++--- supervisor/shared/usb/usb_msc_flash.c | 2 +- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/shared-bindings/storage/__init__.c b/shared-bindings/storage/__init__.c index 14ec16f3096..b1b1c6e6ce8 100644 --- a/shared-bindings/storage/__init__.c +++ b/shared-bindings/storage/__init__.c @@ -93,7 +93,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(storage_umount_obj, storage_umount); //| ) -> None: //| """Remounts the given path with new parameters. //| -//| This can always be done from boot.py. After boot, it can only be done when the host computer +//| This can always be done from ``boot.py``. After boot, it can only be done when the host computer //| doesn't have write access and CircuitPython isn't currently writing to the filesystem. An //| exception will be raised if this is the case. Some host OSes allow you to eject a drive which //| will allow for remounting. @@ -189,7 +189,27 @@ MP_DEFINE_CONST_FUN_OBJ_KW(storage_erase_filesystem_obj, 0, storage_erase_filesy //| def disable_usb_drive() -> None: //| """Disable presenting ``CIRCUITPY`` as a USB mass storage device. //| By default, the device is enabled and ``CIRCUITPY`` is visible. -//| Can be called in ``boot.py``, before USB is connected.""" +//| If `disable_usb_drive()` is called in ``boot.py``, before USB is connected, +//| the mass storage device is not presented at all. +//| The drive cannot be made available again until the next hard reset; +//| `enable_usb_drive()` is not available. +//| +//| If `disable_usb_drive()` is called after ``code.py`` starts, or from the REPL, +//| the USB drive logical unit (LUN) will report as "not ready", +//| causing the host to unmount it. +//| It can be made ready and available again by calling `enable_usb_drive()`. +//| When `disable_usb_drive` is called after ``code.py`` starts or in the REPL, +//| the call will delay 2.5 seconds before returning, +//| so that host has time to detect that the drive is not ready. +//| The host polls the device approximately every one or two seconds. +//| +//| If `disable_usb_drive()` is called when the host is actively writing CIRCUITPY, +//| filesystem corruption could occur. Be careful to call it when the host is quiescent. +//| +//| When the USB drive is disabled, CIRCUITPY becomes read/write, and can be written +//| from user code or the REPL. This is easier than arranging for a `remount()` in ``boot.py``. +//| Code editors and file uploaders can use this feature to write files via the REPL. +//| """ //| ... //| //| @@ -206,14 +226,21 @@ static mp_obj_t storage_disable_usb_drive(void) { MP_DEFINE_CONST_FUN_OBJ_0(storage_disable_usb_drive_obj, storage_disable_usb_drive); //| def enable_usb_drive() -> None: -//| """Enabled presenting ``CIRCUITPY`` as a USB mass storage device. +//| """Enable presenting ``CIRCUITPY`` as a USB mass storage device. //| By default, the device is enabled and ``CIRCUITPY`` is visible, //| so you do not normally need to call this function. -//| Can be called in ``boot.py``, before USB is connected. +//| You can call `enable_usb_drive()` in ``boot.py``, before USB is connected, +//| to reverse a `disable_usb_drive()` in ``boot.py``. +//| +//| If you call `enable_usb_drive()` after ``code.py`` starts or in the REPL, +//| you can reverse the effect of a previous `disable_usb_drive()`, +//| but only if `disable_usb_drive()` was also called after ``code.py`` started or in the REPL. +//| The CIRCUITPY drive will reappear to the host, and become read-only again +//| if it was previously read-only. //| -//| If you enable too many devices at once, you will run out of USB endpoints. +//| If you enable too many USB devices at once, you will run out of USB endpoints. //| The number of available endpoints varies by microcontroller. -//| CircuitPython will go into safe mode after running boot.py to inform you if +//| CircuitPython will go into safe mode after running ``boot.py`` to inform you if //| not enough endpoints are available. //| """ //| ... diff --git a/shared-module/storage/__init__.c b/shared-module/storage/__init__.c index dedcee1ff23..0877ffdea5e 100644 --- a/shared-module/storage/__init__.c +++ b/shared-module/storage/__init__.c @@ -27,7 +27,7 @@ #include "tusb.h" // Is the MSC device enabled? -bool storage_usb_is_enabled; +static volatile bool storage_usb_is_enabled; void storage_usb_set_defaults(void) { storage_usb_is_enabled = CIRCUITPY_USB_MSC_ENABLED_DEFAULT; @@ -38,12 +38,17 @@ bool storage_usb_enabled(void) { } static bool usb_drive_set_enabled(bool enabled) { - // We can't change the descriptors once we're connected. + // We can't change the descriptors once we're connected, but we can make the LUN be ready or not ready. + storage_usb_is_enabled = enabled; if (tud_connected()) { - return false; + // The TEST UNIT READY callback in usb_msc_flash.c checks the value of storage_usb_is_enabled. + // If it's false, TEST UNIT READY will report "not ready" + // Linux and macOS send a TEST UNIT READY poll about every 1.1 seconds or faster. + // Windows polls every 2.1 seconds or so. + // So wait long enough for host to send a TEST UNIT READY and receive a reply. + mp_hal_delay_ms(2500); } filesystem_set_internal_writable_by_usb(enabled); - storage_usb_is_enabled = enabled; return true; } diff --git a/supervisor/shared/usb/usb_msc_flash.c b/supervisor/shared/usb/usb_msc_flash.c index 6f15b508095..163019b7328 100644 --- a/supervisor/shared/usb/usb_msc_flash.c +++ b/supervisor/shared/usb/usb_msc_flash.c @@ -404,7 +404,7 @@ bool tud_msc_test_unit_ready_cb(uint8_t lun) { return false; } - if (ejected[lun] || eject_once[lun] + if (ejected[lun] || eject_once[lun] || (lun == 0 && !storage_usb_enabled()) #ifdef SDCARD_LUN || (lun == SDCARD_LUN && !sdcard_usb_enabled()) #endif From 7828aaf88131034c074a69f57f104a450910fcf5 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 16 Jul 2026 14:14:59 -0400 Subject: [PATCH 2/2] split off unsafe_disable_usb_drive() --- shared-bindings/storage/__init__.c | 66 +++++++++++++++++++----------- shared-bindings/storage/__init__.h | 1 + shared-module/storage/__init__.c | 12 ++++++ 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/shared-bindings/storage/__init__.c b/shared-bindings/storage/__init__.c index b1b1c6e6ce8..4af72d9a864 100644 --- a/shared-bindings/storage/__init__.c +++ b/shared-bindings/storage/__init__.c @@ -188,34 +188,56 @@ MP_DEFINE_CONST_FUN_OBJ_KW(storage_erase_filesystem_obj, 0, storage_erase_filesy //| def disable_usb_drive() -> None: //| """Disable presenting ``CIRCUITPY`` as a USB mass storage device. +//| By default, the device is enabled and ``CIRCUITPY`` is visible, if USB is available. +//| Must called in ``boot.py``, before USB is connected. +// If you want to disable the USB drive after `boot.py` has run, see `unsafe_disable_usb_drive()`. +//| """ +//| ... +//| +//| +static mp_obj_t storage_disable_usb_drive(void) { + #if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_MSC + if (!common_hal_storage_disable_usb_drive()) { + #else + if (true) { + #endif + mp_raise_RuntimeError(MP_ERROR_TEXT("Cannot change USB devices now")); + } + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_0(storage_disable_usb_drive_obj, storage_disable_usb_drive); + +//| def unsafe_disable_usb_drive() -> None: +//| """Disable presenting ``CIRCUITPY`` as a USB mass storage device. //| By default, the device is enabled and ``CIRCUITPY`` is visible. -//| If `disable_usb_drive()` is called in ``boot.py``, before USB is connected, -//| the mass storage device is not presented at all. -//| The drive cannot be made available again until the next hard reset; -//| `enable_usb_drive()` is not available. +//| Unlike `disable_usb_drive()`, `unsafe_disable_usb_drive()` can be called +//| after ``code.py`` starts or from the REPL, after USB has started. //| -//| If `disable_usb_drive()` is called after ``code.py`` starts, or from the REPL, -//| the USB drive logical unit (LUN) will report as "not ready", +//| When `unsafe_disable_usb_drive()` after USB has started, +//| the ``CIRCUITPY`` USB drive logical unit (LUN) will report as "not ready", //| causing the host to unmount it. -//| It can be made ready and available again by calling `enable_usb_drive()`. +//| The drive can be made ready and available again by calling `enable_usb_drive()`. //| When `disable_usb_drive` is called after ``code.py`` starts or in the REPL, //| the call will delay 2.5 seconds before returning, //| so that host has time to detect that the drive is not ready. //| The host polls the device approximately every one or two seconds. //| -//| If `disable_usb_drive()` is called when the host is actively writing CIRCUITPY, +//| Note that if ``unsafe_disable_usb_drive()`` is called when the host is actively writing CIRCUITPY, //| filesystem corruption could occur. Be careful to call it when the host is quiescent. //| //| When the USB drive is disabled, CIRCUITPY becomes read/write, and can be written //| from user code or the REPL. This is easier than arranging for a `remount()` in ``boot.py``. //| Code editors and file uploaders can use this feature to write files via the REPL. +//| +//| If `unsafe_disable_usb_drive()` is called in ``boot.py``, it is identical to calling +//| `disable_usb_drive()`. //| """ //| ... //| //| -static mp_obj_t storage_disable_usb_drive(void) { +static mp_obj_t storage_unsafe_disable_usb_drive(void) { #if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_MSC - if (!common_hal_storage_disable_usb_drive()) { + if (!common_hal_storage_unsafe_disable_usb_drive()) { #else if (true) { #endif @@ -223,18 +245,15 @@ static mp_obj_t storage_disable_usb_drive(void) { } return mp_const_none; } -MP_DEFINE_CONST_FUN_OBJ_0(storage_disable_usb_drive_obj, storage_disable_usb_drive); +MP_DEFINE_CONST_FUN_OBJ_0(storage_unsafe_disable_usb_drive_obj, storage_unsafe_disable_usb_drive); //| def enable_usb_drive() -> None: //| """Enable presenting ``CIRCUITPY`` as a USB mass storage device. //| By default, the device is enabled and ``CIRCUITPY`` is visible, -//| so you do not normally need to call this function. -//| You can call `enable_usb_drive()` in ``boot.py``, before USB is connected, -//| to reverse a `disable_usb_drive()` in ``boot.py``. +//| so you do not normally need to call this function in ``boot.py``. //| //| If you call `enable_usb_drive()` after ``code.py`` starts or in the REPL, -//| you can reverse the effect of a previous `disable_usb_drive()`, -//| but only if `disable_usb_drive()` was also called after ``code.py`` started or in the REPL. +//| you can reverse the effect of a previous `unsafe_disable_usb_drive()`. //| The CIRCUITPY drive will reappear to the host, and become read-only again //| if it was previously read-only. //| @@ -261,13 +280,14 @@ MP_DEFINE_CONST_FUN_OBJ_0(storage_enable_usb_drive_obj, storage_enable_usb_drive static const mp_rom_map_elem_t storage_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_storage) }, - { MP_ROM_QSTR(MP_QSTR_mount), MP_ROM_PTR(&storage_mount_obj) }, - { MP_ROM_QSTR(MP_QSTR_umount), MP_ROM_PTR(&storage_umount_obj) }, - { MP_ROM_QSTR(MP_QSTR_remount), MP_ROM_PTR(&storage_remount_obj) }, - { MP_ROM_QSTR(MP_QSTR_getmount), MP_ROM_PTR(&storage_getmount_obj) }, - { MP_ROM_QSTR(MP_QSTR_erase_filesystem), MP_ROM_PTR(&storage_erase_filesystem_obj) }, - { MP_ROM_QSTR(MP_QSTR_disable_usb_drive), MP_ROM_PTR(&storage_disable_usb_drive_obj) }, - { MP_ROM_QSTR(MP_QSTR_enable_usb_drive), MP_ROM_PTR(&storage_enable_usb_drive_obj) }, + { MP_ROM_QSTR(MP_QSTR_mount), MP_ROM_PTR(&storage_mount_obj) }, + { MP_ROM_QSTR(MP_QSTR_umount), MP_ROM_PTR(&storage_umount_obj) }, + { MP_ROM_QSTR(MP_QSTR_remount), MP_ROM_PTR(&storage_remount_obj) }, + { MP_ROM_QSTR(MP_QSTR_getmount), MP_ROM_PTR(&storage_getmount_obj) }, + { MP_ROM_QSTR(MP_QSTR_erase_filesystem), MP_ROM_PTR(&storage_erase_filesystem_obj) }, + { MP_ROM_QSTR(MP_QSTR_disable_usb_drive), MP_ROM_PTR(&storage_disable_usb_drive_obj) }, + { MP_ROM_QSTR(MP_QSTR_enable_usb_drive), MP_ROM_PTR(&storage_enable_usb_drive_obj) }, + { MP_ROM_QSTR(MP_QSTR_unsafe_disable_usb_drive), MP_ROM_PTR(&storage_unsafe_disable_usb_drive_obj) }, //| class VfsFat: //| def __init__(self, block_device: BlockDevice) -> None: diff --git a/shared-bindings/storage/__init__.h b/shared-bindings/storage/__init__.h index 6df60426295..0e53c78b153 100644 --- a/shared-bindings/storage/__init__.h +++ b/shared-bindings/storage/__init__.h @@ -19,4 +19,5 @@ mp_obj_t common_hal_storage_getmount(const char *path); MP_NORETURN void common_hal_storage_erase_filesystem(bool extended); bool common_hal_storage_disable_usb_drive(void); +bool common_hal_storage_unsafe_disable_usb_drive(void); bool common_hal_storage_enable_usb_drive(void); diff --git a/shared-module/storage/__init__.c b/shared-module/storage/__init__.c index 0877ffdea5e..1522136332a 100644 --- a/shared-module/storage/__init__.c +++ b/shared-module/storage/__init__.c @@ -53,6 +53,14 @@ static bool usb_drive_set_enabled(bool enabled) { } bool common_hal_storage_disable_usb_drive(void) { + if (tud_connected()) { + // Complain if already connected. Use `storage.unsafe_disable_usb_drive()` in that case. + return false; + } + return usb_drive_set_enabled(false); +} + +bool common_hal_storage_unsafe_disable_usb_drive(void) { return usb_drive_set_enabled(false); } @@ -64,6 +72,10 @@ bool common_hal_storage_disable_usb_drive(void) { return false; } +bool common_hal_storage_unsafe_disable_usb_drive(void) { + return false; +} + bool common_hal_storage_enable_usb_drive(void) { return false; }