feat: display manager for e-ink framebuffer output and text messages
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
122
src/display_manager.cpp
Normal file
122
src/display_manager.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "display_manager.h"
|
||||
#include "power_manager.h"
|
||||
#include <M5Unified.h>
|
||||
|
||||
// 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];
|
||||
}
|
||||
25
src/display_manager.h
Normal file
25
src/display_manager.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <M5GFX.h>
|
||||
#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);
|
||||
};
|
||||
Reference in New Issue
Block a user