Community project

Crea Con La Scheda Es3c28p Uno Sketch Che

ESP32
Photo of Crea Con La Scheda Es3c28p Uno Sketch Che
Generated with AI

bxx6yqd6yb-commits

Published September 27, 2026

This project transforms an ESP32 development board into a file manager with a touchscreen interface and microSD card storage. Users can browse, select, and delete files directly from the display using touch controls, then access the archive wirelessly through a built-in web server.

The guide provides a complete wiring diagram connecting the ESP32 to an SD card module and display, a full parts list, the Arduino firmware sketch, and step-by-step assembly instructions. Builders will learn how to set up microSD card communication, implement touch-based file navigation, and serve files over Wi-Fi from an embedded device.

Wiring diagram

Assemble it in 4 steps

1. Prepara la microSD

Formatta una microSD in FAT32, poi inseriscila nello slot microSD della scheda ES3C28P con i contatti dorati rivolti verso l'interno dello slot. Questa è la memoria che conterrà i file.

  • Usa una microSD affidabile; se la scheda resta sulla schermata che chiede la microSD, estraila e reinseriscila a scheda scollegata.
  • Non estrarre la microSD mentre un caricamento o una cancellazione è in corso: il file system può danneggiarsi.

2. Alimenta la scheda

Collega la porta USB-C della ES3C28P a una porta USB del computer o a un alimentatore USB da 5 V. Non servono fili esterni: LCD, touch e lettore microSD sono già collegati sulla scheda.

  • Dopo l'avvio il display mostra il nome Archivio SD, lo spazio totale, occupato e libero e un grafico circolare colorato.
  • Usa solo alimentazione USB da 5 V tramite la porta USB-C; non collegare tensioni esterne ai pin della scheda.

3. Usa il touch per scegliere o eliminare un file

Tocca una riga del file sullo schermo LCD per selezionarla: la riga diventa verde. Tocca ELIMINA per rimuovere definitivamente il file selezionato oppure AGGIORNA per rileggere la cartella.

  • La cancellazione dal touch non passa dal computer e non può essere annullata.
  • Prima di toccare ELIMINA, controlla che la riga verde sia proprio il file che vuoi eliminare.

4. Apri l'archivio dal Wi-Fi

Dal telefono o computer collegati alla rete Wi-Fi ES3C28P-Archivio, usa la password archivio28. Apri poi un browser e visita http://192.168.4.1 per caricare, scaricare o eliminare i file della microSD.

  • La rete Wi-Fi è creata dalla scheda e non richiede Internet. La pagina mostra anche lo spazio libero e occupato.
  • Chi conosce la password della rete può gestire i file: cambia la password nel firmware prima di usare la scheda in un luogo condiviso.

Deploy the firmware

// Il simulatore non possiede un'implementazione coerente di SD_MMC e WebServer;
// mantiene quindi un entrypoint vuoto. Il firmware ESP32 reale qui sotto resta identico.
#if defined(__wasm__) || defined(__wasm32__)
#include <Arduino.h>
void setup() {}
void loop() {}
#else
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SD_MMC.h>
#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include "archivio_usb_excel_splash.h"

// The browser simulator has only an empty SD_MMC header facade.  These inert
// declarations are compiled only for its WebAssembly target; ESP32 hardware
// continues to use the real SD_MMC driver above.
#if defined(__wasm__) || defined(__wasm32__)
#ifndef FILE_READ
#define FILE_READ 0
#endif
#ifndef FILE_WRITE
#define FILE_WRITE 1
#endif
class File {
 public:
  File() = default;
  operator bool() const { return false; }
  bool isDirectory() const { return false; }
  const char *name() const { return ""; }
  size_t size() const { return 0; }
  File openNextFile() { return File(); }
  size_t write(const uint8_t *, size_t) { return 0; }
  void close() {}
};
class SimulatorSDMMC {
 public:
  void setPins(int, int, int, int, int, int) {}
  bool begin(const char *, bool, bool, int) { return false; }
  uint64_t totalBytes() const { return 0; }
  uint64_t usedBytes() const { return 0; }
  File open(const String &, int = FILE_READ) { return File(); }
  bool remove(const String &) { return false; }
};
static SimulatorSDMMC SD_MMC;
#endif

// LCDWIKI ES3C28P built-in hardware

// Forward declarations
String htmlEscape(const String &s);
String safeName(String name);
String humanBytes(uint64_t bytes);
uint64_t usedBytes();
void drawPie(int cx, int cy, int radius, uint64_t used, uint64_t total);
void drawScreen();
bool readTouch(uint16_t &x, uint16_t &y);
void selectRow(int row);
void handleTouch();
String fileListJson();
void serveHome();
void setupServer();

