import React, { useEffect, useState } from "react"; /** * ESP8266 D1 mini – Web Flasher + LittleFS Uploader (Single-file React app) * * Features (matching your list): * 1) Flash your firmware (.bin) to ESP8266 D1 mini via Web Serial + esptool-js * 2) Upload a LittleFS image (.bin) OR build one in-browser from multiple animation .bin files * 3) Accepts files from 50KB to 16MB; allows multi-select for animation files * 4) Includes your Arduino sketch as the default (shown below + supports a default embedded firmware .bin via Base64) * 5) NOTE: On the public web, you cannot truly hide frontend source from view‑source/DevTools; see notes at bottom for mitigations * 6) Change Num LEDs / Color Order / Speed / Brightness in the app; values go to /config.json inside LittleFS * 7) Can erase flash; connects via Web Serial (UX similar to espressif.github.io/esptool-js) * * How the one‑click Upload works: * - If a firmware .bin is selected, it will be flashed. If not, and DEFAULT_FIRMWARE_BASE64 is provided, it will use that. * - For animations: if a LittleFS image is selected, it will flash that; otherwise, it will build a LittleFS image in‑browser * that contains /config.json and all selected animation .bin files. * * Deploy: any static host (Nginx/Apache/Vercel/Netlify). Chrome/Edge desktop required for Web Serial. */ // =============== Script loaders =============== function useScript(src) { const [ok, setOk] = useState(false); useEffect(() => { const s = document.createElement("script"); s.src = src; s.async = true; s.onload = () => setOk(true); s.onerror = () => setOk(false); document.head.appendChild(s); return () => { try { document.head.removeChild(s); } catch {} }; }, [src]); return ok; } const esptoolReady = () => typeof window !== 'undefined' && window.Esptool; const lfsReady = () => typeof window !== 'undefined' && window.LittleFSHelper; // =============== Defaults & helpers =============== const DEFAULT_LAYOUT = { flashSizeMB: 4, // D1 mini common sketchOffset: 0x000000, // app/firmware fsOffset: 0x300000, // 4M flash with 1M LittleFS (Arduino menu: 4M (1M LittleFS)) baud: 921600, }; const MIN_FILE = 50 * 1024; // 50KB const MAX_FILE = 16 * 1024 * 1024; // 16MB // Paste Base64 of your compiled firmware .bin for default one‑click upload (optional) const DEFAULT_FIRMWARE_BASE64 = ""; // <-- paste your base64 string here function b64ToU8(base64) { if (!base64) return null; const bin = atob(base64); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } export default function App() { // Load libs const esptoolOk = useScript("https://unpkg.com/esptool-js@0.5.5/bundle.js"); const lfsOk = useScript("https://unpkg.com/@deepchord/littlefs-helper@0.1.3/dist/index.umd.js"); // State const [layout, setLayout] = useState(DEFAULT_LAYOUT); const [port, setPort] = useState(null); const [busy, setBusy] = useState(false); const [log, setLog] = useState(""); const [firmwareBin, setFirmwareBin] = useState(null); // Uint8Array const [fsImageBin, setFsImageBin] = useState(null); // Uint8Array const [animFiles, setAnimFiles] = useState([]); // [{name, data:Uint8Array}] // Runtime config (goes into /config.json) const [numLeds, setNumLeds] = useState(372); const [colorOrder, setColorOrder] = useState('GRB'); // 'GRB' | 'RGB' const [speedMs, setSpeedMs] = useState(100); const [brightness, setBrightness] = useState(10); const appendLog = (s) => setLog((prev) => prev + s + "\n"); // Handlers — file selections const onSelectFirmware = async (e) => { const f = e.target.files?.[0]; if (!f) return; if (f.size < MIN_FILE || f.size > MAX_FILE) { alert("Firmware must be 50KB–16MB"); e.target.value = ""; return; } setFirmwareBin(new Uint8Array(await f.arrayBuffer())); appendLog(`Firmware selected: ${f.name} (${f.size} bytes)`); }; const onSelectFsImage = async (e) => { const f = e.target.files?.[0]; if (!f) return; if (f.size < MIN_FILE || f.size > MAX_FILE) { alert("LittleFS image must be 50KB–16MB"); e.target.value = ""; return; } setFsImageBin(new Uint8Array(await f.arrayBuffer())); appendLog(`LittleFS image selected: ${f.name} (${f.size} bytes)`); }; const onSelectAnimFiles = async (e) => { const files = Array.from(e.target.files || []); const accepted = []; for (const f of files) { if (f.size < MIN_FILE || f.size > MAX_FILE) { alert(`${f.name}: must be 50KB–16MB`); continue; } accepted.push({ name: f.name, data: new Uint8Array(await f.arrayBuffer()) }); } setAnimFiles(accepted); appendLog(`Selected ${accepted.length} animation file(s).`); }; // Web Serial const connect = async () => { if (!navigator.serial) { alert("Web Serial not supported. Use Chrome/Edge desktop."); return; } try { const p = await navigator.serial.requestPort({}); await p.open({ baudRate: layout.baud }); setPort(p); appendLog("Serial port opened."); } catch (e) { console.error(e); alert("Failed to open port: " + e.message); } }; // esptool wrapper const ensureEsptool = () => { if (!esptoolOk || !esptoolReady()) { alert("esptool-js not loaded yet."); return null; } return new window.Esptool(); }; const withEsptool = async (fn) => { if (!port) { alert("Connect to the device first."); return; } const esp = ensureEsptool(); if (!esp) return; setBusy(true); try { await esp.open(port); appendLog("Syncing with chip… (press/hold BOOT if needed)"); await esp.reset(); await esp.sync(); const chip = await esp.chip(); appendLog("Connected to: " + chip); await fn(esp); appendLog("Done."); } catch (e) { console.error(e); appendLog("Error: " + e.message); alert(e.message); } finally { try { await esp.close(); } catch {} try { await port.close(); } catch {} setPort(null); setBusy(false); appendLog("Port closed."); } }; const eraseFlash = async () => { await withEsptool(async (esp) => { appendLog("Erasing flash…"); await esp.eraseFlash(); appendLog("Erase complete."); }); }; // Build LittleFS image in browser (/config.json + selected animation files) const buildLittleFS = async () => { if (!lfsOk || !lfsReady()) { alert("LittleFS helper not loaded. Use a prebuilt FS image instead."); return null; } const helper = new window.LittleFSHelper(); // Create 1MB FS image by default (match 4M/1M preset). Adjust if needed. await helper.init({ sizeBytes: 1 * 1024 * 1024 }); const cfg = { numLeds: Number(numLeds) || 372, colorOrder: String(colorOrder || 'GRB'), frameDelay: Number(speedMs) || 100, brightness: Number(brightness) || 10, }; await helper.writeFile('/config.json', new TextEncoder().encode(JSON.stringify(cfg))); for (const f of animFiles) { await helper.writeFile('/' + f.name, f.data); } const image = await helper.exportImage(); appendLog(`Built LittleFS image: ${image.byteLength} bytes`); return new Uint8Array(image); }; const flashSegment = async (esp, offset, data, label) => { appendLog(`Flashing ${label} at 0x${offset.toString(16)} (${data.length} bytes)…`); await esp.writeFlash([[offset, data]], undefined, { flashFreq: '40m', flashMode: 'dio', flashSize: `${layout.flashSizeMB}MB`, }); appendLog(`${label} flashed.`); }; const oneClickUpload = async () => { // firmware const fw = firmwareBin || b64ToU8(DEFAULT_FIRMWARE_BASE64); if (!fw) { alert("Please select a firmware .bin or embed DEFAULT_FIRMWARE_BASE64."); return; } // fs image let fsImg = fsImageBin; if (!fsImg && animFiles.length > 0) { fsImg = await buildLittleFS(); if (!fsImg) return; } await withEsptool(async (esp) => { await flashSegment(esp, layout.sketchOffset, fw, 'Firmware'); if (fsImg) await flashSegment(esp, layout.fsOffset, fsImg, 'LittleFS image'); }); }; return (
Flashes firmware (.bin) and the LittleFS image (selected or built in‑browser) in one go.
If no firmware is selected, the app uses DEFAULT_FIRMWARE_BASE64 (if present).
Preset matches Arduino "4M (1M LittleFS)" for D1 mini.
Optional if you embed a default firmware below. (50KB–16MB)
Build FS in‑browser from multiple animation .bin files:
Selected files will be placed at LittleFS root with a generated /config.json.
Your firmware can read /config.json (see example sketch below).
{`
#include
#include
#include
#define LED_TYPE WS2812B
#define COLOR_ORDER RGB
#define DATA_PIN D2
int brightness = 10; // LED brightness (0-255)
int frameDelay = 100; // Delay between frames in ms
int numLeds = 372; // Number of LEDs in strip
bool isGRBMode = true; // true=GRB, false=RGB
const int maxBrightness = 150; // Maximum brightness cap
CRGB *leds;
File ledFile;
String *animationFiles = nullptr;
int numAnimations = 0;
int currentAnimationIndex = 0; // Current animation being played
unsigned long lastFrameTime = 0; // To track time for frame updates
void loadConfig() {
if (!LittleFS.exists("/config.json")) return;
File f = LittleFS.open("/config.json", "r");
if (!f) return;
StaticJsonDocument<256> doc;
if (deserializeJson(doc, f) == DeserializationError::Ok) {
numLeds = doc["numLeds"] | numLeds;
brightness = doc["brightness"]| brightness;
frameDelay = doc["frameDelay"]| frameDelay;
String co = doc["colorOrder"].as();
if (co.length()) isGRBMode = (co == "GRB");
}
f.close();
}
void setup() {
Serial.begin(115200);
Serial.println("Starting LED Animation Player with LittleFS...");
if (!LittleFS.begin()) { Serial.println("LittleFS initialization failed!"); return; }
loadConfig();
brightness = constrain(brightness, 0, maxBrightness);
leds = new CRGB[numLeds];
if (isGRBMode) {
FastLED.addLeds(leds, numLeds).setCorrection(TypicalLEDStrip);
} else {
FastLED.addLeds(leds, numLeds).setCorrection(TypicalLEDStrip);
}
FastLED.setBrightness(brightness);
printSettings();
scanBinFiles();
if (numAnimations > 0) {
Serial.println("Starting with first animation...");
openAnimationFile(0);
} else {
Serial.println("No .bin animation files found!");
}
}
void loop() {
if (numAnimations == 0) return;
if (millis() - lastFrameTime >= frameDelay) {
lastFrameTime = millis();
if (ledFile.available()) {
for (int i = 0; i < numLeds; i++) {
if (ledFile.available() < 3) { ledFile.seek(0); break; }
byte r,g,b;
if (isGRBMode) { g = ledFile.read(); r = ledFile.read(); b = ledFile.read(); }
else { r = ledFile.read(); g = ledFile.read(); b = ledFile.read(); }
leds[i] = CRGB(r,g,b);
}
FastLED.show();
} else {
nextAnimation();
}
}
}
void printSettings() {
Serial.println("=== Current Settings ===");
Serial.println("Brightness: " + String(brightness));
Serial.println("Frame Delay: " + String(frameDelay) + " ms");
Serial.println("Number of LEDs: " + String(numLeds));
Serial.println("Color Order: " + String(isGRBMode ? "GRB" : "RGB"));
Serial.println("Max Brightness Cap: " + String(maxBrightness));
Serial.println("========================");
}
void sortAnimationFiles();
void openAnimationFile(int index);
void nextAnimation();
void scanBinFiles() {
Dir dir = LittleFS.openDir("/");
int fileCount = 0;
while (dir.next()) {
if (!dir.isDirectory()) {
String fileName = dir.fileName();
if (fileName.endsWith(".bin")) fileCount++;
}
}
if (fileCount == 0) { Serial.println("No .bin files found in LittleFS"); return; }
animationFiles = new String[fileCount];
numAnimations = 0;
dir = LittleFS.openDir("/");
while (dir.next() && numAnimations < fileCount) {
if (!dir.isDirectory()) {
String fileName = dir.fileName();
if (fileName.endsWith(".bin")) {
animationFiles[numAnimations] = fileName;
Serial.println("Found .bin file: " + fileName);
numAnimations++;
}
}
}
sortAnimationFiles();
Serial.println("Total .bin files loaded: " + String(numAnimations));
Serial.println("Files will be played in this order:");
for (int i = 0; i < numAnimations; i++) Serial.println(String(i + 1) + ". " + animationFiles[i]);
}
void sortAnimationFiles() {
if (numAnimations <= 1) return;
for (int i = 0; i < numAnimations - 1; i++) {
for (int j = 0; j < numAnimations - i - 1; j++) {
if (animationFiles[j].compareTo(animationFiles[j + 1]) > 0) {
String temp = animationFiles[j];
animationFiles[j] = animationFiles[j + 1];
animationFiles[j + 1] = temp;
}
}
}
}
void openAnimationFile(int index) {
if (ledFile) ledFile.close();
if (index < 0 || index >= numAnimations) { Serial.println("Invalid animation index: " + String(index)); return; }
String fileName = animationFiles[index];
ledFile = LittleFS.open(fileName, "r");
if (!ledFile) { Serial.println("Error opening animation file: " + fileName); nextAnimation(); return; }
ledFile.seek(0);
Serial.println("Animation file opened: " + fileName + " (Index: " + String(index) + ")");
}
void nextAnimation() {
if (numAnimations == 0) return;
currentAnimationIndex = (currentAnimationIndex + 1) % numAnimations;
Serial.println("Switching to next animation: " + String(currentAnimationIndex + 1) + "/" + String(numAnimations));
openAnimationFile(currentAnimationIndex);
}
`}
This is your sketch with added /config.json support so the web app can control Num LEDs, Color Order, Speed, Brightness.