diff --git a/ClientMachineCode/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino b/ClientMachineCode/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino index 46609ee..ef24db0 100644 --- a/ClientMachineCode/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino +++ b/ClientMachineCode/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino @@ -1,172 +1,523 @@ #include -#include "EPD.h" +#include +#include #include "DEV_Config.h" +#include "EPD.h" #include "GUI_Paint.h" +#include +#include -// — Wi-Fi & image endpoint ——————————————————————————————— -const char* ssid = "x"; +// WiFi credentials +const char* ssid = "x"; const char* password = "x"; -const char* HOST = "192.168.x.x"; -const uint16_t PORT = 5000; -const char* PATH = "/Home/default.bmp"; -// — Framebuffers for black & red —————————————————————————————— -UBYTE *BlackImage = nullptr; -UBYTE *RYImage = nullptr; +// API endpoints +const char* connectionInformation = "http://x:5000/home/configuration"; -// — Sleep config —————————————————————————————————————————————— -#define SLEEP_MINUTES 0.5 -#define SLEEP_TIME_US (SLEEP_MINUTES * 60ULL * 1000000ULL) +// These will be updated from the connection information +String imageUrl = ""; // Will be populated from JSON +uint64_t sleepDuration = 30e6; // Default 30 seconds in microseconds + +// Display dimensions - use the constants from Waveshare library +#define EPD_WIDTH EPD_7IN5B_V2_WIDTH +#define EPD_HEIGHT EPD_7IN5B_V2_HEIGHT + +// =========== IMAGE TUNING PARAMETERS =========== +// Basic color thresholds +#define BLACK_TEXT_THRESHOLD 190 // rgb(51,51,51) should be detected as black + +// Dithering settings - NEW +#define ENABLE_DITHERING true // Set to false to disable dithering +#define DITHER_STRENGTH 8 // Lower values = stronger dithering (8-32) + +// Contrast settings - NEW +#define ENHANCE_CONTRAST true // Set to false to disable contrast enhancement +#define CONTRAST_LEVEL 30 // Contrast adjustment level (0-100) +// =============================================== + +// Framebuffers for black and red layers +UBYTE *BlackImage, *RYImage; + +// Error buffers for dithering +int16_t *errorR = NULL; +int16_t *errorG = NULL; +int16_t *errorB = NULL; + +// Create an instance of the JPEG decoder +JPEGDEC jpeg; + +// Apply contrast adjustment to RGB values +void adjustContrast(uint8_t *r, uint8_t *g, uint8_t *b) { + if (!ENHANCE_CONTRAST) return; + + float contrast = (CONTRAST_LEVEL / 100.0) + 1.0; // Convert to decimal & shift range: [0..2] + float intercept = 128 * (1 - contrast); + + *r = constrain((*r * contrast) + intercept, 0, 255); + *g = constrain((*g * contrast) + intercept, 0, 255); + *b = constrain((*b * contrast) + intercept, 0, 255); +} + +// JPEG draw callback function for JPEGDEC +int jpegDrawCallback(JPEGDRAW *pDraw) { + // Get MCU block information + uint16_t *pPixels = pDraw->pPixels; + int x = pDraw->x; + int y = pDraw->y; + int width = pDraw->iWidth; + int height = pDraw->iHeight; + + // Initialize error buffers for dithering if needed + if (ENABLE_DITHERING && errorR == NULL) { + errorR = (int16_t*)malloc(EPD_WIDTH * sizeof(int16_t)); + errorG = (int16_t*)malloc(EPD_WIDTH * sizeof(int16_t)); + errorB = (int16_t*)malloc(EPD_WIDTH * sizeof(int16_t)); + + if (errorR && errorG && errorB) { + memset(errorR, 0, EPD_WIDTH * sizeof(int16_t)); + memset(errorG, 0, EPD_WIDTH * sizeof(int16_t)); + memset(errorB, 0, EPD_WIDTH * sizeof(int16_t)); + } else { + Serial.println("Failed to allocate dithering buffers"); + if (errorR) free(errorR); + if (errorG) free(errorG); + if (errorB) free(errorB); + errorR = errorG = errorB = NULL; + } + } + + // Process each row in this MCU block + for (int iy = 0; iy < height; iy++) { + // Reset error buffers for each row + if (ENABLE_DITHERING && errorR != NULL) { + memset(errorR, 0, EPD_WIDTH * sizeof(int16_t)); + memset(errorG, 0, EPD_WIDTH * sizeof(int16_t)); + memset(errorB, 0, EPD_WIDTH * sizeof(int16_t)); + } + + // Process each pixel in the row + for (int ix = 0; ix < width; ix++) { + int pos_x = x + ix; + int pos_y = y + iy; + + // Skip if outside display bounds + if (pos_x >= EPD_WIDTH || pos_y >= EPD_HEIGHT) continue; + + // Get the 16-bit pixel value (RGB565) + uint16_t pixel = pPixels[iy * width + ix]; + + // Extract RGB components (565 format) and convert to 0-255 range + uint8_t r = ((pixel >> 11) & 0x1F) << 3; + uint8_t g = ((pixel >> 5) & 0x3F) << 2; + uint8_t b = (pixel & 0x1F) << 3; + + // Apply contrast adjustment if enabled + if (ENHANCE_CONTRAST) { + adjustContrast(&r, &g, &b); + } + + // Apply dithering errors if enabled + if (ENABLE_DITHERING && errorR != NULL) { + r = constrain(r + (errorR[pos_x] / DITHER_STRENGTH), 0, 255); + g = constrain(g + (errorG[pos_x] / DITHER_STRENGTH), 0, 255); + b = constrain(b + (errorB[pos_x] / DITHER_STRENGTH), 0, 255); + } + + // Calculate grayscale value + float gray = (r * 0.299 + g * 0.587 + b * 0.114); + + // ===== IMPROVED COLOR CLASSIFICATION LOGIC ===== + // Variable for final color (0=black, 1=white, 2=red) + int finalColor; + + // Check for "redness" - how much stronger red is than other components + float redness = r / (float)(g + b + 1); // Add 1 to avoid division by zero + + // Check if this is likely a red pixel based on redness + if (r > 100 && redness > 1.5) { + finalColor = 2; // Red + } + // If not red, determine if it's black or white based on grayscale + else if (gray < BLACK_TEXT_THRESHOLD) { + finalColor = 0; // Black + } + else { + finalColor = 1; // White + } + + // Determine target colors for error calculation + uint8_t targetR, targetG, targetB; + + switch (finalColor) { + case 0: // Black + targetR = targetG = targetB = 0; + break; + case 2: // Red + targetR = 255; + targetG = targetB = 0; + break; + default: // White + targetR = targetG = targetB = 255; + break; + } + + // Calculate and distribute dithering errors + if (ENABLE_DITHERING && errorR != NULL) { + int16_t err_r = r - targetR; + int16_t err_g = g - targetG; + int16_t err_b = b - targetB; + + // Floyd-Steinberg dithering pattern + if (pos_x + 1 < EPD_WIDTH) { + // Right pixel (7/16) + errorR[pos_x + 1] += (err_r * 7) >> 4; + errorG[pos_x + 1] += (err_g * 7) >> 4; + errorB[pos_x + 1] += (err_b * 7) >> 4; + } + + if (pos_x > 0 && pos_x + 1 < EPD_WIDTH) { + errorR[pos_x - 1] += (err_r * 3) >> 4; // left-down (3/16) + errorG[pos_x - 1] += (err_g * 3) >> 4; + errorB[pos_x - 1] += (err_b * 3) >> 4; + + errorR[pos_x] += (err_r * 5) >> 4; // down (5/16) + errorG[pos_x] += (err_g * 5) >> 4; + errorB[pos_x] += (err_b * 5) >> 4; + + errorR[pos_x + 1] += (err_r * 1) >> 4; // right-down (1/16) + errorG[pos_x + 1] += (err_g * 1) >> 4; + errorB[pos_x + 1] += (err_b * 1) >> 4; + } + } + + // Draw the pixel based on the final color + switch (finalColor) { + case 0: // Black + Paint_SelectImage(BlackImage); + Paint_SetPixel(pos_x, pos_y, BLACK); + Paint_SelectImage(RYImage); + Paint_SetPixel(pos_x, pos_y, WHITE); + break; + + case 2: // Red + Paint_SelectImage(BlackImage); + Paint_SetPixel(pos_x, pos_y, WHITE); + Paint_SelectImage(RYImage); + Paint_SetPixel(pos_x, pos_y, BLACK); // BLACK in RY buffer = RED + break; + + default: // White + Paint_SelectImage(BlackImage); + Paint_SetPixel(pos_x, pos_y, WHITE); + Paint_SelectImage(RYImage); + Paint_SetPixel(pos_x, pos_y, WHITE); + break; + } + } + } + + return 1; // Continue decoding +} void setup() { Serial.begin(115200); - DEV_Module_Init(); - - // 1) Allocate framebuffers - const int W = EPD_7IN5B_V2_WIDTH; - const int H = EPD_7IN5B_V2_HEIGHT; - size_t bufSize = ((W + 7) / 8) * H; - BlackImage = (UBYTE*)malloc(bufSize); - RYImage = (UBYTE*)malloc(bufSize); - if (!BlackImage || !RYImage) { - Serial.println("ERROR: not enough RAM"); while (1) delay(1000); + Serial.println("E-Ink Display Initialization"); + + // Calculate buffer size as in Waveshare example + UWORD Imagesize = ((EPD_WIDTH % 8 == 0) ? (EPD_WIDTH / 8) : (EPD_WIDTH / 8 + 1)) * EPD_HEIGHT; + + // Allocate framebuffers + BlackImage = (UBYTE *)malloc(Imagesize); + RYImage = (UBYTE *)malloc(Imagesize); + + if ((BlackImage == NULL) || (RYImage == NULL)) { + Serial.println("Failed to allocate memory for framebuffers!"); + while(1); // Halt if memory allocation fails } - memset(BlackImage, 0xFF, bufSize); - memset(RYImage, 0xFF, bufSize); + + // Initialize the Paint library with the buffers + Paint_NewImage(BlackImage, EPD_WIDTH, EPD_HEIGHT, 0, WHITE); + Paint_NewImage(RYImage, EPD_WIDTH, EPD_HEIGHT, 0, WHITE); + + // Clear both buffers to WHITE using Paint library + Paint_SelectImage(BlackImage); + Paint_Clear(WHITE); + Paint_SelectImage(RYImage); + Paint_Clear(WHITE); + + Serial.println("Buffers allocated and cleared"); - // 2) Wi-Fi connect with timeout + // Connect to WiFi WiFi.begin(ssid, password); - Serial.print("Wi-Fi connecting"); - unsigned long startAttempt = millis(); - while (WiFi.status() != WL_CONNECTED && millis() - startAttempt < 10000) { - delay(500); Serial.print("."); + Serial.print("Connecting to WiFi"); + int wifiAttempts = 0; + while (WiFi.status() != WL_CONNECTED && wifiAttempts < 20) { + delay(500); + Serial.print("."); + wifiAttempts++; } - if (WiFi.status() != WL_CONNECTED) { - Serial.println(" ❌ Failed to connect to Wi-Fi"); - goto sleep_now; - } - Serial.println(" ✅"); - Serial.print("ESP32 IP = "); Serial.println(WiFi.localIP()); - - // 3) Prepare white canvases - Paint_NewImage(BlackImage, W, H, 0, WHITE); - Paint_NewImage(RYImage, W, H, 0, WHITE); - Paint_SelectImage(BlackImage); Paint_Clear(WHITE); - Paint_SelectImage(RYImage); Paint_Clear(WHITE); - - // 4) Manual HTTP GET via WiFiClient - { - WiFiClient client; - Serial.printf("Connecting to %s:%u …", HOST, PORT); - if (!client.connect(HOST, PORT)) { - WiFi.localIP(); - Serial.println(" FAILED"); - } else { - Serial.println(" OK"); - - // Send the GET request - client.printf("GET %s HTTP/1.1\r\n", PATH); - client.printf("Host: %s\r\n", HOST); - client.print ("Connection: close\r\n\r\n"); - - // Wait up to 15 s for the first byte - uint32_t start = millis(); - while (!client.available() && millis() - start < 15000) { - delay(10); - } - - if (!client.available()) { - Serial.println("No response—timeout"); - } else { - Serial.println("Received response, parsing…"); - - // 4.1 Read and check HTTP status line - String statusLine = client.readStringUntil('\n'); - Serial.print("HTTP Status Line: "); - Serial.println(statusLine); - - int statusCode = statusLine.substring(9, 12).toInt(); - if (statusCode != 200) { - Serial.printf("❌ HTTP Error: %d\n", statusCode); - client.stop(); - goto sleep_now; - } - - // 4.2 Skip remaining headers - while (client.available()) { - String line = client.readStringUntil('\n'); - if (line == "\r" || line == "") break; - } - - // 4.3 Read & parse BMP header (54 bytes) - uint8_t header[54]; - client.readBytes(header, 54); - uint32_t dataOffset = - uint32_t(header[10]) - | (uint32_t(header[11]) << 8) - | (uint32_t(header[12]) << 16) - | (uint32_t(header[13]) << 24); - - // 4.4 Skip any extra header padding - if (dataOffset > 54) { - uint32_t toSkip = dataOffset - 54; - uint8_t dum[32]; - while (toSkip) { - size_t chunk = toSkip > sizeof(dum) ? sizeof(dum) : toSkip; - client.readBytes(dum, chunk); - toSkip -= chunk; - } - } - - // 4.5 Decode BMP bottom-up - int rowSize = ((W * 3 + 3) / 4) * 4; - uint8_t *rowBuf = (uint8_t*)malloc(rowSize); - if (!rowBuf) { - Serial.println("ERROR: rowBuf malloc failed"); - } else { - for (int y = H - 1; y >= 0; y--) { - client.readBytes(rowBuf, rowSize); - - uint8_t mask = 0x80; - uint32_t idx = (y * W) / 8; - for (int x = 0; x < W; x++) { - uint8_t b = rowBuf[x * 3 + 0]; - uint8_t g = rowBuf[x * 3 + 1]; - uint8_t r = rowBuf[x * 3 + 2]; - - bool isRed = (r > 150 && g < 80 && b < 80); - bool isBlack = (!isRed && ((r + g + b) / 3 < 180)); - if (isRed) RYImage[idx] &= ~mask; - if (isBlack) BlackImage[idx] &= ~mask; - - mask >>= 1; - if (!mask) { mask = 0x80; idx++; } - } - } - free(rowBuf); - Serial.println("✅ BMP decoded successfully!"); - } - } - client.stop(); - } - - // 5) Display on the e-ink - EPD_7IN5B_V2_Init(); - EPD_7IN5B_V2_Display(BlackImage, RYImage); - DEV_Delay_ms(2000); - EPD_7IN5B_V2_Sleep(); + + if (WiFi.status() == WL_CONNECTED) { + Serial.println(); + Serial.println("WiFi connected"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + } else { + Serial.println(); + Serial.println("WiFi connection failed!"); + return; } -sleep_now: - // 6) Clean up + // Test connectivity to server and get configuration + Serial.println("Fetching connection information..."); + if (fetchConnectionInformation()) { + // Fetch and display image + fetchAndDisplayImage(); + } else { + Serial.println("Server connectivity test failed - skipping image fetch"); + } + + // Free dithering buffers if allocated - NEW + if (errorR) free(errorR); + if (errorG) free(errorG); + if (errorB) free(errorB); + errorR = errorG = errorB = NULL; + + // Put display to sleep + EPD_7IN5B_V2_Sleep(); + + // Free framebuffers free(BlackImage); free(RYImage); + BlackImage = NULL; + RYImage = NULL; - // 7) Sleep to save power - Serial.printf("💤 Sleeping for %d minutes…\n", SLEEP_MINUTES); - esp_deep_sleep(SLEEP_TIME_US); + // Enter deep sleep + Serial.print("Going to sleep for "); + Serial.print(sleepDuration / 60000000); + Serial.println(" minutes"); + esp_sleep_enable_timer_wakeup(sleepDuration); + esp_deep_sleep_start(); +} + +bool fetchConnectionInformation() { + HTTPClient http; + http.begin(connectionInformation); + http.setTimeout(10000); + + int httpCode = http.GET(); + + Serial.print("HTTP response code: "); + Serial.println(httpCode); + + // Handle the response payload + String payload = ""; + + if (httpCode == HTTP_CODE_OK) { + payload = http.getString(); + + // Debug output to show the exact response + Serial.println("-----RAW HTTP RESPONSE BEGIN-----"); + Serial.println(payload); + Serial.println("-----RAW HTTP RESPONSE END-----"); + + // Check if payload is empty + if (payload.length() == 0) { + Serial.println("Warning: Server returned empty response"); + http.end(); + return false; + } + + // Try to find JSON content in the response + int jsonStart = payload.indexOf('{'); + int jsonEnd = payload.lastIndexOf('}'); + + if (jsonStart >= 0 && jsonEnd >= 0 && jsonEnd > jsonStart) { + String jsonPayload = payload.substring(jsonStart, jsonEnd + 1); + Serial.println("-----EXTRACTED JSON BEGIN-----"); + Serial.println(jsonPayload); + Serial.println("-----EXTRACTED JSON END-----"); + + // Deserialize the JSON document + StaticJsonDocument<512> doc; + DeserializationError error = deserializeJson(doc, jsonPayload); + if (error) { + Serial.print("JSON parsing failed: "); + Serial.println(error.c_str()); + http.end(); + return false; + } + + // Extract values from the JSON + if (doc.containsKey("informationBoardImageUrl")) { + imageUrl = doc["informationBoardImageUrl"].as(); + Serial.print("Image URL set to: "); + Serial.println(imageUrl); + } else { + Serial.println("Warning: informationBoardImageUrl not found in JSON"); + http.end(); + return false; + } + + if (doc.containsKey("updateIntervalMinutes")) { + int minutes = doc["updateIntervalMinutes"].as(); + sleepDuration = (uint64_t)minutes * 60 * 1000000; // Convert minutes to microseconds + Serial.print("Update interval set to: "); + Serial.print(minutes); + Serial.println(" minutes"); + } else { + Serial.println("Warning: updateIntervalMinutes not found in JSON"); + // Keep default sleep duration + } + + http.end(); + return true; + } else { + Serial.println("No valid JSON object found in the response"); + http.end(); + return false; + } + } else { + Serial.print("HTTP request failed with code: "); + Serial.println(httpCode); + http.end(); + return false; + } +} + +void fetchAndDisplayImage() { + // Check WiFi connection before making HTTP request + if (WiFi.status() != WL_CONNECTED) { + Serial.println("WiFi not connected, cannot fetch image"); + return; + } + + if (imageUrl.length() == 0) { + Serial.println("Image URL not set, cannot fetch image"); + return; + } + + HTTPClient http; + http.begin(imageUrl); + http.setTimeout(30000); // Set 30 second timeout + http.addHeader("User-Agent", "ESP32"); + + Serial.print("Starting HTTP GET for image: "); + Serial.println(imageUrl); + int httpCode = http.GET(); + Serial.print("HTTP response code: "); + Serial.println(httpCode); + + if (httpCode == HTTP_CODE_OK) { + int len = http.getSize(); + Serial.print("Content length: "); + Serial.println(len); + + if (len > 0) { + Serial.print("Free heap before allocation: "); + Serial.println(ESP.getFreeHeap()); + + // Check if we have enough memory + if (ESP.getFreeHeap() < len + 10000) { // Keep 10KB buffer + Serial.println("Not enough memory to load image"); + http.end(); + return; + } + + uint8_t *buffer = (uint8_t*)malloc(len); + if (buffer) { + Serial.print("Allocated "); + Serial.print(len); + Serial.println(" bytes for image buffer."); + + // Clear both buffers before processing new image - USING PAINT LIBRARY + Paint_SelectImage(BlackImage); + Paint_Clear(WHITE); + Paint_SelectImage(RYImage); + Paint_Clear(WHITE); + + WiFiClient *stream = http.getStreamPtr(); + int totalBytesRead = 0; + unsigned long timeout = millis() + 30000; // 30 second timeout for reading + while (totalBytesRead < len && millis() < timeout) { + int bytesRead = stream->readBytes(buffer + totalBytesRead, len - totalBytesRead); + if (bytesRead == 0) { + delay(5); // Short delay if no data available + if (stream->available() == 0) { + if (totalBytesRead < len) { + Serial.println("Stream ended prematurely."); + } + break; + } + } else { + totalBytesRead += bytesRead; + } + } + Serial.print("Total bytes read: "); + Serial.println(totalBytesRead); + + if (totalBytesRead == len) { + // Process and display the image using JPEGDEC + Serial.println("Decoding JPEG image..."); + + // Open JPEG image from memory + if (jpeg.openRAM(buffer, len, jpegDrawCallback)) { + // Get information about the image + int jpegWidth = jpeg.getWidth(); + int jpegHeight = jpeg.getHeight(); + Serial.print("JPEG image dimensions: "); + Serial.print(jpegWidth); + Serial.print(" x "); + Serial.println(jpegHeight); + + // Decode the image + if (jpeg.decode(0, 0, 0)) { + Serial.println("JPEG image decoded successfully"); + } else { + Serial.println("Error decoding JPEG image"); + } + + // Close the file + jpeg.close(); + + // Initialize e-ink display exactly as in Waveshare example + DEV_Module_Init(); + EPD_7IN5B_V2_Init(); + EPD_7IN5B_V2_Clear(); + DEV_Delay_ms(500); + + // Display the processed image - using Waveshare's function + Serial.println("Sending image to display..."); + EPD_7IN5B_V2_Display(BlackImage, RYImage); + Serial.println("Image displayed successfully."); + } else { + Serial.println("Failed to open JPEG image"); + } + } else { + Serial.println("Failed to read entire image."); + } + free(buffer); + } else { + Serial.println("Failed to allocate buffer!"); + } + } else { + Serial.println("Content length unknown or invalid."); + } + } else if (httpCode == HTTPC_ERROR_CONNECTION_REFUSED) { + Serial.println("Connection refused - server may be down"); + } else if (httpCode == HTTPC_ERROR_CONNECTION_LOST) { + Serial.println("Connection lost during request"); + } else if (httpCode == HTTPC_ERROR_NO_HTTP_SERVER) { + Serial.println("No HTTP server found"); + } else if (httpCode == HTTPC_ERROR_NOT_CONNECTED) { + Serial.println("Not connected to server"); + } else { + Serial.printf("HTTP GET failed, error: %d\n", httpCode); + } + + http.end(); } void loop() { - // nothing + // Empty - using deep sleep instead }