From 8c5c39dbe3685ef67f5c779925b7a16afa3e4e04 Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 3 Aug 2026 10:56:55 -0400 Subject: [PATCH] feat: display manager for e-ink framebuffer output and text messages Co-authored-by: Cursor --- src/display_manager.cpp | 122 ++++++++++++++++++++++++++++++++++++++++ src/display_manager.h | 25 ++++++++ 2 files changed, 147 insertions(+) create mode 100644 src/display_manager.cpp create mode 100644 src/display_manager.h diff --git a/src/display_manager.cpp b/src/display_manager.cpp new file mode 100644 index 0000000..9da5fae --- /dev/null +++ b/src/display_manager.cpp @@ -0,0 +1,122 @@ +#include "display_manager.h" +#include "power_manager.h" +#include + +// Map 6-color palette indices to RGB565 for M5GFX +static const uint16_t PALETTE_565[6] = { + 0x0000, // Black + 0xFFFF, // White + 0xF800, // Red (approximate) + 0x07E0, // Green (approximate) + 0x001F, // Blue (approximate) + 0xFFE0 // Yellow (approximate) +}; + +void DisplayManager::begin(PowerManager& power) { + _power = &power; + _display = &M5.Display; + + // Set rotation for landscape (device physically rotated) + // Rotation value depends on how the display is mounted — likely 1 or 3 + _display->setRotation(1); + + Serial.printf("[display] Initialized: %dx%d, rotation=%d\n", + _display->width(), _display->height(), _display->getRotation()); + + showMessage("Immich Frame", "Starting up..."); +} + +void DisplayManager::showImage(const ProcessedImage& img) { + if (!img.valid || img.framebuffer == nullptr) { + Serial.println("[display] Invalid image — skipping"); + return; + } + + _power->enableEPDPower(); + + Serial.println("[display] Writing framebuffer to e-ink..."); + unsigned long start = millis(); + + // Write pixel by pixel using palette-mapped colors + _display->startWrite(); + for (uint16_t y = 0; y < img.height; y++) { + for (uint16_t x = 0; x < img.width; x++) { + uint8_t colorIdx = img.framebuffer[y * img.width + x]; + _display->writePixel(x, y, PALETTE_565[colorIdx]); + } + } + _display->endWrite(); + + triggerRefresh(); + + unsigned long elapsed = millis() - start; + Serial.printf("[display] Refresh complete in %lu ms\n", elapsed); + + _power->disableEPDPower(); +} + +void DisplayManager::showMessage(const char* title, const char* body) { + _power->enableEPDPower(); + + _display->fillScreen(TFT_WHITE); + _display->setTextColor(TFT_BLACK); + _display->setTextDatum(middle_center); + _display->setTextSize(2); + _display->drawString(title, DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2 - 30); + _display->setTextSize(1); + _display->drawString(body, DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2 + 20); + + triggerRefresh(); + _power->disableEPDPower(); +} + +void DisplayManager::showMetadata(const AssetInfo& info, uint8_t metaFlags, MetaPosition pos) { + if (metaFlags == 0) return; // No metadata to show + + // Build metadata string + String metaText = ""; + + if ((metaFlags & META_DATE) && info.dateTime.length() > 0) { + // Extract just the date portion (YYYY-MM-DD) + metaText += info.dateTime.substring(0, 10); + } + if ((metaFlags & META_LOCATION) && info.city.length() > 0) { + if (metaText.length() > 0) metaText += " | "; + metaText += info.city; + } + if ((metaFlags & META_PEOPLE) && !info.people.empty()) { + if (metaText.length() > 0) metaText += " | "; + for (size_t i = 0; i < info.people.size(); i++) { + if (i > 0) metaText += ", "; + metaText += info.people[i]; + } + } + if ((metaFlags & META_ALBUM)) { + // Album name would need to be passed separately — skip for now + } + if ((metaFlags & META_CAMERA) && info.camera.length() > 0) { + if (metaText.length() > 0) metaText += " | "; + metaText += info.camera; + } + + if (metaText.length() == 0) return; + + // Draw semi-transparent bar with text + uint16_t barY = (pos == MetaPosition::Top) ? 0 : (DISPLAY_HEIGHT - 30); + _display->fillRect(0, barY, DISPLAY_WIDTH, 30, TFT_BLACK); + _display->setTextColor(TFT_WHITE); + _display->setTextDatum(middle_center); + _display->setTextSize(1); + _display->drawString(metaText.c_str(), DISPLAY_WIDTH / 2, barY + 15); +} + +void DisplayManager::triggerRefresh() { + // M5GFX handles e-ink refresh internally when using the EPD panel driver + // The display() call triggers the actual e-ink refresh cycle (10-20s) + _display->display(); +} + +uint16_t DisplayManager::paletteToColor565(uint8_t index) { + if (index >= DISPLAY_COLORS) return 0; + return PALETTE_565[index]; +} diff --git a/src/display_manager.h b/src/display_manager.h new file mode 100644 index 0000000..028e39e --- /dev/null +++ b/src/display_manager.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include "config.h" +#include "image_pipeline.h" +#include "immich_client.h" +#include "settings.h" + +class PowerManager; + +class DisplayManager { +public: + void begin(PowerManager& power); + void showImage(const ProcessedImage& img); + void showMessage(const char* title, const char* body); + void showMetadata(const AssetInfo& info, uint8_t metaFlags, MetaPosition pos); + +private: + PowerManager* _power = nullptr; + M5GFX* _display = nullptr; + + void triggerRefresh(); + uint16_t paletteToColor565(uint8_t index); +};