constexpr int TFT_CS = 10;
constexpr int TFT_DC = 46;
constexpr int TFT_SCK = 12;
constexpr int TFT_MOSI = 11;
constexpr int TFT_MISO = 13;
constexpr int TFT_BL = 45;
constexpr int TOUCH_SDA = 16;
constexpr int TOUCH_SCL = 15;
constexpr int TOUCH_RST = 18;
constexpr int SD_CLK = 38;
constexpr int SD_CMD = 40;
constexpr int SD_D0 = 39;
constexpr int SD_D1 = 41;
constexpr int SD_D2 = 48;
constexpr int SD_D3 = 47;

const char *AP_NAME = "ES3C28P-Archivio";
const char *AP_PASSWORD = "archivio28";  // At least 8 characters for Wi-Fi.
const uint8_t TOUCH_ADDR = 0x38;

Arduino_DataBus *bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, TFT_SCK, TFT_MOSI, TFT_MISO);
Arduino_GFX *lcd = new Arduino_ILI9341(bus, GFX_NOT_DEFINED, 1, false);
WebServer server(80);
File uploadingFile;
String selectedFile;
bool sdMounted = false;
uint32_t lastTouchMs = 0;

String htmlEscape(const String &s) {
  String r;
  for (size_t i = 0; i < s.length(); ++i) {
    char c = s[i];
    if (c == '&') r += "&amp;";
    else if (c == '<') r += "&lt;";
    else if (c == '>') r += "&gt;";
    else if (c == '\"') r += "&quot;";
    else r += c;
  }
  return r;
}

String safeName(String name) {
  name.replace("\\", "_");
  name.replace("/", "_");
  name.replace("..", "_");
#if defined(__wasm__) || defined(__wasm32__)
  // The simulator String facade lacks remove(); its inert SD path never writes.
  if (name.startsWith(".")) name = "file";
#else
  while (name.startsWith(".")) name.remove(0, 1);
#endif
  if (name.length() == 0) name = "file";
  return name;
}

String humanBytes(uint64_t bytes) {
  if (bytes >= 1024ULL * 1024ULL * 1024ULL) return String((double)bytes / 1073741824.0, 2) + " GB";
  if (bytes >= 1024ULL * 1024ULL) return String((double)bytes / 1048576.0, 1) + " MB";
  if (bytes >= 1024ULL) return String((double)bytes / 1024.0, 1) + " KB";
  return String((unsigned long)bytes) + " B";
}

uint64_t usedBytes() {
  if (!sdMounted) return 0;
  return SD_MMC.usedBytes();
}

// Torta stile Excel: bordo inferiore scuro per la profondita', faccia superiore
// leggermente sollevata e due settori ben distinguibili.
void drawPie(int cx, int cy, int radius, uint64_t used, uint64_t total) {
  float ratio = total ? (float)used / (float)total : 0.0f;
  ratio = constrain(ratio, 0.0f, 1.0f);
  const int start = -90;
  const int usedEnd = start + (int)(ratio * 360.0f);
  const int depth = 7;

  // Profondita' 3D: disegna prima gli strati inferiori, piu' scuri.
  for (int z = depth; z > 0; --z) {
    for (int a = 0; a < 360; a += 3) {
      bool occupied = a + start < usedEnd;
      uint16_t side = occupied ? 0xA280 : 0x0451; // oro scuro / blu scuro
      float r1 = (a + start) * PI / 180.0f;
      float r2 = (a + 3 + start) * PI / 180.0f;
      lcd->fillTriangle(cx, cy + z, cx + cos(r1) * radius, cy + z + sin(r1) * radius,
                        cx + cos(r2) * radius, cy + z + sin(r2) * radius, side);
    }
  }

  // Faccia superiore: oro per l'occupato e blu Excel per il libero.
  for (int a = 0; a < 360; a += 3) {
    bool occupied = a + start < usedEnd;
    uint16_t face = occupied ? 0xFD20 : 0x1D9B; // oro / blu Microsoft
    float r1 = (a + start) * PI / 180.0f;
    float r2 = (a + 3 + start) * PI / 180.0f;
    lcd->fillTriangle(cx, cy, cx + cos(r1) * radius, cy + sin(r1) * radius,
                      cx + cos(r2) * radius, cy + sin(r2) * radius, face);
  }
  // Separatore bianco tra i due valori, come nei grafici a torta Excel.
  if (ratio > 0.0f && ratio < 1.0f) {
    float split = usedEnd * PI / 180.0f;
    lcd->drawLine(cx, cy, cx + cos(split) * radius, cy + sin(split) * radius, 0xFFFF);
  }
  lcd->drawCircle(cx, cy, radius, 0xFFFF);
  lcd->fillRoundRect(cx - 19, cy - 9, 38, 18, 7, 0xFFFF);
  lcd->setTextColor(0x0000);
  lcd->setTextSize(1);
  lcd->setCursor(cx - 15, cy - 3);
  lcd->print(total ? String((int)(ratio * 100)) + "%" : "0%");
}

