diff --git a/docs/environment.rst b/docs/environment.rst index 7d0e41727d3..f4ceaf9a9cb 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -4,7 +4,7 @@ Environment Variables CircuitPython provides support for environment variables. These values can be examined by user code, and are also used as settings by CircuitPython during startup. -CircuitPython looks for a file called ``settings.toml`` at the ``CIRCUITPY`` drive root +CircuitPython looks for a file called ``settings.toml`` at the **CIRCUITPY** drive root to find the values of environment variables, The file format is a subset of the `TOML config file language `__. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 7fad2aac181..d505ff09ef0 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -7,22 +7,22 @@ variety of errors that can happen, what they mean and how to fix them. File system issues ------------------ -If your host computer starts complaining that your ``CIRCUITPY`` drive is corrupted +If your host computer starts complaining that your **CIRCUITPY** drive is corrupted or files cannot be overwritten or deleted, then you will have to erase it completely. -When CircuitPython restarts it will create a fresh empty ``CIRCUITPY`` filesystem. +When CircuitPython restarts it will create a fresh empty **CIRCUITPY** filesystem. -Corruption often happens on Windows when the ``CIRCUITPY`` disk is not safely ejected +Corruption often happens on Windows when the **CIRCUITPY** disk is not safely ejected before being reset by the button or being disconnected from USB. This can also happen on Linux and Mac OSX but it's less likely. -.. caution:: To erase and re-create ``CIRCUITPY`` (for example, to correct a corrupted filesystem), +.. caution:: To erase and re-create **CIRCUITPY** (for example, to correct a corrupted filesystem), follow one of the procedures below. It's important to note that **any files stored on the** - ``CIRCUITPY`` **drive will be erased. Back up your code if possible before continuing!** + **CIRCUITPY** **drive will be erased. Back up your code if possible before continuing!** REPL Erase Method ^^^^^^^^^^^^^^^^^ This is the recommended method of erasing your board. If you are having trouble accessing the -``CIRCUITPY`` drive or the REPL, consider first putting your board into +**CIRCUITPY** drive or the REPL, consider first putting your board into `safe mode `_. **To erase any board if you have access to the REPL:** @@ -30,7 +30,7 @@ This is the recommended method of erasing your board. If you are having trouble #. Connect to the CircuitPython REPL using a terminal program. #. Type ``import storage`` into the REPL. #. Then, type ``storage.erase_filesystem()`` into the REPL. -#. The ``CIRCUITPY`` drive will be erased and the board will restart with an empty ``CIRCUITPY`` drive. +#. The **CIRCUITPY** drive will be erased and the board will restart with an empty **CIRCUITPY** drive. Erase File Method ^^^^^^^^^^^^^^^^^ diff --git a/ports/espressif/README.rst b/ports/espressif/README.rst index fe5542aafe4..5af91591f7e 100644 --- a/ports/espressif/README.rst +++ b/ports/espressif/README.rst @@ -41,7 +41,7 @@ Connecting to the ESP32-C3 **USB Connection:** -On ESP32-C3 REV3 chips, a USB Serial/JTAG Controller is available. Note: This USB connection cannot be used for a ``CIRCUITPY`` drive. +On ESP32-C3 REV3 chips, a USB Serial/JTAG Controller is available. Note: This USB connection cannot be used for a **CIRCUITPY** drive. Depending on the board you have, the USB port may or may not be connected to native USB. diff --git a/shared-bindings/dualbank/__init__.c b/shared-bindings/dualbank/__init__.c index 2c00a5b0100..160ce4947a7 100644 --- a/shared-bindings/dualbank/__init__.c +++ b/shared-bindings/dualbank/__init__.c @@ -34,7 +34,7 @@ //| This module is unavailable as the flash is only large enough for one app partition. //| //| Boards with flash ``>2MB``: -//| This module is enabled/disabled at runtime based on whether the ``CIRCUITPY`` drive +//| This module is enabled/disabled at runtime based on whether the **CIRCUITPY** drive //| is extended or not. See `storage.erase_filesystem()` for more information. //| //| .. code-block:: python diff --git a/shared-bindings/storage/__init__.c b/shared-bindings/storage/__init__.c index 14ec16f3096..8bd43686694 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. @@ -140,24 +140,24 @@ static mp_obj_t storage_getmount(const mp_obj_t mnt_in) { MP_DEFINE_CONST_FUN_OBJ_1(storage_getmount_obj, storage_getmount); //| def erase_filesystem(extended: Optional[bool] = None) -> None: -//| """Erase and re-create the ``CIRCUITPY`` filesystem. +//| """Erase and re-create the **CIRCUITPY** filesystem. //| -//| On boards that present USB-visible ``CIRCUITPY`` drive (e.g., SAMD21 and SAMD51), +//| On boards that present USB-visible **CIRCUITPY** drive (e.g., SAMD21 and SAMD51), //| then call `microcontroller.reset()` to restart CircuitPython and have the -//| host computer remount CIRCUITPY. +//| host computer remount **CIRCUITPY**. //| -//| This function can be called from the REPL when ``CIRCUITPY`` +//| This function can be called from the REPL when **CIRCUITPY** //| has become corrupted. //| //| :param bool extended: On boards that support ``dualbank`` module -//| and the ``extended`` parameter, the ``CIRCUITPY`` storage can be +//| and the ``extended`` parameter, the **CIRCUITPY** storage can be //| extended by setting this to `True`. If this isn't provided or //| set to `None` (default), the existing configuration will be used. //| //| .. note:: New firmware starts with storage extended. In case of an existing //| filesystem (e.g. uf2 load), the existing extension setting is preserved. //| -//| .. warning:: All the data on ``CIRCUITPY`` will be lost, and +//| .. warning:: All the data on **CIRCUITPY** will be lost, and //| CircuitPython will restart on certain boards.""" //| ... //| @@ -187,9 +187,11 @@ static mp_obj_t storage_erase_filesystem(size_t n_args, const mp_obj_t *pos_args MP_DEFINE_CONST_FUN_OBJ_KW(storage_erase_filesystem_obj, 0, storage_erase_filesystem); //| 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.""" +//| """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()`. +//| """ //| ... //| //| @@ -205,15 +207,82 @@ 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 unsafe_disable_usb_drive() -> None: +//| """Disable presenting **CIRCUITPY** as a USB mass storage device. +//| By default, the device is enabled and **CIRCUITPY** is visible. +//| After the call, **CIRCUITPY** will be read/write to your code or from the REPL. +//| +//| Unlike `disable_usb_drive()`, `unsafe_disable_usb_drive()` can be called +//| after ``code.py`` starts or from the REPL, after USB has started. +//| +//| .. warning:: If ``unsafe_disable_usb_drive()`` is called when the host is actively writing **CIRCUITPY**, +//| filesystem corruption can occur. +//| It is similar to the sudden physical removal of a USB drive. +//| Before calling ``unsafe_disable_usb_drive()``, +//| make sure the host has finished any writes to **CIRCUITPY**. +//| +//| * On Windows, do one of these: +//| +//| * Eject ("Safely Remove") the **CIRCUITPY** drive. +//| * Use a "sync" program, such as `Sysinternals Sync `__. +//| * Programmatically call ``_commit()`` or ``_flushall()`` or similar. +//| +//| * On Linux or macOS, do one of these: +//| +//| * Eject (unmount) the **CIRCUITPY** drive. +//| * Type ``sync`` in a terminal. +//| * Programmatically call ``sync()`` or ``fsync()``. +//| +//| * If none of the above are possible or convenient, wait several seconds to allow any writes to complete. +//| This can be unreliable, as the interval to wait depends on the host operating system +//| and how the drive is mounted. +//| In some operating systems, you can specify that the drive be mounted as "sync on write", +//| so that all writes happen immediately. +//| +//| When `unsafe_disable_usb_drive()` is called after USB has started, +//| the **CIRCUITPY** USB drive logical unit (LUN) will report as "not ready", +//| causing the host to unmount it. +//| 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. +//| +//| 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_unsafe_disable_usb_drive(void) { + #if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_MSC + if (!common_hal_storage_unsafe_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_unsafe_disable_usb_drive_obj, storage_unsafe_disable_usb_drive); + //| def enable_usb_drive() -> None: -//| """Enabled 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. +//| """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 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 `unsafe_disable_usb_drive()`. +//| 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. //| """ //| ... @@ -234,13 +303,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 dedcee1ff23..1522136332a 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,16 +38,29 @@ 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; } 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); } @@ -59,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; } 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