USB Mass Storage on SPI-SD boards, and a signal for the cable being pulled - #74
Open
jpirnay wants to merge 3 commits into
Open
USB Mass Storage on SPI-SD boards, and a signal for the cable being pulled#74jpirnay wants to merge 3 commits into
jpirnay wants to merge 3 commits into
Conversation
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 Free-Ink#36 (feat/x4-pro-usb-support), Free-Ink#53, Free-Ink#57, and 79a82d5 ("Add USB Mass Storage capability flag"). Co-authored-by: Julia Nguyen <julia@uxj.io> Co-authored-by: Justin Mitchell <justin@jmitch.com>
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 <julia@uxj.io> Co-authored-by: Justin Mitchell <justin@jmitch.com>
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.
jpirnay
added a commit
to jpirnay/freeink-sdk
that referenced
this pull request
Sep 3, 2026
Both changes were made while preparing the PRs and only ever existed on the PR branches, which were cut from Free-Ink main rather than from here -- so our copy had the weaker version of each. The two pinch tests (Free-Ink#75) assert what the commit message claims and nothing did before: that rotation and pinch can never both accept one gesture, checked from both sides, and that a gesture converging by exactly 20% while both contacts travel 80 px is a pinch rather than the two-finger swipe the translation path would also accept. Host suite goes 35 -> 43 checks. The FREEINK_CAP_USB_MSC comment (Free-Ink#74) no longer talks about "an earlier revision of this comment", which meant nothing outside our own history, and states the requirement positively: ARDUINO_USB_MODE=0 is one way to reach the OTG PHY, not a requirement, and the actual constraint is the prebuilt Arduino core carrying CONFIG_TINYUSB_MSC_ENABLED. Deliberately NOT synced: the stray clang-format reflow in gslUploadFirmware() that Free-Ink#75 drops. The SDK ships no .clang-format, so our pre-commit hook formats these files to the firmware's 120-column limit and would simply re-split that line on the next commit that touches the file. It stays a fork-local artifact.
jpirnay
added a commit
to jpirnay/freeink-sdk
that referenced
this pull request
Sep 3, 2026
Brings in OnePage ESP32-C61 board support (Free-Ink#68), the OnePage shared-SD-rail fix (Free-Ink#72), the driver deepSleep() rework that skips power-off when the screen is already off, and FreeInkUI inline list section headings. One conflict, in SDCardManager.h, and it is the mirror of the one that had to be resolved to get Free-Ink#74 onto main. Our side moved rawBlockDevice()/detachFilesystemForRawAccess() OUT of the #if FREEINK_SD_SDMMC guard (both backends answer them now), which left main's new shutdown() and its #else/#endif orphaned. Resolved by giving shutdown() its own guard. prepareForSleep() (PR Free-Ink#51) and shutdown() (main) now BOTH exist and both stay: on the SDMMC path shutdown() is a superset -- same unmount plus floating the bus pads -- so upstream will want to reconcile them, but that is a decision for Free-Ink#51 and not something to settle inside a merge. Neither is lost and neither changed. Nothing else conflicted: main's deepSleep() rework is confined to the Ssd1677 / Uc8179 / Uc8279 / Uc8279X4 drivers and does not touch LgfxEpdDriver, where our powerControl()/_pwr_known work lives.
Contributor
|
Interesting. Will this allow it to work on the sticky or no? |
1 similar comment
Contributor
|
Interesting. Will this allow it to work on the sticky or no? |
Contributor
Author
I don't have a sticky device to check, but I am doubting it would support it: looking at the schematics :
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
USB Mass Storage was reachable only from the native-SDMMC backend, because
detachFilesystemForRawAccess()lived behindFREEINK_SD_SDMMC. That kept the capability to the X4 Pro / de-link / Paper Mono class of board and locked out every SPI-SD board. This opens it to them, and adds the one signal an ESP32-S3 needs in order to notice that the cable was pulled.Three additive commits. No board changes behaviour unless its own env sets
FREEINK_CAP_USB_MSC.1. The SPI path needs no second block-device driver
SdFat's
SdCardInterfacealready derives fromFsBlockDeviceInterfaceand implements the samereadSector(s)/writeSector(s)contractSdmmcBlockDevicedoes — the card object is the block device. So the SPI path only has to drop theFsVolumewhile keeping the card session alive:FsVolume::end(), deliberately notSdFat::end(), which would also end the card session the USB host is about to read through. Remounting goes back throughbegin(), whosesd.begin()re-runsSdCard::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, so this is not a break.2.
USE_BLOCK_DEVICE_INTERFACEhas to be on for that to compileSdSpiCardderives fromFsBlockDeviceInterfaceonly whenUSE_BLOCK_DEVICE_INTERFACE(orHAS_SDIO_CLASS) is set; otherwise it is a plain concrete class andsd.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 — which is exactly what theSDCardManagerbuild hook already does forUSE_UTF8_LONG_NAMES, so it grows a second, conditional define.It is coupled to
FREEINK_CAP_USB_MSCrather than turned on globally, to keep the vtable and the indirect sector calls off boards that would gain nothing — 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 asnullptr-returning stubs when the option is absent, so a board that never asked for USB Drive still builds unchanged.3. An S3 device cannot see 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, noDCD_EVENT_UNPLUGGEDis raised, andtud_mounted()stays true after the cable is gone — soUsbMassStorageState::Disconnectedis unreachable and an MSC session has no way to end itself. Device-observed on a LilyGo T5 S3: transfers worked, then pulling the cable left the reader on its Connected screen forever.Two signals, because no single one covers every board:
UsbMassStorage::hostSuspended()tud_suspended()— bus idle (no SOF >3 ms), detected by the core independently of VBUS, so it survives the forced B-validBatteryMonitor::isExternalPowerPresent()REG0B:VBUS_STAT[7:5]+PG_STAT[2], out of the same registerreadGaugeCharging()already reads forCHRG_STATisExternalPowerPresent()is deliberately notisCharging(): 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 deliberately no gauge fallback either: the BQ27220 measures the battery, not the input rail, so a board with a gauge but no charger IC genuinely cannot see this. It reportsknown = falseand callers must branch on it, rather than have the SDK answer "no external power" from a sensor that cannot observe external power.Also: a comment correction
FREEINK_CAP_USB_MSC's comment said the capability forces the build into USB-OTG mode (ARDUINO_USB_MODE=0). It does not have to: a firmware can keepARDUINO_USB_MODE=1, so USB Serial/JTAG stays the board's normal personality for monitoring and flashing, and switch the shared PHY to OTG at runtime for the duration of a transfer. Both X4 Pro and LilyGo T5 S3 ship that way in CrossPoint. The real build requirement — which was not documented — is the platform's prebuilt Arduino core, whose TinyUSB component carriesCONFIG_TINYUSB_MSC_ENABLED; an env that rebuilds the core from source (custom_sdkconfig/custom_component_remove) drops that component andUSBMSCwill not link.Testing
rawBlockDevice()return type and the guard restructure, but I want to be explicit that I have not exercised the SDMMC path.x4pro,lilygo_t5s3and the C3defaultenv (the last is the one that must not pick upUSE_BLOCK_DEVICE_INTERFACE).Branched from
main; the work originally sat on top of #51 and was rebased off it, so this PR carries no LilyGo board changes and is independent of that one.Attribution
Builds on the USB-MSC work by @uxjulia (#36
feat/x4-pro-usb-support, #53, #57) and @itsthisjustin (79a82d5, the capability flag).Co-authored-bytrailers are on the commits.AI usage
PARTIALLY. An AI coding assistant was used for the investigation, for drafting the code comments, and for this description. The hardware bring-up and the device validation on the LilyGo were done by hand on the board.