void drawScreen() {
  lcd->fillScreen(0xFFFF);
  lcd->fillRect(0, 0, 320, 30, 0x001F);
  lcd->setTextColor(0xFFFF);
  lcd->setTextSize(2);
  lcd->setCursor(8, 7);
  lcd->print("Archivio SD");

  if (!sdMounted) {
    lcd->setTextColor(0xF800);
    lcd->setTextSize(2);
    lcd->setCursor(20, 82);
    lcd->print("microSD assente");
    lcd->setTextColor(0x0000);
    lcd->setTextSize(1);
    lcd->setCursor(20, 112);
    lcd->print("Wi-Fi e pagina web attivi");
    lcd->setCursor(20, 128);
    lcd->print("Inserire una microSD FAT32");
    return;
  }
  uint64_t total = SD_MMC.totalBytes();
  uint64_t used = usedBytes();
  uint64_t freeBytes = total > used ? total - used : 0;
  drawPie(278, 65, 35, used, total);
  lcd->setTextColor(0x0000);
  lcd->setTextSize(1);
  lcd->setCursor(8, 38); lcd->print("Totale: " + humanBytes(total));
  lcd->setCursor(8, 50); lcd->print("Usato:  " + humanBytes(used));
  lcd->setCursor(8, 62); lcd->print("Libero: " + humanBytes(freeBytes));
  lcd->fillRect(8, 76, 9, 9, 0xFBE0);
  lcd->setCursor(21, 76); lcd->print("Occupato");
  lcd->fillRect(84, 76, 9, 9, 0x07FF);
  lcd->setCursor(97, 76); lcd->print("Libero");

  lcd->drawFastHLine(0, 105, 320, 0xC618);
  File root = SD_MMC.open("/");
  File entry = root.openNextFile();
  int row = 0;
  while (entry && row < 5) {
    String name = String(entry.name());
    if (!entry.isDirectory()) {
      int y = 110 + row * 20;
      bool picked = name == selectedFile;
      lcd->fillRect(4, y, 312, 18, picked ? 0x07E0 : 0xE71C);
      lcd->setTextColor(0x0000);
      lcd->setTextSize(1);
      lcd->setCursor(8, y + 5);
      String shown = name;
      if (shown.length() > 30) shown = shown.substring(0, 27) + "...";
      lcd->print(shown + " " + humanBytes(entry.size()));
      row++;
    }
    entry = root.openNextFile();
  }
  root.close();
  lcd->fillRect(4, 214, 152, 22, 0x001F);
  lcd->fillRect(164, 214, 152, 22, selectedFile.length() ? 0xF800 : 0xC618);
  lcd->setTextColor(0xFFFF);
  lcd->setTextSize(1);
  lcd->setCursor(21, 221); lcd->print("AGGIORNA");
  lcd->setCursor(194, 221); lcd->print("ELIMINA");
}

bool readTouch(uint16_t &x, uint16_t &y) {
  Wire.beginTransmission(TOUCH_ADDR);
  Wire.write(0x02);
  if (Wire.endTransmission(false) != 0 || Wire.requestFrom((int)TOUCH_ADDR, 5) != 5) return false;
  uint8_t points = Wire.read() & 0x0F;
  uint8_t xh = Wire.read();
  uint8_t xl = Wire.read();
  uint8_t yh = Wire.read();
  uint8_t yl = Wire.read();
  if (points == 0) return false;
  uint16_t rawX = ((xh & 0x0F) << 8) | xl;
  uint16_t rawY = ((yh & 0x0F) << 8) | yl;
  // Native touch area is 240x320; this is its normal landscape transform.
  x = min((uint16_t)319, rawY);
  y = min((uint16_t)239, (uint16_t)(239 - min((uint16_t)239, rawX)));
  return true;
}

void selectRow(int row) {
  File root = SD_MMC.open("/");
  File entry = root.openNextFile();
  int n = 0;
  while (entry) {
    if (!entry.isDirectory()) {
      if (n == row) { selectedFile = String(entry.name()); break; }
      n++;
    }
    entry = root.openNextFile();
  }
  root.close();
  drawScreen();
}

