diff --git a/usermods/sd_card/readme.md b/usermods/sd_card/readme.md
new file mode 100644
index 00000000..299b68eb
--- /dev/null
+++ b/usermods/sd_card/readme.md
@@ -0,0 +1,34 @@
+# SD-card mod
+
+## Build
+- modify `platformio.ini` and add to the `build_flags` of your configuration the following
+- choose the way your SD is connected
+ 1. via `-D WLED_USE_SD_MMC` when connected via MMC
+ 2. via `-D WLED_USE_SD_SPI` when connected via SPI (use usermod page to setup SPI pins)
+
+### Test
+- enable `-D SD_PRINT_HOME_DIR` and `-D WLED_DEBUG`
+- this will print all files in `/` on boot via serial
+
+## Configuration
+### MMC
+- The MMC port / pins needs no configuration as they are specified by Espressif
+### SPI
+- The SPI port / pins can be modified via the WLED web-UI: `Config → Usermod → SD Card`
+ | option | effect | default |
+ | ----------------- | ------------------------------------------------------------------------------------------------ | ------- |
+ | `pinSourceSelect` | GPIO that is connected to SD's `SS`(source select) / `CS`(chip select) | 16 |
+ | `pinSourceClock` | GPIO that is connected to SD's `SCLK` (source clock) / `CLK`(clock) | 14 |
+ | `pinPoci` | GPIO that is connected to SD's `POCI`☨ (Peripheral-Out-Ctrl-In) / `MISO` (deprecated) | 36 |
+ | `pinPico` | GPIO that is connected to SD's `PICO`☨ (Peripheral-In-Ctrl-Out) / `MOSI` (deprecated) | 14 |
+ | `sdEnable` | Enable to read data from the SD-card | true |
+
+ ☨Following new naming convention of [OSHWA](https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/)
+
+## Usage in other mods
+- creates a macro `SD_ADAPTER` which is either mapped to `SD` or `SD_MMC` (see `SD_Test.ino` how to use SD / SD_MMC functions)
+
+- checks if the specified file is available on the SD card
+ ```cpp
+ bool file_onSD(const char *filepath) {...}
+ ```
\ No newline at end of file
diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/usermod_sd_card.h
new file mode 100644
index 00000000..5dac7915
--- /dev/null
+++ b/usermods/sd_card/usermod_sd_card.h
@@ -0,0 +1,243 @@
+#pragma once
+
+#include "wled.h"
+
+// SD connected via MMC / SPI
+#if defined(WLED_USE_SD_MMC)
+ #define USED_STORAGE_FILESYSTEMS "SD MMC, LittleFS"
+ #define SD_ADAPTER SD_MMC
+ #include "SD_MMC.h"
+// SD connected via SPI (adjustable via usermod config)
+#elif defined(WLED_USE_SD_SPI)
+ #define SD_ADAPTER SD
+ #define USED_STORAGE_FILESYSTEMS "SD SPI, LittleFS"
+ #include "SD.h"
+ #include "SPI.h"
+#endif
+
+#ifdef WLED_USE_SD_MMC
+#elif defined(WLED_USE_SD_SPI)
+ SPIClass spiPort = SPIClass(VSPI);
+#endif
+
+void listDir( const char * dirname, uint8_t levels);
+
+class UsermodSdCard : public Usermod {
+ private:
+ bool sdInitDone = false;
+
+ #ifdef WLED_USE_SD_SPI
+ int8_t configPinSourceSelect = 16;
+ int8_t configPinSourceClock = 14;
+ int8_t configPinPoci = 36; // confusing names? Then have a look :)
+ int8_t configPinPico = 15; // https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/
+
+ //acquired and initialize the SPI port
+ void init_SD_SPI()
+ {
+ if(!configSdEnabled) return;
+ if(sdInitDone) return;
+
+ PinManagerPinType pins[5] = {
+ { configPinSourceSelect, true },
+ { configPinSourceClock, true },
+ { configPinPoci, false },
+ { configPinPico, true }
+ };
+
+ if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) {
+ DEBUG_PRINTF("[%s] SD (SPI) pin allocation failed!\n", _name);
+ sdInitDone = false;
+ return;
+ }
+
+ bool returnOfInitSD = false;
+
+ #if defined(WLED_USE_SD_SPI)
+ spiPort.begin(configPinSourceClock, configPinPoci, configPinPico, configPinSourceSelect);
+ returnOfInitSD = SD_ADAPTER.begin(configPinSourceSelect, spiPort);
+ #endif
+
+ if(!returnOfInitSD) {
+ DEBUG_PRINTF("[%s] SPI begin failed!\n", _name);
+ sdInitDone = false;
+ return;
+ }
+
+ sdInitDone = true;
+ }
+
+ //deinitialize the acquired SPI port
+ void deinit_SD_SPI()
+ {
+ if(!sdInitDone) return;
+
+ SD_ADAPTER.end();
+
+ DEBUG_PRINTF("[%s] deallocate pins!\n", _name);
+ pinManager.deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard);
+ pinManager.deallocatePin(configPinSourceClock, PinOwner::UM_SdCard);
+ pinManager.deallocatePin(configPinPoci, PinOwner::UM_SdCard);
+ pinManager.deallocatePin(configPinPico, PinOwner::UM_SdCard);
+
+ sdInitDone = false;
+ }
+
+ // some SPI pin was changed, while SPI was initialized, reinit to new port
+ void reinit_SD_SPI()
+ {
+ deinit_SD_SPI();
+ init_SD_SPI();
+ }
+ #endif
+
+ #ifdef WLED_USE_SD_MMC
+ void init_SD_MMC() {
+ if(sdInitDone) return;
+ bool returnOfInitSD = false;
+ returnOfInitSD = SD_ADAPTER.begin();
+ DEBUG_PRINTF("[%s] MMC begin\n", _name);
+
+ if(!returnOfInitSD) {
+ DEBUG_PRINTF("[%s] MMC begin failed!\n", _name);
+ sdInitDone = false;
+ return;
+ }
+
+ sdInitDone = true;
+ }
+ #endif
+
+ public:
+ static bool configSdEnabled;
+ static const char _name[];
+
+ void setup() {
+ DEBUG_PRINTF("[%s] usermod loaded \n", _name);
+ #if defined(WLED_USE_SD_SPI)
+ init_SD_SPI();
+ #elif defined(WLED_USE_SD_MMC)
+ init_SD_MMC();
+ #endif
+
+ #if defined(SD_ADAPTER) && defined(SD_PRINT_HOME_DIR)
+ listDir("/", 0);
+ #endif
+ }
+
+ void loop(){
+
+ }
+
+ uint16_t getId()
+ {
+ return USERMOD_ID_SD_CARD;
+ }
+
+ void addToConfig(JsonObject& root)
+ {
+ #ifdef WLED_USE_SD_SPI
+ JsonObject top = root.createNestedObject(FPSTR(_name));
+ top["pinSourceSelect"] = configPinSourceSelect;
+ top["pinSourceClock"] = configPinSourceClock;
+ top["pinPoci"] = configPinPoci;
+ top["pinPico"] = configPinPico;
+ top["sdEnabled"] = configSdEnabled;
+ #endif
+ }
+
+ bool readFromConfig(JsonObject &root)
+ {
+ #ifdef WLED_USE_SD_SPI
+ JsonObject top = root[FPSTR(_name)];
+ if (top.isNull()) {
+ DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name);
+ return false;
+ }
+
+ uint8_t oldPinSourceSelect = configPinSourceSelect;
+ uint8_t oldPinSourceClock = configPinSourceClock;
+ uint8_t oldPinPoci = configPinPoci;
+ uint8_t oldPinPico = configPinPico;
+ bool oldSdEnabled = configSdEnabled;
+
+ getJsonValue(top["pinSourceSelect"], configPinSourceSelect);
+ getJsonValue(top["pinSourceClock"], configPinSourceClock);
+ getJsonValue(top["pinPoci"], configPinPoci);
+ getJsonValue(top["pinPico"], configPinPico);
+ getJsonValue(top["sdEnabled"], configSdEnabled);
+
+ if(configSdEnabled != oldSdEnabled) {
+ configSdEnabled ? init_SD_SPI() : deinit_SD_SPI();
+ DEBUG_PRINTF("[%s] SD card %s\n", _name, configSdEnabled ? "enabled" : "disabled");
+ }
+
+ if( configSdEnabled && (
+ oldPinSourceSelect != configPinSourceSelect ||
+ oldPinSourceClock != configPinSourceClock ||
+ oldPinPoci != configPinPoci ||
+ oldPinPico != configPinPico)
+ )
+ {
+ DEBUG_PRINTF("[%s] Init SD card based of config\n", _name);
+ DEBUG_PRINTF("[%s] Config changes \n - SS: %d -> %d\n - MI: %d -> %d\n - MO: %d -> %d\n - En: %d -> %d\n", _name, oldPinSourceSelect, configPinSourceSelect, oldPinSourceClock, configPinSourceClock, oldPinPoci, configPinPoci, oldPinPico, configPinPico);
+ reinit_SD_SPI();
+ }
+ #endif
+
+ return true;
+ }
+};
+
+const char UsermodSdCard::_name[] PROGMEM = "SD Card";
+bool UsermodSdCard::configSdEnabled = true;
+
+#ifdef SD_ADAPTER
+//checks if the file is available on SD card
+bool file_onSD(const char *filepath)
+{
+ #ifdef WLED_USE_SD_SPI
+ if(!UsermodSdCard::configSdEnabled) return false;
+ #endif
+
+ uint8_t cardType = SD_ADAPTER.cardType();
+ if(cardType == CARD_NONE) {
+ DEBUG_PRINTF("[%s] not attached / cardType none\n", UsermodSdCard::_name);
+ return false; // no SD card attached
+ }
+ if(cardType == CARD_MMC || cardType == CARD_SD || cardType == CARD_SDHC)
+ {
+ return SD_ADAPTER.exists(filepath);
+ }
+
+ return false; // unknown card type
+}
+
+void listDir( const char * dirname, uint8_t levels){
+ DEBUG_PRINTF("Listing directory: %s\n", dirname);
+
+ File root = SD_ADAPTER.open(dirname);
+ if(!root){
+ DEBUG_PRINTF("Failed to open directory\n");
+ return;
+ }
+ if(!root.isDirectory()){
+ DEBUG_PRINTF("Not a directory\n");
+ return;
+ }
+
+ File file = root.openNextFile();
+ while(file){
+ if(file.isDirectory()){
+ DEBUG_PRINTF(" DIR : %s\n",file.name());
+ if(levels){
+ listDir(file.name(), levels -1);
+ }
+ } else {
+ DEBUG_PRINTF(" FILE: %s SIZE: %d\n",file.name(), file.size());
+ }
+ file = root.openNextFile();
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/wled00/const.h b/wled00/const.h
index c62cd21b..f092fe63 100644
--- a/wled00/const.h
+++ b/wled00/const.h
@@ -96,6 +96,7 @@
#define USERMOD_ID_ANALOG_CLOCK 33 //Usermod "Analog_Clock.h"
#define USERMOD_ID_PING_PONG_CLOCK 34 //Usermod "usermod_v2_ping_pong_clock.h"
#define USERMOD_ID_ADS1115 35 //Usermod "usermod_ads1115.h"
+#define USERMOD_ID_SD_CARD 37 //Usermod "usermod_sd_card.h"
//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h
index 4a0c6889..db211961 100644
--- a/wled00/pin_manager.h
+++ b/wled00/pin_manager.h
@@ -57,7 +57,8 @@ enum struct PinOwner : uint8_t {
UM_QuinLEDAnPenta = USERMOD_ID_QUINLED_AN_PENTA, // 0x17 // Usermod "quinled-an-penta.h"
UM_BME280 = USERMOD_ID_BME280, // 0x18 // Usermod "usermod_bme280.h -- Uses "standard" HW_I2C pins
UM_BH1750 = USERMOD_ID_BH1750, // 0x19 // Usermod "usermod_bme280.h -- Uses "standard" HW_I2C pins
- UM_Audioreactive = USERMOD_ID_AUDIOREACTIVE // 0x1E // Usermod "audio_reactive.h"
+ UM_Audioreactive = USERMOD_ID_AUDIOREACTIVE, // 0x1E // Usermod "audio_reactive.h"
+ UM_SdCard = USERMOD_ID_SD_CARD // 0x24 // Usermod "usermod_sd_card.h"
};
static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected");
diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp
index f161bef7..e40ccf75 100644
--- a/wled00/usermods_list.cpp
+++ b/wled00/usermods_list.cpp
@@ -160,6 +160,18 @@
#include "../usermods/ADS1115_v2/usermod_ads1115.h"
#endif
+#if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI)
+// This include of SD.h and SD_MMC.h must happen here, else they won't be
+// resolved correctly (when included in mod's header only)
+ #ifdef WLED_USE_SD_MMC
+ #include "SD_MMC.h"
+ #elif defined(WLED_USE_SD_SPI)
+ #include "SD.h"
+ #include "SPI.h"
+ #endif
+ #include "../usermods/sd_card/usermod_sd_card.h"
+#endif
+
void registerUsermods()
{
/*
@@ -307,4 +319,8 @@ void registerUsermods()
#ifdef USERMOD_ADS1115
usermods.add(new ADS1115Usermod());
#endif
+
+ #ifdef SD_ADAPTER
+ usermods.add(new UsermodSdCard());
+ #endif
}