#include #include #include #include #include #include #include #include "config.h" #include "dither.h" // Display instance SPIClass displaySPI(SPI2_HOST); GxEPD2_730c_GDEP073E01 epd(PIN_EPD_CS, PIN_EPD_DC, PIN_EPD_RST, PIN_EPD_BUSY); // Buffers (allocated in PSRAM) static uint8_t* jpegBuf = nullptr; // raw JPEG data from server static uint8_t* rgbBuf = nullptr; // decoded RGB888 (800*480*3 = 1,152,000 bytes) static uint8_t* displayBuf = nullptr; // dithered 4bpp output (800*480/2 = 192,000 bytes) static size_t jpegLen = 0; static String photoName = ""; // name of the photo currently being displayed // JPEG decoder callback — writes decoded pixels into rgbBuf static int jpegDrawCallback(JPEGDRAW* pDraw) { for (int y = 0; y < pDraw->iHeight; y++) { int destY = pDraw->y + y; if (destY >= DISPLAY_HEIGHT) continue; for (int x = 0; x < pDraw->iWidth; x++) { int destX = pDraw->x + x; if (destX >= DISPLAY_WIDTH) continue; // JPEGDEC outputs RGB565 by default uint16_t pixel = pDraw->pPixels[y * pDraw->iWidth + x]; uint8_t r = ((pixel >> 11) & 0x1F) << 3; uint8_t g = ((pixel >> 5) & 0x3F) << 2; uint8_t b = ( pixel & 0x1F) << 3; int idx = (destY * DISPLAY_WIDTH + destX) * 3; rgbBuf[idx + 0] = r; rgbBuf[idx + 1] = g; rgbBuf[idx + 2] = b; } } return 1; // continue decoding } bool connectWiFi() { Serial.printf("Connecting to WiFi '%s'...\n", WIFI_SSID); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 60) { delay(500); Serial.print("."); attempts++; } if (WiFi.status() == WL_CONNECTED) { Serial.printf("\nConnected! IP: %s\n", WiFi.localIP().toString().c_str()); return true; } Serial.println("\nWiFi connection failed."); return false; } bool fetchPhoto() { HTTPClient http; http.begin(PHOTO_SERVER_URL); http.setTimeout(30000); const char* headerKeys[] = {"X-Photo-Name"}; http.collectHeaders(headerKeys, 1); Serial.printf("Fetching photo from %s\n", PHOTO_SERVER_URL); int httpCode = http.GET(); if (httpCode != HTTP_CODE_OK) { Serial.printf("HTTP error: %d\n", httpCode); http.end(); return false; } // Capture photo name from response header photoName = http.header("X-Photo-Name"); Serial.printf("Photo: %s\n", photoName.c_str()); jpegLen = http.getSize(); if (jpegLen <= 0 || jpegLen > 2 * 1024 * 1024) { // Unknown size — read into buffer with a max cap Serial.println("Reading response (unknown or oversized content-length)..."); WiFiClient* stream = http.getStreamPtr(); jpegLen = 0; size_t maxSize = 2 * 1024 * 1024; while (stream->connected() || stream->available()) { size_t avail = stream->available(); if (avail == 0) { delay(10); continue; } size_t toRead = min(avail, maxSize - jpegLen); if (toRead == 0) break; size_t read = stream->readBytes(jpegBuf + jpegLen, toRead); jpegLen += read; } } else { Serial.printf("Content-Length: %u bytes\n", jpegLen); WiFiClient* stream = http.getStreamPtr(); size_t received = 0; while (received < jpegLen) { size_t avail = stream->available(); if (avail == 0) { delay(10); continue; } size_t read = stream->readBytes(jpegBuf + received, min(avail, jpegLen - received)); received += read; } } http.end(); Serial.printf("Received %u bytes of JPEG data\n", jpegLen); return jpegLen > 0; } bool decodeJpeg() { Serial.println("Decoding JPEG..."); JPEGDEC jpeg; if (!jpeg.openRAM(jpegBuf, jpegLen, jpegDrawCallback)) { Serial.println("Failed to open JPEG"); return false; } Serial.printf("JPEG: %dx%d\n", jpeg.getWidth(), jpeg.getHeight()); // Use 1/1 scale if the server already resized to 800x480. // If the image is larger, we could use 1/2 or 1/4 scale, but // the server should handle resizing. jpeg.setPixelType(RGB565_BIG_ENDIAN); if (!jpeg.decode(0, 0, 0)) { Serial.println("JPEG decode failed"); return false; } Serial.println("JPEG decoded successfully"); return true; } void displayImage() { Serial.println("Dithering to 6-color palette..."); memset(displayBuf, 0, DISPLAY_WIDTH * DISPLAY_HEIGHT / 2); ditherFloydSteinberg(rgbBuf, displayBuf, DISPLAY_WIDTH, DISPLAY_HEIGHT); Serial.println("Dithering complete"); Serial.println("Writing to e-paper display..."); epd.init(115200, true, 50, false); // Write raw 4bpp native-encoded pixel data directly to the panel. // invert=true tells GxEPD2 to skip its color remapping since our // dither output already uses native panel color indices. epd.writeNative(displayBuf, nullptr, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, true, false, false); epd.refresh(false); // full refresh Serial.println("Waiting for display refresh (~15s)..."); epd.hibernate(); Serial.println("Display updated!"); } void sendHeartbeat() { HTTPClient http; String url = PHOTO_SERVER_URL; // Replace /photo with /heartbeat url = url.substring(0, url.lastIndexOf('/')) + "/heartbeat"; http.begin(url); http.addHeader("Content-Type", "application/json"); String body = "{\"photo\":\"" + photoName + "\",\"free_heap\":" + String(ESP.getFreeHeap()) + "}"; int code = http.POST(body); Serial.printf("Heartbeat sent (%d)\n", code); http.end(); } void enterDeepSleep() { Serial.printf("Going to deep sleep for %d seconds...\n", SLEEP_DURATION_SEC); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); esp_sleep_enable_timer_wakeup((uint64_t)SLEEP_DURATION_SEC * 1000000ULL); esp_deep_sleep_start(); } void setup() { Serial.begin(115200); delay(1000); Serial.println("\n=== E-Ink Photo Frame ==="); // Check PSRAM if (!psramFound()) { Serial.println("ERROR: PSRAM not found! Cannot continue."); return; } Serial.printf("PSRAM: %u bytes free\n", ESP.getFreePsram()); // Allocate buffers in PSRAM jpegBuf = (uint8_t*)ps_malloc(2 * 1024 * 1024); // 2MB for JPEG rgbBuf = (uint8_t*)ps_malloc(DISPLAY_WIDTH * DISPLAY_HEIGHT * 3); // ~1.1MB displayBuf = (uint8_t*)ps_malloc(DISPLAY_WIDTH * DISPLAY_HEIGHT / 2); // 192KB if (!jpegBuf || !rgbBuf || !displayBuf) { Serial.println("ERROR: Failed to allocate PSRAM buffers!"); return; } // Initialize SPI for the display displaySPI.begin(PIN_SPI_SCLK, -1, PIN_SPI_MOSI, PIN_EPD_CS); epd.selectSPI(displaySPI, SPISettings(20000000, MSBFIRST, SPI_MODE0)); if (!connectWiFi()) { Serial.println("No WiFi — going back to sleep"); enterDeepSleep(); return; } if (!fetchPhoto()) { Serial.println("Failed to fetch photo — going back to sleep"); enterDeepSleep(); return; } if (!decodeJpeg()) { Serial.println("Failed to decode JPEG — going back to sleep"); enterDeepSleep(); return; } displayImage(); sendHeartbeat(); enterDeepSleep(); } void loop() { // Never reached — we deep sleep from setup() }