void handleTouch() {
  if (!sdMounted || millis() - lastTouchMs < 350) return;
  uint16_t x, y;
  if (!readTouch(x, y)) return;
  lastTouchMs = millis();
  if (y >= 110 && y < 210) {
    selectRow((y - 110) / 20);
  } else if (y >= 214 && x < 156) {
    drawScreen();
  } else if (y >= 214 && x >= 164 && selectedFile.length()) {
    SD_MMC.remove(selectedFile);
    selectedFile = "";
    drawScreen();
  }
}

String fileListJson() {
#if defined(__wasm__) || defined(__wasm32__)
  return "[]";
#else
  if (!sdMounted) return "[]";
  String json = "[";
  bool first = true;
  File root = SD_MMC.open("/");
  File entry = root.openNextFile();
  while (entry) {
    if (!entry.isDirectory()) {
      if (!first) json += ",";
      String name = String(entry.name());
      json += "{\"name\":\"" + htmlEscape(name) + "\",\"size\":\"" + humanBytes(entry.size()) + "\"}";
      first = false;
    }
    entry = root.openNextFile();
  }
  root.close();
  return json + "]";
#endif
}

void serveHome() {
  const char page[] PROGMEM = R"HTML(<!doctype html><html lang='it'><head><meta name='viewport' content='width=device-width,initial-scale=1'><title>Archivio ES3C28P</title><style>body{margin:0;background:#f3f5f9;color:#172033;font:15px Segoe UI,Arial,sans-serif}.bar{padding:18px 6%;background:#075cbd;color:#fff;font-size:22px;font-weight:600}.wrap{max-width:960px;margin:24px auto;padding:0 16px}.card{background:#fff;border-radius:14px;padding:20px;margin:16px 0;box-shadow:0 2px 10px #0001}button,.upload{border:0;border-radius:8px;padding:10px 15px;background:#0969da;color:#fff;font-size:14px;cursor:pointer}.danger{background:#d92d20}.meter{height:18px;border-radius:9px;background:#dce8f8;overflow:hidden}.meter div{height:100%;background:#20a464}.files{width:100%;border-collapse:collapse}.files td,.files th{padding:12px;border-bottom:1px solid #e5e7eb;text-align:left}.files tr:hover{background:#f0f7ff}.muted{color:#5d6b82}</style></head><body><div class='bar'>📁 Archivio microSD — ES3C28P</div><main class='wrap'><section class='card'><b>Spazio della microSD</b><p id='space' class='muted'>Lettura in corso…</p><div class='meter'><div id='bar'></div></div></section><section class='card'><b>Carica un file</b><p class='muted'>Il file viene salvato direttamente nella cartella principale della microSD.</p><form id='form'><input type='file' id='file' required> <button>Carica</button></form><p id='status'></p></section><section class='card'><b>File</b><table class='files'><thead><tr><th>Nome</th><th>Dimensione</th><th>Azioni</th></tr></thead><tbody id='files'></tbody></table></section></main><script>const esc=s=>s.replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));async function refresh(){let s=await fetch('/api/space').then(r=>r.json());space.textContent=`Totale ${s.total} · Occupato ${s.used} · Libero ${s.free}`;bar.style.width=s.percent+'%';let a=await fetch('/api/files').then(r=>r.json());files.innerHTML=a.map(f=>`<tr><td>📄 ${esc(f.name)}</td><td>${f.size}</td><td><a href='/download?name=${encodeURIComponent(f.name)}'><button>Scarica</button></a> <button class='danger' onclick='del(${JSON.stringify(f.name)})'>Elimina</button></td></tr>`).join('')||'<tr><td colspan=3 class=muted>Nessun file nella microSD.</td></tr>'}async function del(n){if(confirm('Eliminare definitivamente '+n+'?')){await fetch('/delete?name='+encodeURIComponent(n),{method:'POST'});refresh()}}form.onsubmit=async e=>{e.preventDefault();let f=file.files[0],d=new FormData();d.append('file',f);status.textContent='Caricamento…';let r=await fetch('/upload',{method:'POST',body:d});status.textContent=await r.text();file.value='';refresh()};refresh();</script></body></html>)HTML";
  server.send_P(200, "text/html; charset=utf-8", page);
}

void setupServer() {
  server.on("/", HTTP_GET, serveHome);
  server.on("/api/files", HTTP_GET, [](){ server.send(200, "application/json", fileListJson()); });
  server.on("/api/space", HTTP_GET, [](){
    if (!sdMounted) { server.send(200, "application/json", "{\"total\":\"microSD assente\",\"used\":\"-\",\"free\":\"-\",\"percent\":0}"); return; }
    uint64_t total = SD_MMC.totalBytes(), used = usedBytes();
    uint64_t freeBytes = total > used ? total - used : 0;
    int pct = total ? (int)((used * 100ULL) / total) : 0;
    server.send(200, "application/json", "{\"total\":\"" + humanBytes(total) + "\",\"used\":\"" + humanBytes(used) + "\",\"free\":\"" + humanBytes(freeBytes) + "\",\"percent\":" + String(pct) + "}");
  });
  server.on("/download", HTTP_GET, [](){
    if (!sdMounted) { server.send(503, "text/plain", "microSD non disponibile"); return; }
    String name = safeName(server.arg("name"));
    File f = SD_MMC.open("/" + name, FILE_READ);
    if (!f || f.isDirectory()) { server.send(404, "text/plain", "File non trovato"); return; }
    server.sendHeader("Content-Disposition", "attachment; filename=\"" + name + "\"");
#if defined(__wasm__) || defined(__wasm32__)
    server.send(501, "text/plain", "Download microSD non disponibile nel simulatore browser.");
#else
    server.streamFile(f, "application/octet-stream");
#endif
    f.close();
  });
  server.on("/delete", HTTP_POST, [](){
    if (!sdMounted) { server.send(503, "text/plain", "microSD non disponibile"); return; }
    String name = safeName(server.arg("name"));
    if (SD_MMC.remove("/" + name)) { if (selectedFile == "/" + name) selectedFile = ""; drawScreen(); server.send(200, "text/plain", "File eliminato."); }
    else server.send(404, "text/plain", "File non trovato.");
  });
#if defined(__wasm__) || defined(__wasm32__)
  server.on("/upload", HTTP_POST, [](){
    server.send(501, "text/plain", "Caricamento su microSD non disponibile nel simulatore browser.");
  });
#else
  server.on("/upload", HTTP_POST, [](){
    if (!sdMounted) { server.send(503, "text/plain", "microSD non disponibile: inserire una scheda FAT32 e riavviare."); return; }
    server.send(200, "text/plain", "File caricato nella microSD."); drawScreen();
  }, [](){
    if (!sdMounted) return;
    HTTPUpload &upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      String name = safeName(upload.filename);
      uploadingFile = SD_MMC.open("/" + name, FILE_WRITE);
    } else if (upload.status == UPLOAD_FILE_WRITE && uploadingFile) {
      uploadingFile.write(upload.buf, upload.currentSize);
    } else if (upload.status == UPLOAD_FILE_END && uploadingFile) {
      uploadingFile.close();
    } else if (upload.status == UPLOAD_FILE_ABORTED && uploadingFile) {
      uploadingFile.close();
    }
  });
#endif
  server.begin();
}

void setup() {
  Serial.begin(115200);
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);
  lcd->begin();
  // Schermata iniziale incorporata nel firmware: non dipende dalla microSD.
#if defined(__wasm__) || defined(__wasm32__)
  lcd->fillScreen(0x001B);
  lcd->setTextColor(0xFFFF);
  lcd->setTextSize(2);
  lcd->setCursor(34, 104);
  lcd->print("ARCHIVIO USB");
#else
  lcd->draw16bitRGBBitmap(0, 0, ARCHIVIO_USB_EXCEL_SPLASH_DATA,
                          ARCHIVIO_USB_EXCEL_SPLASH_WIDTH, ARCHIVIO_USB_EXCEL_SPLASH_HEIGHT);
#endif
  delay(2200);

  pinMode(TOUCH_RST, OUTPUT);
  digitalWrite(TOUCH_RST, LOW); delay(20); digitalWrite(TOUCH_RST, HIGH); delay(100);
  Wire.begin(TOUCH_SDA, TOUCH_SCL);

  WiFi.mode(WIFI_AP);
  WiFi.softAP(AP_NAME, AP_PASSWORD);
  setupServer();

  SD_MMC.setPins(SD_CLK, SD_CMD, SD_D0, SD_D1, SD_D2, SD_D3);
  sdMounted = SD_MMC.begin("/sdcard", false, false, 8);
  if (!sdMounted) Serial.println("microSD assente: pagina web comunque disponibile.");
  drawScreen();
  Serial.printf("Apri http://%s  Wi-Fi: %s\n", WiFi.softAPIP().toString().c_str(), AP_NAME);
}

void loop() {
  server.handleClient();
  handleTouch();
}
#endif  // simulatore isolato; codice precedente compilato sulla ES3C28P

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik