From 303362196df2b5ab3206169e9b43c02de0b065a9 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 3 Sep 2026 13:49:10 +0200 Subject: [PATCH 1/3] feat(sd): expose a raw block device on the SPI path too, for USB-MSC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit USB Mass Storage was reachable only from the native-SDMMC backend, because detachFilesystemForRawAccess() lived behind FREEINK_SD_SDMMC. That kept the capability limited to the X4 Pro / de-link / Paper Mono class of board and locked out every SPI-SD board — notably the LilyGo T5 S3, whose card is on the shared SPI bus (SCLK14 MISO21 MOSI13 CS12, vendor pinmap docs/pinmap.md). No second driver is needed for that: SdFat's SdCardInterface already derives from FsBlockDeviceInterface and implements the same readSector(s)/writeSector(s) contract SdmmcBlockDevice does, so the card object IS the block device. The SPI path only has to drop the FsVolume while keeping the card session alive, which is FsVolume::end() rather than SdFat::end() (the latter also ends the card). Remounting goes back through begin(), whose sd.begin() re-runs SdCard::begin() on the same factory-owned card object. rawBlockDevice() now answers on both backends and returns the interface type rather than the SDMMC-specific one; it had no callers. Also corrects the FREEINK_CAP_USB_MSC comment in BoardConfig.h, which claimed the capability forces ARDUINO_USB_MODE=0. It does not: the shipped implementation keeps USB Serial/JTAG as the board's normal USB personality and switches the shared PHY to OTG at runtime for the transfer only. The real build requirement is the prebuilt Arduino core (CONFIG_TINYUSB_MSC_ENABLED), which a custom_sdkconfig core rebuild drops. Builds on the USB-MSC work by Julia Nguyen and Justin Mitchell: freeink-sdk #36 (feat/x4-pro-usb-support), #53, #57, and 79a82d5 ("Add USB Mass Storage capability flag"). Co-authored-by: Julia Nguyen Co-authored-by: Justin Mitchell --- .../BoardConfig/include/BoardConfig.h | 27 ++++++++++++++----- .../SDCardManager/include/SDCardManager.h | 20 ++++++++------ .../SDCardManager/src/SDCardManager.cpp | 25 +++++++++++++++++ 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index 5489a653..85a415dd 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -211,13 +211,26 @@ #define FREEINK_CAP_WARMLIGHT (FREEINK_DEVICE_X4PRO || FREEINK_DEVICE_MURPHY_M4 || FREEINK_DEVICE_EEGO_A4) #endif // USB Mass Storage ("USB Transfer" mode): exposes the SD card to a host over -// USB-MSC. OPT-IN (default off), NOT board-derived: it forces the build into -// USB-OTG mode (ARDUINO_USB_MODE=0 + CONFIG_TINYUSB_MSC_ENABLED), which changes -// how the USB serial console works — so a board enables it in its OWN env -// alongside those flags (e.g. X4 Pro adds -DFREEINK_CAP_USB_MSC=1 -// -DARDUINO_USB_MODE=0 -DARDUINO_USB_CDC_ON_BOOT=1). Native-USB (ESP32-S3/C3 -// OTG) targets only. When 0, UsbMassStorage links stub bodies and pulls in no -// TinyUSB/MSC code. Requires SDMMC/SPI storage exposing a block device. +// USB-MSC. OPT-IN (default off), NOT board-derived, so a board enables it in +// its OWN env (e.g. -DFREEINK_CAP_USB_MSC=1). Native-USB (ESP32-S3/C3 OTG) +// targets only. When 0, UsbMassStorage links stub bodies and pulls in no +// TinyUSB/MSC code. +// +// ARDUINO_USB_MODE=0 is one way to reach the OTG PHY, not a requirement of this +// library. A firmware can equally keep ARDUINO_USB_MODE=1 — so USB Serial/JTAG +// stays the board's normal USB personality for monitoring and flashing — and +// switch the shared PHY to OTG at runtime for the duration of a transfer, +// handing it back before it reboots out of the mode. X4 Pro and LilyGo T5 S3 +// both ship that way in CrossPoint. +// +// What this DOES require is the platform's prebuilt Arduino core, whose TinyUSB +// component is built with CONFIG_TINYUSB_MSC_ENABLED: an env that rebuilds the +// core from source (custom_sdkconfig / custom_component_remove) drops that +// component and USBMSC will not link. +// +// Storage must expose a 512-byte-sector block device. Both SDCardManager +// backends do — SDMMC natively, and SPI through SdFat's own SdCard — so this is +// no longer an SDMMC-only capability. #ifndef FREEINK_CAP_USB_MSC #define FREEINK_CAP_USB_MSC 0 #endif diff --git a/libs/hardware/SDCardManager/include/SDCardManager.h b/libs/hardware/SDCardManager/include/SDCardManager.h index 4fe8df8a..d14b8377 100644 --- a/libs/hardware/SDCardManager/include/SDCardManager.h +++ b/libs/hardware/SDCardManager/include/SDCardManager.h @@ -75,16 +75,20 @@ class SDCardManager { using PowerHook = void (*)(); void setPowerHook(PowerHook hook) { _powerHook = hook; } -#if FREEINK_SD_SDMMC - // The raw SDMMC block device (512-byte sector I/O) backing the volume, for - // exposing the card over USB-MSC ("USB Transfer" mode). Null until begin() - // succeeds. The returned pointer implements SdFat's FsBlockDeviceInterface. + // The raw block device (512-byte sector I/O) backing the volume, for exposing + // the card over USB-MSC ("USB Transfer" mode). Null until begin() succeeds. // Do NOT touch the filesystem while the card is handed to the USB host. - freeink::SdmmcBlockDevice* rawBlockDevice() { return _dev; } - // End the FsVolume mount while keeping the native block device alive for a - // raw USB-MSC owner. The caller must reinitialize the manager after the - // owner releases the card. + // + // Both backends answer this: SDMMC boards hand back their native esp-idf + // device, SPI boards hand back SdFat's own SdCard (SdCardInterface derives + // from FsBlockDeviceInterface, so the card object IS a block device — no + // second driver is needed for the SPI path). + FsBlockDeviceInterface* rawBlockDevice(); + // End the FsVolume mount while keeping the block device alive for a raw + // USB-MSC owner. The caller must reinitialize the manager (begin()) after the + // owner releases the card. Returns null when nothing is mounted. FsBlockDeviceInterface* detachFilesystemForRawAccess(); +#if FREEINK_SD_SDMMC // Stop the card for deep sleep: unmount the volume, stop the SDMMC host, and // float the bus pads so their pull-ups stop back-feeding the card's VDD net // through sleep. Idempotent; call only after all file users have stopped. A diff --git a/libs/hardware/SDCardManager/src/SDCardManager.cpp b/libs/hardware/SDCardManager/src/SDCardManager.cpp index eafd25ac..8c4e0c12 100644 --- a/libs/hardware/SDCardManager/src/SDCardManager.cpp +++ b/libs/hardware/SDCardManager/src/SDCardManager.cpp @@ -65,6 +65,8 @@ bool SDCardManager::begin() { return initialized; } +FsBlockDeviceInterface* SDCardManager::rawBlockDevice() { return _dev; } + FsBlockDeviceInterface* SDCardManager::detachFilesystemForRawAccess() { if (!initialized || !_dev) return nullptr; _vol.end(); @@ -195,6 +197,29 @@ bool SDCardManager::begin() { return initialized; } + +// SdFat's card object already IS a block device: SdCardInterface derives from +// FsBlockDeviceInterface and implements the same readSector(s)/writeSector(s) +// contract SdmmcBlockDevice does. So the SPI path needs no second driver for +// USB-MSC — it only needs the volume dropped while the card session stays up. +FsBlockDeviceInterface* SDCardManager::rawBlockDevice() { return initialized ? sd.card() : nullptr; } + +FsBlockDeviceInterface* SDCardManager::detachFilesystemForRawAccess() { + if (!initialized) return nullptr; + auto* const card = sd.card(); + if (!card) return nullptr; + // FsVolume::end() ONLY — deliberately not sd.end(), which would also call + // SdCard::end() and tear down the card session the USB host is about to read + // through. The card stays initialized and selected; remounting later goes + // back through begin(), whose sd.begin() re-runs SdCard::begin() on the same + // (factory-owned, statically allocated) card object. + sd.FsVolume::end(); + initialized = false; + cachedTotalBytes = 0; + cachedUsedBytes = 0; + cachedUsedBytesValid = false; + return card; +} #endif bool SDCardManager::ready() const { From 6afb940381021ffb94baf638d32f0bb60481a2b0 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 3 Sep 2026 14:05:55 +0200 Subject: [PATCH 2/3] fix(sd): turn USE_BLOCK_DEVICE_INTERFACE on with the USB-MSC capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SPI raw-block-device path added in the previous commit does not compile on its own: SdFat's SdSpiCard only derives from FsBlockDeviceInterface when USE_BLOCK_DEVICE_INTERFACE (or HAS_SDIO_CLASS) is set — otherwise it is a plain concrete class with no such base and sd.card() cannot be returned as one. SdFat compiles as its own library, so the option has to be appended to every lib builder's env; the SDCardManager build hook already does exactly that for USE_UTF8_LONG_NAMES, so it grows a second, conditional define. Coupling it to FREEINK_CAP_USB_MSC rather than turning it on globally keeps the vtable and the indirect sector calls off the boards that would gain nothing from them — notably the C3, which has no USB-OTG peripheral and can never serve MSC at all. The raw-access functions are guarded to match and link as nullptr-returning stubs when the option is absent, so a board that never asked for USB Drive still builds. Co-authored-by: Julia Nguyen Co-authored-by: Justin Mitchell --- .../SDCardManager/inject_build_flags.py | 57 ++++++++++++++----- .../SDCardManager/src/SDCardManager.cpp | 25 ++++++-- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/libs/hardware/SDCardManager/inject_build_flags.py b/libs/hardware/SDCardManager/inject_build_flags.py index e55b08b4..3ea34027 100644 --- a/libs/hardware/SDCardManager/inject_build_flags.py +++ b/libs/hardware/SDCardManager/inject_build_flags.py @@ -1,23 +1,50 @@ -# SDCardManager build hook: force SdFat's UTF-8 long-filename support on for -# the WHOLE build (SdFat compiles as its own library, so a define in our -# library.json "flags" would not reach it). +# SDCardManager build hook: force two SdFat options on for the WHOLE build. +# SdFat compiles as its own library, so a define in our library.json "flags" +# would not reach it — it has to be appended to every lib builder's env here. # -# Without USE_UTF8_LONG_NAMES, SdFat returns mangled names for any file with -# a non-ASCII character ("The 7½ Deaths..." listed but unopenable — no -# metadata, no cover, no reading). There is no situation where a FreeInk -# firmware wants that, so this is not a user-facing option. +# 1. USE_UTF8_LONG_NAMES, always. Without it, SdFat returns mangled names for +# any file with a non-ASCII character ("The 71/2 Deaths..." listed but +# unopenable — no metadata, no cover, no reading). There is no situation +# where a FreeInk firmware wants that, so this is not a user-facing option. +# +# 2. USE_BLOCK_DEVICE_INTERFACE, but only when the build enables USB Mass +# Storage (FREEINK_CAP_USB_MSC). It is what makes SdSpiCard derive from +# FsBlockDeviceInterface, which is how SDCardManager hands the SPI-attached +# card to a USB host as a raw block device — without it SdSpiCard is a plain +# concrete class and detachFilesystemForRawAccess() has nothing to return. +# The SDMMC backend needs the same option for its FsVolume, so coupling both +# to the capability keeps it off the boards that pay for it in vtables and +# indirect calls for nothing. Import("env") -_DEFINE = ("USE_UTF8_LONG_NAMES", "1") +_ALWAYS = [("USE_UTF8_LONG_NAMES", "1")] +_IF_USB_MSC = [("USE_BLOCK_DEVICE_INTERFACE", "1")] + + +def _defines(e): + return {d[0] if isinstance(d, (tuple, list)) else d for d in e.get("CPPDEFINES", [])} + + +def _usb_msc_enabled(e): + for d in e.get("CPPDEFINES", []): + if isinstance(d, (tuple, list)) and d[0] == "FREEINK_CAP_USB_MSC": + return str(d[1]) not in ("0", "") + return False + +def _append(e, wanted): + have = _defines(e) + missing = [d for d in wanted if d[0] not in have] + if missing: + e.Append(CPPDEFINES=missing) -def _append(e): - defines = {d[0] if isinstance(d, tuple) else d for d in e.get("CPPDEFINES", [])} - if _DEFINE[0] not in defines: - e.Append(CPPDEFINES=[_DEFINE]) +_wanted = list(_ALWAYS) +# Read the capability off the project env: a lib builder's env may not carry it. +if _usb_msc_enabled(env) or _usb_msc_enabled(DefaultEnvironment()): + _wanted += _IF_USB_MSC -_append(env) -_append(DefaultEnvironment()) +_append(env, _wanted) +_append(DefaultEnvironment(), _wanted) for lb in env.GetLibBuilders(): - _append(lb.env) + _append(lb.env, _wanted) diff --git a/libs/hardware/SDCardManager/src/SDCardManager.cpp b/libs/hardware/SDCardManager/src/SDCardManager.cpp index 8c4e0c12..a57c5502 100644 --- a/libs/hardware/SDCardManager/src/SDCardManager.cpp +++ b/libs/hardware/SDCardManager/src/SDCardManager.cpp @@ -198,10 +198,18 @@ bool SDCardManager::begin() { return initialized; } -// SdFat's card object already IS a block device: SdCardInterface derives from -// FsBlockDeviceInterface and implements the same readSector(s)/writeSector(s) -// contract SdmmcBlockDevice does. So the SPI path needs no second driver for -// USB-MSC — it only needs the volume dropped while the card session stays up. +// SdFat's card object already IS a block device: with USE_BLOCK_DEVICE_INTERFACE +// (or HAS_SDIO_CLASS) set, SdSpiCard derives from FsBlockDeviceInterface and +// implements the same readSector(s)/writeSector(s) contract SdmmcBlockDevice +// does. So the SPI path needs no second driver for USB-MSC — it only needs the +// volume dropped while the card session stays up. +// +// Without that option SdSpiCard is a plain concrete class with no such base, so +// there is nothing to hand out. The build hook (inject_build_flags.py) turns the +// option on whenever FREEINK_CAP_USB_MSC is set, so the stubs below are what a +// board that never asked for USB Drive links. +#if USE_BLOCK_DEVICE_INTERFACE || HAS_SDIO_CLASS + FsBlockDeviceInterface* SDCardManager::rawBlockDevice() { return initialized ? sd.card() : nullptr; } FsBlockDeviceInterface* SDCardManager::detachFilesystemForRawAccess() { @@ -220,7 +228,14 @@ FsBlockDeviceInterface* SDCardManager::detachFilesystemForRawAccess() { cachedUsedBytesValid = false; return card; } -#endif + +#else // USE_BLOCK_DEVICE_INTERFACE || HAS_SDIO_CLASS + +FsBlockDeviceInterface* SDCardManager::rawBlockDevice() { return nullptr; } +FsBlockDeviceInterface* SDCardManager::detachFilesystemForRawAccess() { return nullptr; } + +#endif // USE_BLOCK_DEVICE_INTERFACE || HAS_SDIO_CLASS +#endif // FREEINK_SD_SDMMC bool SDCardManager::ready() const { return initialized; From a587342489a6aeef2b1b142b251828b79f384a15 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 3 Sep 2026 15:44:28 +0200 Subject: [PATCH 3/3] feat(usb,battery): give consumers a way to see the cable leave MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An ESP32-S3 device cannot detect an unplug through TinyUSB. Arduino's tinyusb init passes otg_io_conf = NULL (cores/esp32/esp32-hal-tinyusb.c:140), so no VBUS line is routed to the OTG core through the GPIO matrix and IDF forces B-session-valid permanently on. The core never sees session end, no DCD_EVENT_UNPLUGGED is raised, and tud_mounted() stays true after the cable is gone — so UsbMassStorage::state() never reaches Disconnected and a USB-MSC session has no way to end itself. Device-observed on a LilyGo T5 S3. Two signals, because no single one covers every board: UsbMassStorage::hostSuspended() wraps tud_suspended(). Bus suspend is detected by the OTG core from bus idle (no SOF for >3 ms), independent of VBUS, so it survives the forced B-valid. It is a HINT rather than a verdict — a host suspending an idle bus is indistinguishable — so it is exposed raw and the caller is told to require persistence. BatteryMonitor::isExternalPowerPresent() reads the BQ25896's REG0B VBUS_STAT [7:5] and PG_STAT [2], out of the same register readGaugeCharging() already uses for CHRG_STAT. This is a physical reading of the input rail and is unambiguous. It is deliberately NOT isCharging(): a full battery stops charging with the cable still attached, so charge state reports "unplugged" while plugged in — the exact failure mode that makes charge-based USB inference wrong. There is no gauge fallback for it. The BQ27220 measures the battery, not the input rail, so a board with a gauge but no charger IC genuinely cannot see this; it reports `known = false` and callers must branch on that. Answering "no external power" from a sensor that cannot observe external power would be worse than admitting ignorance. The M5 PMIC path uses the externalPower field readM5Pm1Status() already decodes from PWR_SRC, not the charging flag derived from it. --- .../BatteryMonitor/include/BatteryMonitor.h | 15 ++++++ .../BatteryMonitor/src/BatteryMonitor.cpp | 52 +++++++++++++++++++ .../UsbMassStorage/include/UsbMassStorage.h | 15 ++++++ .../UsbMassStorage/src/UsbMassStorage.cpp | 3 ++ 4 files changed, 85 insertions(+) diff --git a/libs/hardware/BatteryMonitor/include/BatteryMonitor.h b/libs/hardware/BatteryMonitor/include/BatteryMonitor.h index bcef6f80..2ec37d38 100644 --- a/libs/hardware/BatteryMonitor/include/BatteryMonitor.h +++ b/libs/hardware/BatteryMonitor/include/BatteryMonitor.h @@ -71,6 +71,21 @@ class BatteryMonitor { // a board with a gauge but no charger IC (e.g. X3) still reports it. bool isCharging() const; + // True when external power (a USB cable or charger) is physically present. + // + // This is NOT isCharging(): a full battery stops charging while still + // plugged in, so isCharging() goes false with the cable still attached. + // Only a source that reports the input rail itself can answer this, so it + // is currently the BQ25896's REG0B — VBUS_STAT[7:5] plus PG_STAT — and + // nothing else. Boards without that charger IC cannot observe it. + // + // `known` (optional, out) is set false when the board has no way to tell, + // or the read failed. Callers MUST branch on it: a bare false means "no + // opinion" just as often as it means "unplugged", and treating the two the + // same is how you get a device that thinks the cable is gone because it + // never had a way to see it. + bool isExternalPowerPresent(bool* known = nullptr) const; + // Percentage from a millivolt value, off a standard 1S Li-ion discharge // curve. The result is always a multiple of 10: voltage cannot resolve a // Li-ion pack any finer than that, and pretending otherwise just produces a diff --git a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp index 1687668c..bb556fe0 100644 --- a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp +++ b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp @@ -290,6 +290,35 @@ bool readGaugeCharging(bool& known) { known = false; return false; } + +// External-power presence from the charger IC, out of the SAME REG0B read that +// readGaugeCharging() uses for CHRG_STAT: +// VBUS_STAT [7:5] — 000 no input, 001 USB host SDP, 010 USB CDP, 011 adapter, +// 111 OTG (we are SOURCING power, not receiving it) +// PG_STAT [2] — power good +// Anything but "no input" and "OTG" means a cable is supplying us. Unlike +// charge state this stays true at 100%, which is exactly the case that makes +// charge-based USB inference wrong. +// +// Only the BQ25896 answers this. The BQ27220 gauge measures the BATTERY, not +// the input rail, so there is deliberately no gauge fallback here — reporting +// "no external power" from a gauge that cannot see the input would be a lie. +bool readChargerExternalPower(bool& known) { + const auto& g = BoardConfig::ACTIVE.batteryGauge; + if (g.chargerAddr == 0) { + known = false; + return false; + } + uint8_t status = 0; + if (!readReg8(g.chargerAddr, BQ25896_REG_STATUS, status)) { + known = false; + return false; + } + known = true; + const uint8_t vbus = (status >> 5) & 0x07; + const bool powerGood = (status & 0x04) != 0; + return (vbus != 0x00 && vbus != 0x07) || powerGood; +} } // namespace #endif // FREEINK_BATTERY_I2C_GAUGE @@ -506,6 +535,29 @@ bool BatteryMonitor::isCharging() const { return digitalRead(_chargeStatusPin) == chargeActiveLevel(); } +bool BatteryMonitor::isExternalPowerPresent(bool* known) const { + bool observed = false; +#if FREEINK_BATTERY_I2C_GAUGE + const bool present = readChargerExternalPower(observed); + if (observed) { + if (known) *known = true; + return present; + } +#endif + if (hasM5Pm1Backend()) { + // The M5 PMIC reports its supply source directly (PWR_SRC 5VIN/5VINOUT), + // which readM5Pm1Status() already decodes into externalPower — the right + // field here, rather than the `charging` one it derives from it. + Status status; + if (readM5Pm1Status(status) && status.externalPowerKnown) { + if (known) *known = true; + return status.externalPower; + } + } + if (known) *known = false; + return false; +} + bool BatteryMonitor::readM5Pm1Status(Status& status) const { status.supported = true; if (!hasM5Pm1Backend()) return false; diff --git a/libs/hardware/UsbMassStorage/include/UsbMassStorage.h b/libs/hardware/UsbMassStorage/include/UsbMassStorage.h index 3c2b0269..210daa52 100644 --- a/libs/hardware/UsbMassStorage/include/UsbMassStorage.h +++ b/libs/hardware/UsbMassStorage/include/UsbMassStorage.h @@ -35,6 +35,20 @@ class UsbMassStorage { bool active() const { return _active; } UsbMassStorageState state() const; bool hostConnected() const; + // True while the USB bus is idle (no SOF for >3 ms), which is what the device + // sees when the cable is pulled. + // + // Needed because tud_mounted() CANNOT report an unplug on the ESP32-S3: + // Arduino's tinyusb init passes otg_io_conf = NULL (esp32-hal-tinyusb.c), so + // no VBUS line is routed to the OTG core and IDF forces B-session-valid on. + // The core therefore never detects session end, and state() stays Connected + // forever after the cable is gone. Bus suspend is detected by the core itself + // and is unaffected. + // + // A host suspending an idle bus looks identical, so this is a HINT, not a + // verdict: callers must require it to persist before acting on it, and should + // prefer a physical VBUS reading where the board has one. + bool hostSuspended() const; // Soft-disconnect the USB device from the host. Call from application/task // context, never from an MSC callback; end() still owns final teardown. bool disconnectHost() const; @@ -64,6 +78,7 @@ class UsbMassStorage { UsbMassStorageState state() const { return UsbMassStorageState::Idle; } bool hostConnected() const { return false; } bool disconnectHost() const { return false; } + bool hostSuspended() const { return false; } }; } // namespace freeink diff --git a/libs/hardware/UsbMassStorage/src/UsbMassStorage.cpp b/libs/hardware/UsbMassStorage/src/UsbMassStorage.cpp index eef550c9..4cbbcab3 100644 --- a/libs/hardware/UsbMassStorage/src/UsbMassStorage.cpp +++ b/libs/hardware/UsbMassStorage/src/UsbMassStorage.cpp @@ -12,6 +12,7 @@ extern "C" bool tud_mounted(void); extern "C" bool tud_disconnect(void); +extern "C" bool tud_suspended(void); namespace freeink { namespace { @@ -190,6 +191,8 @@ bool UsbMassStorage::hostConnected() const { bool UsbMassStorage::disconnectHost() const { return _active && tud_disconnect(); } +bool UsbMassStorage::hostSuspended() const { return _active && tud_suspended(); } + void UsbMassStorage::markAccessed() const { auto current = _state.load(); while (current != UsbMassStorageState::Ejected && current != UsbMassStorageState::IoError) {