#include "Web_Server.h" #include #include #include "SD_Card.h" #include "Audio_Player.h" // ── Inter-task flags (set by web handler, read by main loop) ────────────────── volatile int g_webDisplayRequest = -1; volatile bool g_webDisplayDirty = false; static WebServer server(80); static File uploadFile; static bool uploadOK = false; static String uploadSavedAs = ""; // ── Helpers ─────────────────────────────────────────────────────────────────── static bool allowedPhoto(const String& name) { String l = name; l.toLowerCase(); return l.endsWith(".png") || l.endsWith(".jpg") || l.endsWith(".jpeg"); } static bool allowedAudio(const String& name) { String l = name; l.toLowerCase(); return l.endsWith(".mp3") || l.endsWith(".wav"); } // Build a compact JSON representation of all memories static String memoriesJson() { String j = "["; for (int i = 0; i < g_memoryCount; i++) { if (i > 0) j += ","; j += "{\"id\":"; j += g_memories[i].id; j += ",\"name\":\""; j += g_memories[i].name; j += "\",\"photo\":\""; j += g_memories[i].photo; j += "\",\"audio\":\""; j += g_memories[i].audio; j += "\"}"; } j += "]"; return j; } // ── Single-page app HTML ────────────────────────────────────────────────────── // Served from PROGMEM to save RAM static const char APP_HTML[] PROGMEM = R"HTML( Memory Frame

Add Memory

Upload a photo and a soundbite together

Step 1 of 3 — Choose a Photo
📷

Tap to choose a photo
from your camera roll

preview
Uploading photo...
Step 2 of 3 — Choose a Soundbite
🎤

Tap to choose a sound clip
.mp3 or .wav file

Uploading audio...
Step 3 of 3 — Name This Memory

Settings

Configure your frame

Now showing
-
Volume
Speaker volume for audio playback
12 / 21
)HTML"; // ── Upload handlers ─────────────────────────────────────────────────────────── static String pendingUploadPath = ""; static void handleUploadFile(const String& subdir, bool (*validator)(const String&)) { HTTPUpload& up = server.upload(); if (up.status == UPLOAD_FILE_START) { String fname = up.filename; uploadOK = false; uploadSavedAs = ""; if (!validator(fname)) { printf("Upload: rejected %s\r\n", fname.c_str()); return; } pendingUploadPath = "/" + subdir + "/" + fname; uploadFile = SD_MMC.open(pendingUploadPath, "w"); uploadOK = (bool)uploadFile; if (uploadOK) uploadSavedAs = fname; printf("Upload: writing to %s\r\n", pendingUploadPath.c_str()); } else if (up.status == UPLOAD_FILE_WRITE) { if (uploadFile) uploadFile.write(up.buf, up.currentSize); } else if (up.status == UPLOAD_FILE_END) { if (uploadFile) { uploadFile.close(); printf("Upload: done (%u bytes)\r\n", up.totalSize); } else { uploadOK = false; } } } // ── Route setup ─────────────────────────────────────────────────────────────── void initWebServer() { WiFi.softAP("love", "love1234"); printf("WiFi AP: SSID='love' PW='love1234' IP=%s\r\n", WiFi.softAPIP().toString().c_str()); // Main SPA server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", APP_HTML); }); // GET /api/memories → JSON array server.on("/api/memories", HTTP_GET, []() { server.send(200, "application/json", memoriesJson()); }); // GET /api/settings → {volume, currentIndex, currentName} server.on("/api/settings", HTTP_GET, []() { String name = ""; if (g_currentIndex >= 0 && g_currentIndex < g_memoryCount) name = String(g_memories[g_currentIndex].name); String j = "{\"volume\":"; j += g_volume; j += ",\"currentIndex\":"; j += g_currentIndex; j += ",\"currentName\":\""; j += name; j += "\"}"; server.send(200, "application/json", j); }); // POST /api/settings body: {volume:N} server.on("/api/settings", HTTP_POST, []() { String body = server.arg("plain"); // parse volume from simple JSON: {"volume":12} int vi = body.indexOf("\"volume\":"); if (vi >= 0) { int v = body.substring(vi + 9).toInt(); if (v < 0) v = 0; if (v > 21) v = 21; setPlayerVolume((uint8_t)v); Config_Save(); } server.send(200, "application/json", "{\"ok\":true}"); }); // POST /api/upload/photo — multipart upload server.on("/api/upload/photo", HTTP_POST, []() { if (uploadOK) server.send(200, "application/json", "{\"ok\":true,\"filename\":\"" + uploadSavedAs + "\"}"); else server.send(400, "application/json", "{\"ok\":false,\"error\":\"upload failed\"}"); }, []() { handleUploadFile("photos", allowedPhoto); } ); // POST /api/upload/audio — multipart upload server.on("/api/upload/audio", HTTP_POST, []() { if (uploadOK) server.send(200, "application/json", "{\"ok\":true,\"filename\":\"" + uploadSavedAs + "\"}"); else server.send(400, "application/json", "{\"ok\":false,\"error\":\"upload failed\"}"); }, []() { handleUploadFile("audio", allowedAudio); } ); // POST /api/memory body: {name, photo, audio} server.on("/api/memory", HTTP_POST, []() { String body = server.arg("plain"); // Minimal parser — extract name, photo, audio auto extract = [&](const char* key) -> String { String k = String("\"") + key + "\":\""; int p = body.indexOf(k); if (p < 0) return ""; int s = p + k.length(); int e = body.indexOf('"', s); if (e < 0) return ""; return body.substring(s, e); }; String name = extract("name"); String photo = extract("photo"); String audio = extract("audio"); if (name.isEmpty() || photo.isEmpty()) { server.send(400, "application/json", "{\"ok\":false,\"error\":\"missing name or photo\"}"); return; } int id = Config_Add_Memory(name.c_str(), photo.c_str(), audio.c_str()); if (id > 0) server.send(200, "application/json", "{\"ok\":true,\"id\":" + String(id) + "}"); else server.send(500, "application/json", "{\"ok\":false,\"error\":\"too many memories\"}"); }); // DELETE /api/memory?id=N server.on("/api/memory", HTTP_DELETE, []() { if (!server.hasArg("id")) { server.send(400, "application/json", "{\"ok\":false}"); return; } int id = server.arg("id").toInt(); bool ok = Config_Delete_Memory(id); server.send(200, "application/json", ok ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"not found\"}"); }); // POST /api/display?id=N — show a memory on the frame NOW server.on("/api/display", HTTP_POST, []() { if (!server.hasArg("id")) { server.send(400, "application/json", "{\"ok\":false}"); return; } int id = server.arg("id").toInt(); for (int i = 0; i < g_memoryCount; i++) { if (g_memories[i].id == id) { g_currentIndex = i; g_webDisplayRequest = i; g_webDisplayDirty = true; Config_Save(); server.send(200, "application/json", "{\"ok\":true}"); return; } } server.send(404, "application/json", "{\"ok\":false,\"error\":\"memory not found\"}"); }); // GET /photo/{filename} — serve a photo thumbnail from SD server.on("/photo", HTTP_GET, []() { String name = server.pathArg(0); if (name.isEmpty() && server.hasArg("f")) name = server.arg("f"); // Fallback: extract from URL String uri = server.uri(); if (name.isEmpty()) { int slash = uri.lastIndexOf('/'); if (slash >= 0) name = uri.substring(slash + 1); } if (name.isEmpty()) { server.send(404, "text/plain", "Not found"); return; } String path = "/photos/" + name; File f = SD_MMC.open(path, "r"); if (!f) { server.send(404, "text/plain", "Not found"); return; } server.streamFile(f, "image/png"); f.close(); }); server.onNotFound([]() { // Also handle /photo/xxx.png via URI parsing String uri = server.uri(); if (uri.startsWith("/photo/")) { String name = uri.substring(7); String path = "/photos/" + name; File f = SD_MMC.open(path, "r"); if (f) { String ct = name.endsWith(".png") ? "image/png" : "image/jpeg"; server.streamFile(f, ct); f.close(); return; } } server.send(404, "text/plain", "Not found"); }); server.begin(); printf("Web server started\r\n"); } void handleWebServer() { server.handleClient(); }