CCleaner for your SD card, running on the ESP32 itself.
Desktop operating systems leave hidden files on every card you plug in. A Mac adds a
.DS_Store to each folder it opens, a ._ AppleDouble file next to each file it copies, and
folders such as .Spotlight-V100, .Trashes and .fseventsd. Windows leaves Thumbs.db and a
$RECYCLE.BIN. Linux leaves .Trash-1000 and lost+found. Android leaves LOST.DIR and
.trashed-* files, and a Synology NAS leaves an @eaDir folder next to every photo.
The desktop hides these files. Your handheld's file browser and your firmware's directory listings show them, and they take up space and directory entries on the card.
Removing them usually means finding a computer, mounting the card, and knowing which files are safe to delete.
ESP32 Cleaner removes them on the device, with the card still in the slot.
- Scan. Walks the whole card and lists every file and folder that a rule matches.
- Review by pack, then by item. Results open as a summary with one row per pack, which you can tick or untick as a whole. Open a pack to tick items one by one. Nothing you left unticked is deleted.
- Quick clean. Scans and removes in one step.
- Dry run. Runs the whole clean and reports real numbers, but deletes nothing.
- Your own rules. Glob patterns, added on the device or in a text file on the card.
- Scales to large cards. Results go to a manifest file on the card. RAM holds one offset and one selection bit per item, so tens of thousands of results fit in a few hundred KB of SRAM.
| Pack | Default | Matches |
|---|---|---|
| macOS junk | on | .DS_Store · ._* · .Spotlight-V100 · .Trashes · .fseventsd · .TemporaryItems · .DocumentRevisions-V100 · __MACOSX · .apdisk · .VolumeIcon.icns |
| Windows junk | on | Thumbs.db · ehthumbs.db · desktop.ini · $RECYCLE.BIN · RECYCLER · System Volume Information · ~$* |
| Linux junk | on | .Trash-* · .directory · .thumbnails · lost+found · *~ |
| Android junk | on | LOST.DIR · .trashed-* · .pending-* |
| NAS junk | on | @eaDir · #recycle · .@__thumb · .AppleDouble · .AppleDB |
| Dev caches | off | __pycache__ · *.pyc · .pytest_cache · .mypy_cache · .ruff_cache |
| Logs & temp | off, risky | *.log · *.tmp · *.bak · *.old · *.crdownload · *.part |
| Empty folders | off, risky | folders with nothing in them |
| Empty files | off, risky | zero-byte files |
| Custom | on | whatever you add |
The three risky packs ship disabled because they can match real data or folders that other
software needs. Examples are a log you wanted to keep, the empty roms/gba folder a handheld
expects, and the zero-byte .nomedia file that hides a folder from the Android gallery. You can
turn them on in Rules. When a clean includes one, the confirm screen names the pack and asks
you to press Enter a second time.
Dev caches is off because most cards have none. Python and the listed tools rebuild these caches on their next run.
The two empty packs match on content, not on a name. They apply only to items that no other
rule matched, so an empty .DS_Store still counts as macOS junk. A folder that holds only junk
is not empty until the junk is removed, so the scan after the clean finds it.
The firmware stores which packs are on, the dry-run switch and case-sensitive matching in NVS, so they survive a power cycle. Each pack is saved under its own name. A firmware update keeps your choices and starts any new pack at its default.
A rule is a glob matched against the name of a file or folder, never against its path, so /
has no special meaning.
* |
any run of characters, including none |
? |
exactly one character |
[a-z] [abc] |
one character from a set |
[!abc] |
one character not from a set |
Matching ignores case by default, because FAT cards do. You can change this in Settings.
Add rules on the device under Rules › Custom › + Add a rule, or write them on a computer into
/sdcleaner.rules at the root of the card:
# ESP32 Cleaner - custom rules, one per line.
# Format: <target> <pattern> target: f=file d=dir a=any
f *.dmp
d __pycache__
a cache
Settings › Reload rules reads an edited file without a reboot. The firmware refuses a pattern
made only of * and ?, because it would match everything on the card.
When you add or delete a rule on the device, the firmware rewrites only that rule's line. It keeps your comments, blank lines and any line it could not read. It writes the new file in full before it removes the old one, so losing power during a save does not lose your rules.
These images are captured from a real Cardputer ADV. See Screenshots below.
| Device | Status |
|---|---|
| M5Stack Cardputer ADV | supported |
| M5Stack Cardputer (original) | supported |
One firmware image runs on both. It detects the board at runtime, which is how the splash and About screens name the exact device. To add another board, see Adding a board below.
Each release has two images. Flash the
-factory.bin from a computer at offset 0x0, or copy the plain .bin to the SD card and
start it from M5Launcher.
esptool.py --chip esp32s3 write_flash 0x0 esp32-cleaner-<version>-cardputer-factory.binTo build and flash from source:
pio run -e cardputer -t upload
pio device monitorThe modules with no hardware in them run on the host. These are the glob matcher, the manifest record format, the rules-file line parser and the layout arithmetic.
pio test -e nativeTo check a real scan against a known answer, seed a card with one of everything. The script also writes near-misses that must survive and a path too long to list.
./tools/seed-test-card.sh /Volumes/MYCARDMovement keys repeat while held. Action keys do not.
| Key | Action |
|---|---|
↑ ↓ (; .) |
move up / down |
← → (, /) |
page up / page down |
space |
toggle the item, pack or setting under the cursor |
tab |
in a list, cycle the pack filter · in the rule editor, switch files / folders / any |
a / n |
select all / select none |
d |
delete the custom rule under the cursor |
enter |
confirm |
esc (`) |
back, and cancel a running scan or clean |
del |
back · erase a character in the rule editor |
On-screen hints use the names printed on the keycaps, not the characters the keys send. The
firmware draws the arrows as shapes, so they appear in any font. Each board names its own
controls through hal::Caps, so a device with buttons instead of a keyboard shows hints for
its buttons.
The images in this file are captured from the device. The firmware streams its canvas over USB, so they show the real UI with real numbers.
pio run -e cardputer-shots -t upload # same firmware plus a scripted screen tour
./tools/grab-screenshots.py # writes docs/img/*.pngThe same run checks the real scan and prints the results next to the images. It checks that the per-pack totals add up, that select all, select pack and select none agree with the manifest, that every recorded line parses back, and that the pack filters split the results with no overlap and nothing missing. The tour always cleans in dry-run mode, so regenerating the images never deletes anything.
The grabber uses only the Python standard library and encodes the PNGs itself. The device reports its own pixel byte order, so the grabber needs no install and no per-board settings.
Board specifics (display, input, SD wiring) live behind src/hal/hal.h. Nothing in
src/main.cpp depends on a device, and no screen hardcodes a pixel offset. src/ui.h derives
the whole layout from the panel size and the font at runtime.
- Write
src/hal/hal_<board>.cpp, wrapped in#if defined(BOARD_<X>), implementingbegin(),display(),update(),pollKeys(),sdConfig(),caps()andboardName(). - Map the board's input (buttons, touch, encoder or serial) onto the
hal::KeyEventflags. Name those controls inhal::Caps, so on-screen hints do not say;on a device with no keyboard. - If the board can't type characters, report
caps().textInput = false. The on-device rule editor then disappears, and users edit/sdcleaner.ruleson a computer. Everything else works unchanged. - Add an env to
platformio.iniwith-D BOARD_<X>.
App code does not change.
src/
main.cpp screens and the state machine
ui.h/.cpp theme and widgets: header, footer, menu, list, scrollbar, bars
layout.h/.cpp screen geometry, derived from the panel (host-tested)
rules.h/.cpp packs, glob rules, NVS settings, /sdcleaner.rules
rulefile.h/.cpp one line of /sdcleaner.rules (host-tested)
scanner.h/.cpp card walk, result manifest, selection, deletion
glob.h/.cpp the wildcard matcher (host-tested)
manifest.h/.cpp the result record format (host-tested)
screenshot.* canvas-over-USB dump, documentation builds only
hal/ board abstraction and one file per board
tools/
seed-test-card.sh write a card with one of everything, for testing
grab-screenshots.py collect docs/img/*.png from a running device
MIT · made by REZOR · rezor.me












