Skip to content

Add a generic SD card service with live-configurable pins - #134

Closed
TouchMyLight wants to merge 0 commit into
MoonModules:mainfrom
TouchMyLight:sdcard
Closed

TouchMyLight wants to merge 0 commit into
MoonModules:mainfrom
TouchMyLight:sdcard

Conversation

@TouchMyLight

Copy link
Copy Markdown

add microSD read/write support, shaped as a chip-capability service rather than a board-specific firmware variant: pins are runtime configuration like AudioService's mic, and no new Kconfig option or sdkconfig fragment is needed.
Core

  • platform::hasSdCard is now a chip-capability flag (CONFIG_SOC_SDMMC_HOST_SUPPORTED), not a per-board Kconfig gate; sdMount() takes pins as a runtime SdPinConfig argument (clk/cmd/d0-d3/pwr) instead of hardcoded GPIOs, with bus width derived from which data lines are set. New platform::feedWatchdog() and platform::sdFormat() calls.
  • New SdCardModule (Services child): pin controls hidden where the chip has no SDMMC host, a usage gauge, and a hidden format button. Mounting lives in prepare()/affectsPrepare(), not setup(), so a pin edit unmounts and remounts live, the same contract I2cBusModule's sda/scl have; release() unmounts. This was a real bug caught by bench testing a first version that only wired setup().
  • HttpServerModule: /api/sddir + /api/sdfile routes mirroring the flash File Manager's, with an SD-sized Content-Length ceiling (FAT32's 4 GiB − 1) and overflow-safe parsing on this 32-bit target.

UI

  • New SD Card panel (renderSdCardManager), sharing the File Manager's tree/toolbar machinery over an apiKind parameter. A 💾 Format button, SD-panel only, press-twice armed like the existing delete/restore buttons, wipes and reformats the card as FAT32.

Catalog

  • Waveshare ESP32-P4-ETH: SdCardModule wired with its schematic pins (clk 43, cmd 44, d0-d3 39-42, pwr 45), "SD card" moved to supported.
  • check_devices.py: "SD card" added to the supported-capability vocabulary.

Tests: unit_SdCardModule.cpp — hidden pin controls on a hardware-less build, the Service role, prepare()'s "no SD hardware"/pin-live-reconfiguration contracts, release() safety, the format button's hidden declaration and its refusal while unmounted.

Docs: gpio-usage.md and core/system.md carry minimal SD mentions; the full per-board pin table belongs on docs/reference/hardware/esp32-p4-eth.md, which doesn't exist on this branch (it's part of the still-unmerged esp32-p4-eth_ar/PR #130 branch) — not duplicated here.

Bench-verified end to end on the Waveshare ESP32-P4-ETH (plain esp32p4rev1-eth firmware, flashed over OTA): mount survives reboot with persisted pins, mkdir/upload/read/delete round-trip, live pin-remount without a reboot, and — with the product owner's explicit go-ahead to wipe the card's real content — a live Format that emptied the card and left it fully writable afterward.

docs/reference/metrics/docgen.md is deliberately NOT included in this commit: regenerating it surfaces a pre-existing, unrelated error in src/light/drivers/Hub75Slots.h (a 2-line code comment, from a prior commit that never re-ran the generator) that this change did not introduce and does not fix. Warnings are otherwise flat against the committed baseline, net of one untraced +1 in the over-wide-comment-lines rule after exhaustive searching across every touched file.

@coderabbitai

coderabbitai Bot commented Oct 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: MoonModules/MoonLight/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2661ec76-a368-4c4b-aa43-1889bd04d450

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TouchMyLight

TouchMyLight commented Oct 9, 2026 •

Copy link
Copy Markdown
Author

this is bigger feature introduction. let me know if it should be changed to better be integrated into this project.
maybe a bit of context would help: I used MoonLights (v4.0.0) for an installation with 8000 LEDs over 20 outputs as a 20x20x20 cube (not easily supported by WLED, and nicer to use for others than raw fastLED). It ran stable for 4 days with DDP over Ethernet.
this first PR is to support the SD interface on the board (to be able to read large files), and another will follow up for a FSEQ 3D animation player. This way my installation can run without requiring a computer to send the animation over network.

@ewowi

ewowi commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

All of this is supercool !

I am currently working on this project: https://moonmodules.org/MoonLight/how-to/multi-board-installation.html

But exactly 20x20x20 is my next project. This is my old setup running on old MoonLight:

trim.B4A063AA-7068-43F6-87E8-2DBCB01DD1D3.MOV

So I will look at your work with great interest.

Coming back to this soon !

@TouchMyLight

Copy link
Copy Markdown
Author

more information about the installation and the hardware sources are available here: https://git.touchmylight.info/TouchMyLight/chandeLED

@ewowi

ewowi commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

more information about the installation and the hardware sources are available here: https://git.touchmylight.info/TouchMyLight/chandeLED

That's a beauty! Where can I buy 🙂

Regarding :

1x16 using PARLIO
2x8 using SPI
1x4 using RMT

You know the P4 also has lcd_cam, which give you another 16 pins to drive 🙂

And if you want even more, MoonLight supports "pinExpander" based on @hpwit his virtual clockless driver using 595 shift registers, giving you up to 120 more pins 👻

@TouchMyLight

Copy link
Copy Markdown
Author

I actually used the LCD driver for the cube. The readme was outdated, but this is fixed now.
I did not know about the 595 support though. Thanks for pointing it out. It might come in handy for pin limited boards. For the chandeLED I probably don't need more output than 24, and don't have lots of board space for more connectors
I stopped selling the boards I make because the distribution aspect does not excite me that much. I prefer spending my time designing new boards. But you can find the hardware manufacturing files in the releases and can send them to you board house. There are some improved planned for v2, but the v1 works and can be used.

@ewowi

ewowi commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks a lot for this, and for testing it end to end on the P4-ETH, including a live format. Your plan behind it is exactly the kind of thing MoonLight should do: an FSEQ player that runs an installation like your 20×20×20 cube without a computer. Other uses the card opens up:

  • Show playback without a computer: FSEQ sequences from xLights, frame by frame from the card (your next PR).
  • Recording a show: capture what arrives over DDP, Art-Net or E1.31, and replay it standalone.
  • Media for effects: video or image sequences larger than the internal flash.
  • Libraries: many MoonLive scripts, palettes and presets, swapped per show.
  • Backups and logs: device backups, and a log of an unattended night.

The platform side (platform_esp32_sdmmc.cpp) is the strongest part: pins as runtime settings with the bus width derived from the wired lines, live remounting through prepare(), and the power pin, LDO domain and NO_CD/NO_WP handled with their reasons. The changes below are about how it joins the rest of MoonLight.

1. The card as a mount point in the File Manager. If the platform's file layer sends every path under /sd to the card, the existing /api/file and /api/dir routes, upload streaming, delete and the File Manager all work unchanged, and the card shows as an sd folder next to the internal flash. That removes the six *Sd* handlers and removeRecursiveSd (main moved that walk to FilesystemModule::removeTree), the separate panel and the mod.type === "SdCardModule" check in app.js (check_code counts type names in src/ui, and that count may only fall). Format becomes controls_.addButton("format", /*confirm*/ true) on the SdCardModule card, which already gives the press-twice confirmation.

2. Flash: only in images with room. Measured on the classic ESP32 image, same base commit: +81.2 KB (FatFs 26.1, SD protocol 16.0, SD host driver 14.3, C stdio file functions 15.2, the module, routes and UI 9.6). On a 4 MB board that is a quarter of the app slot's remaining room, and the classic ESP32's SD host only works on its fixed pins (2, 4, 12, 13, 14, 15). Please build the SD card into the images that have room (P4, S31, the 16 MB S3) and leave it out of the 4 MB ones, the same way the S3-Zero image leaves out the USB host. Using FatFs directly (f_open, f_read, f_write) rather than fopen/fread saves most of the stdio part.

3. Large transfers off the render thread. HTTP requests are served from tick20ms, so a multi-GB upload freezes the LEDs, the preview and OSC for its whole length, and platform::feedWatchdog() silences the check that reports exactly that. Please drop feedWatchdog and move a large file a chunk per tick, the way the server already sends long replies. Your FSEQ player needs the same per-frame streaming (a 20×20×20 cube at 40 fps reads about 1 MB/s), so this work pays twice. Until then, a size cap on SD uploads keeps the render thread safe.

4. Card sizes past 4 GB. size_t and the uint32_t gauge are 32-bit on the ESP32, so a 32 GB card reports a total of 0 and a 48 GB card reports 16 GB. Report KiB or uint64_t from the platform.

5. One open file per download. sdReadAt reopens and seeks for every 1 KB chunk; on FAT a seek walks the cluster chain from the start, so large downloads slow down badly. One handle per stream, like fsWriteStream on the write side.

6. Smaller items.

  • sdRead, sdExists and sdWriteAtomic have no caller; please remove them.
  • 1ULL << pins.pwr with a pin above 63 is undefined behavior: check GPIO_IS_VALID_OUTPUT_GPIO first.
  • Docs: system.md says the card mounts once in setup() and needs a reboot, while it remounts live in prepare(). It also references SdCardModule.png, which isn't in the PR, and a file browser control that doesn't exist. gpio-usage.md already has a microSD cell (39-45, "unimplemented") and esp32-p4-eth.md says MoonLight doesn't mount the card: please edit those rather than adding a paragraph. Headings in positive form ("Built field by field" rather than "Not SDMMC_SLOT_CONFIG_DEFAULT()").

7. Tests. Map /sd to a folder on the computer in the desktop build, the way the desktop already maps the internal flash. Then the tests can cover mount and remount on a pin change, format, the usage gauge, and the routes under /sd (path traversal, 413 too large, 507 card full), as unit_FileManagerModule does for flash.

8. Start again from current main. 17 of the 21 files changed on main since this branched: HttpServerModule.cpp is largely rewritten, app.js's File Manager grew, and esp32-p4-eth.md now exists. A plain git rebase would stop on nearly every file, and points 1 to 3 rewrite those parts anyway, so it is easier to put the branch on today's main and bring back what carries over. With upstream pointing at MoonModules/MoonLight and origin at your fork:

git fetch upstream
git switch sdcard
git reset --hard upstream/main      # the branch now starts from today's main
git checkout ade3f71f -- src/platform/esp32/platform_esp32_sdmmc.cpp src/core/system/SdCardModule.h src/core/system/SdCardModule.cpp test/unit/core/unit_SdCardModule.cpp
# rework along points 1 to 7, then commit
git push --force-with-lease origin sdcard

The force-push keeps this PR and its discussion, and your first version stays reachable as ade3f71f to copy anything else from. If you prefer to keep the commit history, git rebase upstream/main works too: at each conflict, keep both your change and main's, git add the file and git rebase --continue.

Locally, uv run moondeck/check/check_code.py, uv run moondeck/check/check_prose.py and uv run moondeck/docs/build_docs.py --strict catch most of the above. SPI cards (esp_vfs_fat_sdspi_mount, which would bring the card to the C3 and the cheap breakout boards) can come later as a bus choice on the same module.

Looking forward to the FSEQ player!

TouchMyLight pushed a commit to TouchMyLight/MoonLight that referenced this pull request Oct 12, 2026
PR MoonModules#136 review item 6 (yours, lower priority): fsWriteStream (pull, upload-to-completion in one call) duplicated the exact same chunk-and-commit loop three times - once each for flash on ESP32, flash on desktop, and the card's own callback path - instead of being one core helper over the already-platform-abstracted fsWriteStreamOpen/Write/Close. fsReadAt (positional, reopen-per-call) had no caller left in src/ since point 5 of the MoonModules#134 review moved every real download onto FsReadStream.

Flash: esp32p4rev1-eth -704 B (2329250 -> 2328546). desktop-linux -176 B (2548104 -> 2547928).
Account: src +17/-134 lines | test +12/-5 lines | code findings 1642 -> 1630 (complex function -3, duplicated block -8, large stack frame -1) | duplicated lines 3767 -> 3738 (-29)

Core:
- platform.h: fsWriteStream is now one inline function, pulling from `src` into a 1024-byte chunk and handing each to fsWriteStreamOpen/Write/Close - the same shape encoderClaim/Release already give a helper composed over other platform primitives. fsReadAt is gone.
- platform_esp32_fs.cpp, platform_desktop.cpp: their own fsWriteStream/fsReadAt implementations are gone - fsWriteStreamOpen/Write/Close already dispatched flash vs. SD internally, so nothing platform-specific was left to write here.
- platform_esp32_sdmmc.cpp/.h: sdCardWriteStreamCb (a fourth copy of the same loop, directly against FatFs) and sdCardReadAt are gone with their last caller.

Tests:
- unit_FileManagerModule.cpp: the positional read-back test now reads through FsReadStream (skip-then-read, the same contract a real download uses) instead of fsReadAt; a stale comment naming fsReadAt as the download primitive is corrected.
- unit_HttpServerModule_sdcard.cpp: the multi-tick upload/download round-trip test reads the file back through FsReadStream instead of fsReadAt, unchanged in what it proves.

Checks (scoped to this diff's own trigger-table buckets - src/ and test/ only):
- check_specs: PASS
- check_prose: PASS - 0 new findings (one pre-existing, unrelated finding in docs/reference/hardware/gpio-usage.md, not touched by this diff)
- check_docgen: PASS - 0 errors, 0 net new warnings (matches the committed baseline exactly)
- check_code: PASS - findings fell (1642 -> 1630): the three duplicate pull-loops were themselves flagged as duplicated blocks against each other, and one of the three carried its own large-stack-frame finding
- build_desktop: PASS - zero errors/warnings in touched files
- test_desktop: PASS - 2436/2436
- run_scenario: PASS - FilesystemModule (2/2), FileManagerModule (1/1), HttpServerModule (1/1, 1 pre-existing unrelated skip)
- check_esp32_built: PASS - fresh after the real rebuild+flash
- collect_kpi: PASS on the account above; the live ESP32 tick/FPS reading reflects whatever demo config is already on the bench board, not this diff
- Real hardware (esp32p4rev1-eth, serial): rebuilt, reflashed, verified live - a flash-file upload/download round trip and an SD-file upload/download round trip both came back byte-exact through the consolidated primitive. One unrelated Guru Meditation (Cache error) self-recovered mid-session (clean reboot, rst:0xc) during an unrelated DELETE call on a path this diff never touches; retried cleanly afterward with no further incident, the same benign class already seen earlier this session.
TouchMyLight pushed a commit to TouchMyLight/MoonLight that referenced this pull request Oct 12, 2026
PR MoonModules#136 review item 9 (yours, lower priority): fsSize and the SD-side sdCardSize behind it returned long, 32-bit and signed on ESP32. A real file at or past 2 GiB-1 (well inside FAT32's own 4 GiB-1 ceiling) overflowed that into a negative number, which every caller's `< 0` check then read as "missing" rather than "large" - the exact same shape point 4 of the MoonModules#134 review already fixed for sdCardUsed/sdCardTotal, widened to uint64_t for the same reason. fsSize carries a -1 sentinel, so its fix widens to the signed int64_t rather than unsigned.

Flash: esp32p4rev1-eth +120 B (2328842 -> 2328962). desktop-linux unchanged.
Account: src +22/-22 lines | test +17/-3 lines | duplicated lines 3738 -> 3738 (+0) | code findings 1630 -> 1630 (+0)

Core:
- platform.h: fsSize widened to int64_t; FsWriteSrc and the stream primitives untouched, since a single bounded chunk never approaches the 32-bit ceiling even where the whole file does.
- platform_esp32_fs.cpp, platform_desktop.cpp: their fsSize implementations' narrowing casts (`static_cast<long>`) widened to int64_t. The desktop one was already correct on Linux (64-bit long) but not on Windows, where long stays 32-bit even in a 64-bit build - a second, previously silent instance of the same bug, fixed by the same change.
- platform_esp32_sdmmc.h/.cpp: sdCardSize widened the same way - FatFs's own FSIZE_t already holds the real value correctly; only the narrowing return cast was throwing it away.
- HttpServerModule.cpp, FilesystemModule.cpp, ControlModule.h, MoonLiveScriptFile.h: every `const long size = platform::fsSize(...)` local widened to int64_t to match, and the two Content-Length/log %ld format specifiers that printed one widened to %lld with an explicit cast - the same pairing other wide values in this codebase already use (JsonSink's %lld, the SD free-space message's %llu).

Tests:
- unit_FileManagerModule.cpp: a new case resizes a real file to 3,000,000,000 bytes (a sparse resize, not a real multi-gigabyte write) and checks fsSize reports it exactly, not negative - the case that overflowed a 32-bit signed long before this change.

Checks (scoped to this diff's own trigger-table buckets - src/ and test/ only):
- check_specs: PASS
- check_prose: PASS - 0 new findings (one pre-existing, unrelated finding in docs/reference/hardware/gpio-usage.md, not touched by this diff)
- check_docgen: PASS - 0 errors, 0 net new warnings (matches the committed baseline exactly)
- check_code: PASS - 0 net new findings
- build_desktop: PASS - zero errors/warnings in touched files
- test_desktop: PASS - 2439/2439 (1 new)
- run_scenario: PASS - FileManagerModule (1/1), ControlModule (2/2), FilesystemModule (2/2)
- check_esp32_built: PASS - fresh after the real rebuild+flash
- collect_kpi: PASS on the account above; the live ESP32 tick/FPS reading reflects whatever demo config is already on the bench board, not this diff
- Real hardware (esp32p4rev1-eth, serial): rebuilt, reflashed, verified live - a flash-file and an SD-file round trip both came back byte-exact, no crash. Reproducing the exact >2 GiB boundary on this bench setup was not practical (the throttled transfer rate measured earlier this session, ~98 KB/s, would take hours to move 2+ GB); that exact boundary is proven on desktop instead, where the fix is the same portable int64_t arithmetic on both platforms, unlike a chip-specific driver call.
ewowi pushed a commit that referenced this pull request Oct 12, 2026
Adds microSD read/write as a chip-capability service on boards with flash to spare. The card mounts as a second folder in the existing File Manager tree, with no new HTTP routes and no new client UI: /api/dir and /api/file already dispatch on path, so the card gets upload, download, mkdir and recursive delete for free. A large transfer resumes a bounded chunk at a time across many ticks rather than blocking one, so a multi-minute upload never stalls rendering.

Flash: esp32p4rev1-eth +150640 B (2177776 -> 2328416), esp32s3-zero +13152 B (2219120 -> 2232272).
Account: src +1279/-51 lines | test +596/-1 lines | code findings 1620 -> 1629 (blocking call on the render path +6, complex function +6, large stack frame -3) | duplicated lines 3733 -> 3733 (+0)

Core:
- New SdCardModule (pins, format button, KiB usage gauge) and platform::sdMount/sdUnmount/sdFormat/sdCardUsed/sdCardTotal (uint64_t, so a >4 GB card is read correctly; the shared, size_t, flash-sized Progress gauge is fed KiB instead of being widened for one caller).
- platform::fsXxx (read/write/list/exists/remove/mkdir) each gained a path-prefix check: /sd routes to the card, everything else keeps going to LittleFS/the desktop root. FileManagerModule, HttpServerModule's /api/dir and /api/file, and FilesystemModule::removeTree needed no changes at all to pick the card up.
- New FsReadStream/FsWriteStream (explicit open/step/close) replace the reopen-per-chunk fsReadAt pattern for both flash and SD downloads, fixing a FAT cluster-chain re-traversal cost on every chunk.
- New SdTransfer slot pair + HttpServerModule::driveSdTransfers, called from tick20ms next to drainPreviewSend/drainStateSend: a non-blocking read/writeSome per active slot per tick, bounded to one buffer's worth, with a 10 s stall guard (a liveness check, not a size or time cap on the transfer itself). /sd uploads and downloads hand off into this instead of the flash path's blocking-but-small synchronous handler.
- esp32/platform_esp32_sdmmc.cpp: mount/unmount/format (field-by-field slot config, explicit NO_CD/NO_WP, GPIO validation before any pin-mask shift, the power-switch and LDO-domain sequencing a P4 needs) plus the file ops, written directly against FatFs (f_open/f_read/f_write) rather than through the stdio/VFS layer, to drop stdio's share of the flash cost. A write lands in a .tmp file and is renamed into place only on success (f_rename itself won't replace an existing file, so the old one is unlinked first); every failure path - write error, a stalled peer, a dropped connection - discards the .tmp instead, so a failed upload never leaves a partial file under its real name. Verified by killing a 10 MB upload mid-transfer: the card came back to exactly its prior contents.
- A real bug from live hardware testing: driveSdUpload read into a 4 KB stack-local every tick, which a multi-minute upload stacked deeply enough to panic the board (a crash a browser's fetch() reports as the opaque "NetworkError when attempting to fetch resource.", not a clue to the real cause). Fixed by reading into the SdTransfer struct's own scratch buffer instead, the same way driveSdDownload already did it. Verified with a 40 MB upload (10x the file that first crashed it): one continuous 3m50s connection, board uptime and heap unmoved, file landed byte-exact.
- hasSdCard gates on CONFIG_SOC_SDMMC_HOST_SUPPORTED *and* a new project Kconfig bool (MM_SD_CARD, set per-board: esp32p4rev1-eth, esp32s31, esp32s3-n16r8), not on chip capability alone - classic ESP32's SD host only works on fixed pins anyway, and the point is shipping it only where there's flash to spare. Desktop's hasSdCard is unconditionally true, backed by a real <root>/sd subfolder, so its tests exercise the real mount/format/gauge logic.

UI:
- app.js: zero changes for the File Manager tree itself (the unified /api/dir and /api/file routes make the card appear automatically); the format button renders through the existing generic confirm-button machinery.
- A second bug from live use: the drop-upload path still applied FM_UPLOAD_CAP (256 KiB, sized for flash config files) to a /sd drop, where the server has no cap of its own beyond the card's free space - an 8.7 MB upload was rejected client-side before any request was sent. Fixed with fmIsSdPath(destDir), gating both the size check and the "not uploaded" message's wording.
- A progress bar for the upload, requested after the fix: fmDropUpload now goes through uploadWithProgress (the same XMLHttpRequest helper the firmware-flash UI already uses - fetch cannot report upload progress at all), driving a small floating, non-modal panel (one row per file, closes on completion) so a long /sd transfer is visible without blocking the rest of the page the way the firmware-update overlay deliberately does.
- style.css: .fm-upload-panel / .fm-upload-panel-row, bottom-left (the PiP preview pane already claims bottom-right), matching its fixed-position/border/shadow convention.

Scripts/MoonDeck:
- check_devices.py: "SD card" added to SUPPORTED_VOCAB.

Tests:
- unit_SdCardModule.cpp: pins visible/hidden by hasSdCard, mount/remount/release/format/tick1s-gauge round trip against a real backing folder.
- unit_HttpServerModule_sdcard.cpp: traversal rejection, a multi-chunk upload/download round trip, the stall timeout, 507 (card full), 413 (FAT32 ceiling), the root listing advertising "sd", DELETE removeTree on a non-empty SD folder.
- file-manager-sd-upload.test.mjs: fmIsSdPath's gating, a flash-bound file over cap skipped client-side, the same file bound for /sd sent instead, a dropped connection reported plainly rather than as a generic network error, a 507 surfacing the server's own message.

Docs/CI:
- docs/moonmodules/core/system.md: new "SD Card" section (pins, format, gauge, "appears under /sd in the File Manager"). Its card image reference is kept (check_docgen requires the tag even though the asset doesn't exist yet - a disclosed, unresolved gap, not a regression).
- docs/reference/hardware/gpio-usage.md, esp32-p4-eth.md: dropped the "(unimplemented)" / "MoonLight does not mount it" claims the feature now makes false.
- esp32/main/Kconfig.projbuild, three sdkconfig.defaults.*: MM_SD_CARD.
- mooninstaller/deviceModels.json: the Waveshare ESP32-P4-ETH's "SD card" moved planned -> supported, with its SdCardModule pin catalog entry.

Reviews (🐇 ewowi, PR #134):
1. Mount as /sd, delete the parallel API - done. The original commit's sdXxx public functions, /api/sddir + /api/sdfile routes, and the File Manager's separate SD panel are gone; one tree, one toggle.
2. Ship only where there's flash to spare; use FatFs directly, not stdio - done, via the new MM_SD_CARD Kconfig bool and the raw-FatFs rewrite above. The hoped-for flash saving didn't fully materialize (~240 B measured, not the ~15 KB estimated) because platform_esp32_fs.cpp's LittleFS side still links stdio regardless; said here rather than left to look like a wasted rewrite.
3. Large transfers blocking the render thread - done, as a real resumable per-tick transfer (SdTransfer + driveSdTransfers) rather than the smaller interim fallback of a size cap; no SD-specific ceiling beyond the card's own free space, which the 507 check already enforces. feedWatchdog() is gone: nothing blocks long enough to need it.
4. Cards over 4 GB - done, via the uint64_t sdCardUsed/Total above.
5. One open handle per download - done, via FsReadStream/FsWriteStream.
6. Smaller items (GPIO validation, doc fixes, positive doc-comment headings) - done; see platform_esp32_sdmmc.cpp's validOutputPin and its own "Drive 0 is assumed, not queried" / "Built field by field" sections.
7. Desktop test coverage - done: hasSdCard is unconditionally true on desktop, backed by a real subfolder, so unit_SdCardModule.cpp and the HTTP route tests exercise the real logic rather than a no-hardware stub.
8. Start again from current main - done; this branched from main after #135 merged, with the four reusable files (platform_esp32_sdmmc.cpp, SdCardModule.h/.cpp, unit_SdCardModule.cpp) rewritten against it rather than cherry-picked, since 17 of the original 21 files had changed on main since the old branch point.

Checks:
- check_specs: PASS - 136/136 modules
- check_prose: PASS - 1 finding in added lines, a verified Vale tool artifact (isolating one changed table row without its header mis-parses it as a long sentence; a direct Vale run against the real file shows zero findings there)
- build_docs --strict: PASS
- check_docgen: PASS - 0 errors, 4314 warnings (was 4286; the new SD files' own docs, not a regression in a touched existing file)
- test_host --python: PASS - 377 passed
- check_taglines: SKIP - no front-page .md touched
- check_code: FAIL - ratchet rose (blocking call on the render path +6, complex function +6); large stack frame improved (-3). Both remaining deltas are structural, not debt: one more per-tick resumable transfer mirroring drainPreviewSend/drainStateSend's already-accepted shape, and one more periodic gauge read mirroring filesystemUsed's. Already simplified what this change touched (driveSdTransfers split into driveSdUpload/driveSdDownload, handleWriteFile's /sd leg extracted into handleWriteFileSd) before accepting the rest per coding-standards.md's own escape valve: "a function may stay over a limit when splitting it would hide a sequence that belongs together."
- build_desktop: PASS - zero errors/warnings in touched files
- test_desktop: PASS - 2427/2427
- run_scenario: PASS - 30 scenarios, 17 passed / 13 skipped, 0 failed
- check_esp32_built: PASS - fresh after the real rebuild+flash
- build_desktop --no-jit: SKIP - MoonLive not touched
- collect_kpi: PASS - recorded above
- test_host --js: PASS - 243 passed
- check_devices: PASS - 38 devices, 0 issues
- check_firmwares: PASS - 14 variants, 0 issues
- improv_smoke_test: SKIP - provisioning path not touched
- Real hardware (esp32p4rev1-eth, over the network - serial unavailable this session): mount, File Manager discoverability, upload/download round trip, a 40 MB /sd upload completing without a crash, an interrupted upload leaving no partial file. The progress panel's wiring has its own JS tests; its on-screen appearance still wants a look in a real browser.
ewowi added a commit that referenced this pull request Oct 12, 2026
Mount a microSD card at /sd, addressing ewowi's PR #134 review

This branch was successfully deployed

1 active deployment
github-pages — 17dae99d Deployed Oct 10, 2026 by ewowi via deploy-pages #213
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants