2026-08-03 10:56:55 -04:00
|
|
|
#include "display_manager.h"
|
|
|
|
|
#include "power_manager.h"
|
|
|
|
|
#include <M5Unified.h>
|
|
|
|
|
|
2026-08-03 19:54:59 -04:00
|
|
|
// EPD-exact palette: values produce distance=0 in Panel_ED2208's
|
|
|
|
|
// _rgb_to_epd_color nearest-color lookup, guaranteeing correct mapping.
|
|
|
|
|
// Order matches our pipeline's palette indices (0=black..5=yellow).
|
|
|
|
|
static const lgfx::bgr888_t EPD_PALETTE[6] = {
|
|
|
|
|
{0, 0, 0}, // Black (EPD_BLACK)
|
|
|
|
|
{255, 255, 255}, // White (EPD_WHITE)
|
|
|
|
|
{191, 0, 0}, // Red (EPD_RED)
|
|
|
|
|
{67, 138, 28}, // Green (EPD_GREEN)
|
|
|
|
|
{100, 64, 255}, // Blue (EPD_BLUE)
|
|
|
|
|
{255, 243, 56} // Yellow (EPD_YELLOW)
|
2026-08-03 10:56:55 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void DisplayManager::begin(PowerManager& power) {
|
|
|
|
|
_power = &power;
|
|
|
|
|
_display = &M5.Display;
|
|
|
|
|
|
2026-08-03 11:31:36 -04:00
|
|
|
// Ensure no automatic e-ink refreshes happen
|
|
|
|
|
_display->setAutoDisplay(false);
|
|
|
|
|
|
2026-08-03 10:56:55 -04:00
|
|
|
// Set rotation for landscape (device physically rotated)
|
2026-08-03 11:12:34 -04:00
|
|
|
_display->setRotation(3);
|
2026-08-03 10:56:55 -04:00
|
|
|
|
2026-08-03 19:52:22 -04:00
|
|
|
// Disable the panel driver's internal dithering (epd_quality applies a diagonal
|
|
|
|
|
// bias pattern via _dither_row_rgb_pair). Since our ImagePipeline already does
|
|
|
|
|
// proper Floyd-Steinberg dithering to the 6-color palette, the driver should
|
|
|
|
|
// just do a clean nearest-color lookup with no additional spatial bias.
|
|
|
|
|
// On Panel_ED2208 this only affects the dither function — NOT refresh quality.
|
|
|
|
|
_display->setEpdMode(epd_mode_t::epd_fastest);
|
|
|
|
|
|
|
|
|
|
Serial.printf("[display] Initialized: %dx%d, rotation=%d, epd_mode=fastest (no driver dither)\n",
|
2026-08-03 10:56:55 -04:00
|
|
|
_display->width(), _display->height(), _display->getRotation());
|
2026-08-03 11:25:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DisplayManager::showSetupScreen() {
|
|
|
|
|
_power->enableEPDPower();
|
|
|
|
|
|
2026-08-03 11:31:36 -04:00
|
|
|
// Batch all drawing — nothing hits the panel until display() at the end
|
|
|
|
|
_display->startWrite();
|
2026-08-03 11:25:41 -04:00
|
|
|
_display->fillScreen(TFT_WHITE);
|
|
|
|
|
|
|
|
|
|
// Generate QR code for WiFi auto-connect
|
|
|
|
|
const char* qrData = "WIFI:T:nopass;S:PaperColor-Setup;;";
|
|
|
|
|
QRCode qrcode;
|
|
|
|
|
uint8_t qrcodeData[qrcode_getBufferSize(6)];
|
|
|
|
|
qrcode_initText(&qrcode, qrcodeData, 6, ECC_LOW, qrData);
|
|
|
|
|
|
|
|
|
|
// Draw QR code centered vertically, left-of-center horizontally
|
|
|
|
|
int qrSize = qrcode.size;
|
2026-08-03 11:31:36 -04:00
|
|
|
int scale = 4;
|
2026-08-03 11:25:41 -04:00
|
|
|
int qrPixels = qrSize * scale;
|
|
|
|
|
int qrX = (DISPLAY_WIDTH / 3) - (qrPixels / 2);
|
|
|
|
|
int qrY = (DISPLAY_HEIGHT - qrPixels) / 2;
|
|
|
|
|
|
|
|
|
|
for (int y = 0; y < qrSize; y++) {
|
|
|
|
|
for (int x = 0; x < qrSize; x++) {
|
|
|
|
|
uint16_t color = qrcode_getModule(&qrcode, x, y) ? TFT_BLACK : TFT_WHITE;
|
|
|
|
|
_display->fillRect(qrX + x * scale, qrY + y * scale, scale, scale, color);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 11:31:36 -04:00
|
|
|
// Text instructions to the right of QR code
|
2026-08-03 11:25:41 -04:00
|
|
|
int textX = DISPLAY_WIDTH / 2 + 40;
|
|
|
|
|
_display->setTextColor(TFT_BLACK);
|
|
|
|
|
_display->setTextDatum(middle_left);
|
|
|
|
|
_display->setTextSize(2);
|
|
|
|
|
_display->drawString("PaperColor", textX, DISPLAY_HEIGHT / 2 - 60);
|
|
|
|
|
_display->setTextSize(1);
|
|
|
|
|
_display->drawString("Scan QR to connect", textX, DISPLAY_HEIGHT / 2 - 20);
|
|
|
|
|
_display->drawString("or join WiFi:", textX, DISPLAY_HEIGHT / 2 + 10);
|
|
|
|
|
_display->setTextSize(1.5);
|
|
|
|
|
_display->drawString("PaperColor-Setup", textX, DISPLAY_HEIGHT / 2 + 40);
|
|
|
|
|
_display->setTextSize(1);
|
|
|
|
|
_display->drawString("Then open:", textX, DISPLAY_HEIGHT / 2 + 70);
|
|
|
|
|
_display->drawString("192.168.4.1", textX, DISPLAY_HEIGHT / 2 + 95);
|
2026-08-03 10:56:55 -04:00
|
|
|
|
2026-08-03 11:31:36 -04:00
|
|
|
_display->endWrite();
|
|
|
|
|
|
|
|
|
|
// Single e-ink refresh — the only panel update during boot
|
2026-08-03 11:25:41 -04:00
|
|
|
triggerRefresh();
|
|
|
|
|
_power->disableEPDPower();
|
2026-08-03 10:56:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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...");
|
|
|
|
|
|
2026-08-03 19:54:59 -04:00
|
|
|
// Bulk-write framebuffer using palette lookup (handles rotation internally)
|
2026-08-03 10:56:55 -04:00
|
|
|
_display->startWrite();
|
2026-08-03 19:54:59 -04:00
|
|
|
_display->pushImage(0, 0, img.width, img.height,
|
|
|
|
|
img.framebuffer, lgfx::color_depth_t::palette_8bit, EPD_PALETTE);
|
2026-08-03 10:56:55 -04:00
|
|
|
_display->endWrite();
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
// Do NOT refresh here — caller should add metadata overlay then call refresh()
|
2026-08-03 10:56:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DisplayManager::showMessage(const char* title, const char* body) {
|
|
|
|
|
_power->enableEPDPower();
|
|
|
|
|
|
2026-08-03 11:31:36 -04:00
|
|
|
_display->startWrite();
|
2026-08-03 10:56:55 -04:00
|
|
|
_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);
|
2026-08-03 11:31:36 -04:00
|
|
|
_display->endWrite();
|
2026-08-03 10:56:55 -04:00
|
|
|
|
|
|
|
|
triggerRefresh();
|
|
|
|
|
_power->disableEPDPower();
|
|
|
|
|
}
|
|
|
|
|
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
void DisplayManager::showMetadata(const AssetInfo& info, uint8_t metaFlags, MetaPosition pos,
|
|
|
|
|
const String& dateFmt, const String& timeFmt) {
|
|
|
|
|
showMetadataInRegion(info, metaFlags, pos, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, dateFmt, timeFmt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DisplayManager::showMetadataInRegion(const AssetInfo& info, uint8_t metaFlags, MetaPosition pos,
|
|
|
|
|
uint16_t regionX, uint16_t regionY,
|
|
|
|
|
uint16_t regionW, uint16_t regionH,
|
|
|
|
|
const String& dateFmt, const String& timeFmt) {
|
|
|
|
|
if (metaFlags == 0) return;
|
2026-08-03 10:56:55 -04:00
|
|
|
|
2026-08-03 20:50:01 -04:00
|
|
|
// Build metadata string (excluding location — it gets its own badge)
|
2026-08-03 10:56:55 -04:00
|
|
|
String metaText = "";
|
|
|
|
|
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
// Parse ISO datetime (YYYY-MM-DDTHH:MM:SS) into struct tm for strftime
|
|
|
|
|
if (((metaFlags & META_DATE) || (metaFlags & META_TIME)) && info.dateTime.length() >= 19) {
|
|
|
|
|
struct tm t = {};
|
|
|
|
|
sscanf(info.dateTime.c_str(), "%d-%d-%dT%d:%d:%d",
|
|
|
|
|
&t.tm_year, &t.tm_mon, &t.tm_mday,
|
|
|
|
|
&t.tm_hour, &t.tm_min, &t.tm_sec);
|
|
|
|
|
t.tm_year -= 1900;
|
|
|
|
|
t.tm_mon -= 1;
|
|
|
|
|
|
|
|
|
|
char buf[64];
|
|
|
|
|
if ((metaFlags & META_DATE) && (metaFlags & META_TIME)) {
|
|
|
|
|
String combinedFmt = dateFmt + " " + timeFmt;
|
|
|
|
|
strftime(buf, sizeof(buf), combinedFmt.c_str(), &t);
|
|
|
|
|
metaText += buf;
|
|
|
|
|
} else if (metaFlags & META_DATE) {
|
|
|
|
|
strftime(buf, sizeof(buf), dateFmt.c_str(), &t);
|
|
|
|
|
metaText += buf;
|
|
|
|
|
} else {
|
|
|
|
|
strftime(buf, sizeof(buf), timeFmt.c_str(), &t);
|
|
|
|
|
metaText += buf;
|
|
|
|
|
}
|
|
|
|
|
} else if ((metaFlags & META_DATE) && info.dateTime.length() >= 10) {
|
|
|
|
|
// Fallback if datetime is too short for full parse
|
2026-08-03 10:56:55 -04:00
|
|
|
metaText += info.dateTime.substring(0, 10);
|
|
|
|
|
}
|
|
|
|
|
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)) {
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
// Album name would need to be passed separately
|
2026-08-03 10:56:55 -04:00
|
|
|
}
|
|
|
|
|
if ((metaFlags & META_CAMERA) && info.camera.length() > 0) {
|
|
|
|
|
if (metaText.length() > 0) metaText += " | ";
|
|
|
|
|
metaText += info.camera;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 20:50:01 -04:00
|
|
|
// Location gets its own badge in lower-left
|
|
|
|
|
String locationText = "";
|
|
|
|
|
if ((metaFlags & META_LOCATION) && info.city.length() > 0) {
|
|
|
|
|
locationText = info.city;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (metaText.length() == 0 && locationText.length() == 0) return;
|
|
|
|
|
|
|
|
|
|
static constexpr uint16_t MARGIN = 8;
|
|
|
|
|
static constexpr uint16_t PAD_X = 8;
|
|
|
|
|
static constexpr uint16_t PAD_Y = 5;
|
|
|
|
|
static constexpr uint16_t TEXT_H = 20;
|
2026-08-03 21:03:46 -04:00
|
|
|
static constexpr uint16_t ICON_SIZE = 12;
|
2026-08-03 21:06:48 -04:00
|
|
|
static constexpr uint16_t ICON_GAP = 6;
|
2026-08-03 21:03:46 -04:00
|
|
|
|
|
|
|
|
bool showIcons = (regionW == DISPLAY_WIDTH);
|
2026-08-03 10:56:55 -04:00
|
|
|
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
_display->setTextSize(1.5f);
|
2026-08-03 10:56:55 -04:00
|
|
|
_display->setTextColor(TFT_WHITE);
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
|
2026-08-03 20:50:01 -04:00
|
|
|
// Track where the main badge ends up (for location badge offset)
|
|
|
|
|
uint16_t mainBadgeBottomY = 0;
|
|
|
|
|
|
|
|
|
|
// Draw main metadata badge (date/time/people/camera) at configured position
|
|
|
|
|
if (metaText.length() > 0) {
|
|
|
|
|
switch (pos) {
|
|
|
|
|
case MetaPosition::CaptionBottom:
|
|
|
|
|
case MetaPosition::CaptionTop: {
|
|
|
|
|
uint16_t barH = 36;
|
|
|
|
|
uint16_t barY = (pos == MetaPosition::CaptionTop)
|
|
|
|
|
? regionY
|
|
|
|
|
: regionY + regionH - barH;
|
|
|
|
|
_display->fillRect(regionX, barY, regionW, barH, TFT_BLACK);
|
|
|
|
|
_display->setTextDatum(middle_center);
|
|
|
|
|
_display->drawString(metaText.c_str(), regionX + regionW / 2, barY + barH / 2);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case MetaPosition::OverlayTopLeft:
|
|
|
|
|
case MetaPosition::OverlayTopRight:
|
|
|
|
|
case MetaPosition::OverlayBottomLeft:
|
|
|
|
|
case MetaPosition::OverlayBottomRight: {
|
|
|
|
|
uint16_t textW = _display->textWidth(metaText.c_str());
|
2026-08-03 21:03:46 -04:00
|
|
|
uint16_t iconExtra = showIcons ? (ICON_SIZE + ICON_GAP) : 0;
|
|
|
|
|
uint16_t badgeW = min((uint16_t)(textW + iconExtra + PAD_X * 2), regionW);
|
2026-08-03 20:50:01 -04:00
|
|
|
uint16_t badgeH = TEXT_H + PAD_Y * 2;
|
|
|
|
|
|
|
|
|
|
uint16_t bx, by;
|
|
|
|
|
switch (pos) {
|
|
|
|
|
case MetaPosition::OverlayTopLeft:
|
|
|
|
|
bx = regionX + MARGIN;
|
|
|
|
|
by = regionY + MARGIN;
|
|
|
|
|
break;
|
|
|
|
|
case MetaPosition::OverlayTopRight:
|
|
|
|
|
bx = regionX + regionW - badgeW - MARGIN;
|
|
|
|
|
by = regionY + MARGIN;
|
|
|
|
|
break;
|
|
|
|
|
case MetaPosition::OverlayBottomLeft:
|
|
|
|
|
bx = regionX + MARGIN;
|
|
|
|
|
by = regionY + regionH - badgeH - MARGIN;
|
|
|
|
|
break;
|
|
|
|
|
case MetaPosition::OverlayBottomRight:
|
|
|
|
|
bx = regionX + regionW - badgeW - MARGIN;
|
|
|
|
|
by = regionY + regionH - badgeH - MARGIN;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
bx = regionX + MARGIN;
|
|
|
|
|
by = regionY + MARGIN;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawOverlayBadge(bx, by, badgeW, badgeH);
|
|
|
|
|
_display->setTextDatum(middle_left);
|
2026-08-03 21:03:46 -04:00
|
|
|
uint16_t textX = bx + PAD_X;
|
|
|
|
|
if (showIcons) {
|
2026-08-03 21:06:48 -04:00
|
|
|
drawCalendarIcon(bx + PAD_X, by + (badgeH - ICON_SIZE) / 2 - 1, ICON_SIZE);
|
2026-08-03 21:03:46 -04:00
|
|
|
textX += ICON_SIZE + ICON_GAP;
|
|
|
|
|
}
|
|
|
|
|
_display->drawString(metaText.c_str(), textX, by + badgeH / 2);
|
2026-08-03 20:50:01 -04:00
|
|
|
mainBadgeBottomY = by + badgeH;
|
|
|
|
|
break;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
}
|
2026-08-03 20:50:01 -04:00
|
|
|
}
|
|
|
|
|
}
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
|
2026-08-03 20:50:01 -04:00
|
|
|
// Draw location badge in lower-left corner (separate from main badge)
|
|
|
|
|
if (locationText.length() > 0) {
|
|
|
|
|
uint16_t locTextW = _display->textWidth(locationText.c_str());
|
2026-08-03 21:03:46 -04:00
|
|
|
uint16_t iconExtra = showIcons ? (ICON_SIZE + ICON_GAP) : 0;
|
|
|
|
|
uint16_t locBadgeW = min((uint16_t)(locTextW + iconExtra + PAD_X * 2), regionW);
|
2026-08-03 20:50:01 -04:00
|
|
|
uint16_t locBadgeH = TEXT_H + PAD_Y * 2;
|
|
|
|
|
|
|
|
|
|
uint16_t lx = regionX + MARGIN;
|
|
|
|
|
uint16_t ly = regionY + regionH - locBadgeH - MARGIN;
|
|
|
|
|
|
|
|
|
|
// If main badge is also in bottom-left, stack location above it
|
|
|
|
|
if (pos == MetaPosition::OverlayBottomLeft && metaText.length() > 0) {
|
|
|
|
|
ly = ly - locBadgeH - 4;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
}
|
2026-08-03 20:50:01 -04:00
|
|
|
|
|
|
|
|
drawOverlayBadge(lx, ly, locBadgeW, locBadgeH);
|
|
|
|
|
_display->setTextColor(TFT_WHITE);
|
|
|
|
|
_display->setTextDatum(middle_left);
|
2026-08-03 21:03:46 -04:00
|
|
|
uint16_t locTextX = lx + PAD_X;
|
|
|
|
|
if (showIcons) {
|
2026-08-03 21:06:48 -04:00
|
|
|
drawPinIcon(lx + PAD_X, ly + (locBadgeH - ICON_SIZE) / 2 - 1, ICON_SIZE);
|
2026-08-03 21:03:46 -04:00
|
|
|
locTextX += ICON_SIZE + ICON_GAP;
|
|
|
|
|
}
|
|
|
|
|
_display->drawString(locationText.c_str(), locTextX, ly + locBadgeH / 2);
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 20:50:01 -04:00
|
|
|
void DisplayManager::drawOverlayBadge(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
|
|
|
|
static constexpr int16_t R = 4;
|
|
|
|
|
|
|
|
|
|
if (!_translucent) {
|
|
|
|
|
_display->fillRoundRect(x, y, w, h, R, TFT_BLACK);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checkerboard pattern within rounded-rect bounds for 50% opacity effect.
|
|
|
|
|
// Use fillRoundRect to create a clip mask in black, then punch holes by
|
|
|
|
|
// restoring every-other pixel. Simpler: just iterate and test corner radii.
|
|
|
|
|
for (uint16_t py = y; py < y + h; py++) {
|
|
|
|
|
for (uint16_t px = x; px < x + w; px++) {
|
|
|
|
|
if ((px + py) % 2 != 0) continue;
|
|
|
|
|
|
|
|
|
|
// Check if pixel is inside the rounded rect (corner exclusion)
|
|
|
|
|
int16_t lx = px - x;
|
|
|
|
|
int16_t ly = py - y;
|
|
|
|
|
bool inside = true;
|
|
|
|
|
|
|
|
|
|
// Top-left corner
|
|
|
|
|
if (lx < R && ly < R) {
|
|
|
|
|
int16_t dx = R - 1 - lx;
|
|
|
|
|
int16_t dy = R - 1 - ly;
|
|
|
|
|
inside = (dx * dx + dy * dy) <= (R * R);
|
|
|
|
|
}
|
|
|
|
|
// Top-right corner
|
|
|
|
|
else if (lx >= w - R && ly < R) {
|
|
|
|
|
int16_t dx = lx - (w - R);
|
|
|
|
|
int16_t dy = R - 1 - ly;
|
|
|
|
|
inside = (dx * dx + dy * dy) <= (R * R);
|
|
|
|
|
}
|
|
|
|
|
// Bottom-left corner
|
|
|
|
|
else if (lx < R && ly >= h - R) {
|
|
|
|
|
int16_t dx = R - 1 - lx;
|
|
|
|
|
int16_t dy = ly - (h - R);
|
|
|
|
|
inside = (dx * dx + dy * dy) <= (R * R);
|
|
|
|
|
}
|
|
|
|
|
// Bottom-right corner
|
|
|
|
|
else if (lx >= w - R && ly >= h - R) {
|
|
|
|
|
int16_t dx = lx - (w - R);
|
|
|
|
|
int16_t dy = ly - (h - R);
|
|
|
|
|
inside = (dx * dx + dy * dy) <= (R * R);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (inside) {
|
|
|
|
|
_display->writePixel(px, py, TFT_BLACK);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-03 19:52:22 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-03 21:03:46 -04:00
|
|
|
void DisplayManager::drawPinIcon(int32_t x, int32_t y, int32_t size) {
|
|
|
|
|
int32_t r = size * 2 / 5;
|
|
|
|
|
int32_t cx = x + size / 2;
|
|
|
|
|
int32_t cy = y + r + 1;
|
|
|
|
|
_display->fillCircle(cx, cy, r, TFT_WHITE);
|
|
|
|
|
_display->fillTriangle(cx - r + 1, cy + r / 2, cx + r - 1, cy + r / 2,
|
|
|
|
|
cx, y + size - 1, TFT_WHITE);
|
|
|
|
|
_display->fillCircle(cx, cy, r / 3, TFT_BLACK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DisplayManager::drawCalendarIcon(int32_t x, int32_t y, int32_t size) {
|
|
|
|
|
int32_t bodyY = y + size / 4;
|
|
|
|
|
int32_t bodyH = size - size / 4;
|
|
|
|
|
int32_t bodyW = size;
|
|
|
|
|
_display->drawRect(x, bodyY, bodyW, bodyH, TFT_WHITE);
|
|
|
|
|
int32_t headerH = bodyH / 3;
|
|
|
|
|
_display->drawLine(x, bodyY + headerH, x + bodyW - 1, bodyY + headerH, TFT_WHITE);
|
|
|
|
|
int32_t tabW = 2, tabH = size / 4 + 1;
|
|
|
|
|
_display->fillRect(x + bodyW / 4, y, tabW, tabH, TFT_WHITE);
|
|
|
|
|
_display->fillRect(x + bodyW * 3 / 4 - tabW, y, tabW, tabH, TFT_WHITE);
|
|
|
|
|
int32_t dotSize = max((int32_t)1, size / 6);
|
|
|
|
|
int32_t gridY = bodyY + headerH + 2;
|
|
|
|
|
int32_t gridX = x + 2;
|
|
|
|
|
int32_t gapX = (bodyW - 4) / 3;
|
|
|
|
|
int32_t gapY = (bodyH - headerH - 3) / 2;
|
|
|
|
|
for (int row = 0; row < 2; row++)
|
|
|
|
|
for (int col = 0; col < 3; col++)
|
|
|
|
|
_display->fillRect(gridX + col * gapX, gridY + row * gapY,
|
|
|
|
|
dotSize, dotSize, TFT_WHITE);
|
|
|
|
|
}
|
|
|
|
|
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
void DisplayManager::showSleepIndicator(MetaPosition metaPos) {
|
|
|
|
|
_power->enableEPDPower();
|
|
|
|
|
|
2026-08-03 19:52:22 -04:00
|
|
|
static constexpr uint16_t MARGIN = 8;
|
|
|
|
|
static constexpr uint16_t PAD_X = 6;
|
|
|
|
|
static constexpr uint16_t PAD_Y = 4;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
|
2026-08-03 19:52:22 -04:00
|
|
|
_display->setTextSize(1);
|
|
|
|
|
uint16_t textW = _display->textWidth("SLP");
|
|
|
|
|
uint16_t textH = 14;
|
|
|
|
|
uint16_t badgeW = textW + PAD_X * 2;
|
|
|
|
|
uint16_t badgeH = textH + PAD_Y * 2;
|
|
|
|
|
|
|
|
|
|
// Pick a corner opposite to metadata position
|
|
|
|
|
uint16_t bx, by;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
switch (metaPos) {
|
|
|
|
|
case MetaPosition::OverlayTopLeft:
|
|
|
|
|
case MetaPosition::CaptionTop:
|
2026-08-03 19:52:22 -04:00
|
|
|
bx = DISPLAY_WIDTH - badgeW - MARGIN;
|
|
|
|
|
by = DISPLAY_HEIGHT - badgeH - MARGIN;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
break;
|
|
|
|
|
case MetaPosition::OverlayTopRight:
|
2026-08-03 19:52:22 -04:00
|
|
|
bx = MARGIN;
|
|
|
|
|
by = DISPLAY_HEIGHT - badgeH - MARGIN;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
break;
|
|
|
|
|
case MetaPosition::OverlayBottomLeft:
|
2026-08-03 19:52:22 -04:00
|
|
|
bx = DISPLAY_WIDTH - badgeW - MARGIN;
|
|
|
|
|
by = MARGIN;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
break;
|
|
|
|
|
case MetaPosition::OverlayBottomRight:
|
|
|
|
|
case MetaPosition::CaptionBottom:
|
|
|
|
|
default:
|
2026-08-03 19:52:22 -04:00
|
|
|
bx = MARGIN;
|
|
|
|
|
by = MARGIN;
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 20:50:01 -04:00
|
|
|
drawOverlayBadge(bx, by, badgeW, badgeH);
|
2026-08-03 19:52:22 -04:00
|
|
|
_display->setTextColor(TFT_WHITE);
|
|
|
|
|
_display->setTextDatum(middle_center);
|
|
|
|
|
_display->drawString("SLP", bx + badgeW / 2, by + badgeH / 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DisplayManager::showBatteryIndicator(uint8_t percent, MetaPosition metaPos) {
|
|
|
|
|
static constexpr uint16_t MARGIN = 8;
|
|
|
|
|
static constexpr uint16_t PAD_X = 6;
|
|
|
|
|
static constexpr uint16_t PAD_Y = 4;
|
|
|
|
|
|
|
|
|
|
// Battery icon dimensions
|
|
|
|
|
static constexpr uint16_t BATT_W = 20;
|
|
|
|
|
static constexpr uint16_t BATT_H = 10;
|
|
|
|
|
static constexpr uint16_t NUB_W = 3;
|
|
|
|
|
static constexpr uint16_t NUB_H = 5;
|
|
|
|
|
static constexpr uint16_t GAP = 4;
|
|
|
|
|
|
|
|
|
|
// Build percentage text
|
|
|
|
|
char pctText[5];
|
|
|
|
|
snprintf(pctText, sizeof(pctText), "%d%%", percent);
|
|
|
|
|
|
2026-08-03 10:56:55 -04:00
|
|
|
_display->setTextSize(1);
|
2026-08-03 19:52:22 -04:00
|
|
|
uint16_t textW = _display->textWidth(pctText);
|
|
|
|
|
uint16_t textH = 14;
|
|
|
|
|
|
|
|
|
|
// Total badge size: icon + gap + text + padding
|
|
|
|
|
uint16_t contentW = BATT_W + NUB_W + GAP + textW;
|
|
|
|
|
uint16_t contentH = max(BATT_H, textH);
|
|
|
|
|
uint16_t badgeW = contentW + PAD_X * 2;
|
|
|
|
|
uint16_t badgeH = contentH + PAD_Y * 2;
|
|
|
|
|
|
|
|
|
|
// Position: top-right by default, top-left if metadata occupies top-right
|
|
|
|
|
uint16_t bx, by;
|
|
|
|
|
if (metaPos == MetaPosition::OverlayTopRight) {
|
|
|
|
|
bx = MARGIN;
|
|
|
|
|
by = MARGIN;
|
|
|
|
|
} else {
|
|
|
|
|
bx = DISPLAY_WIDTH - badgeW - MARGIN;
|
|
|
|
|
by = MARGIN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw badge background
|
2026-08-03 20:50:01 -04:00
|
|
|
drawOverlayBadge(bx, by, badgeW, badgeH);
|
2026-08-03 19:52:22 -04:00
|
|
|
|
|
|
|
|
// Draw battery outline (white rect with 1px border)
|
|
|
|
|
uint16_t iconX = bx + PAD_X;
|
|
|
|
|
uint16_t iconY = by + (badgeH - BATT_H) / 2;
|
|
|
|
|
_display->drawRect(iconX, iconY, BATT_W, BATT_H, TFT_WHITE);
|
|
|
|
|
|
|
|
|
|
// Draw nub on right side of battery
|
|
|
|
|
uint16_t nubX = iconX + BATT_W;
|
|
|
|
|
uint16_t nubY = iconY + (BATT_H - NUB_H) / 2;
|
|
|
|
|
_display->fillRect(nubX, nubY, NUB_W, NUB_H, TFT_WHITE);
|
|
|
|
|
|
|
|
|
|
// Draw fill level inside battery (1px inset)
|
|
|
|
|
uint16_t fillMaxW = BATT_W - 2;
|
|
|
|
|
uint16_t fillW = (fillMaxW * percent) / 100;
|
|
|
|
|
if (fillW > 0) {
|
|
|
|
|
_display->fillRect(iconX + 1, iconY + 1, fillW, BATT_H - 2, TFT_WHITE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw percentage text
|
|
|
|
|
uint16_t textX = iconX + BATT_W + NUB_W + GAP;
|
|
|
|
|
_display->setTextColor(TFT_WHITE);
|
|
|
|
|
_display->setTextDatum(middle_left);
|
|
|
|
|
_display->drawString(pctText, textX, by + badgeH / 2);
|
feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 15:53:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DisplayManager::refresh() {
|
|
|
|
|
unsigned long start = millis();
|
|
|
|
|
triggerRefresh();
|
|
|
|
|
unsigned long elapsed = millis() - start;
|
|
|
|
|
Serial.printf("[display] Refresh complete in %lu ms\n", elapsed);
|
|
|
|
|
_power->disableEPDPower();
|
2026-08-03 10:56:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DisplayManager::triggerRefresh() {
|
2026-08-03 19:54:59 -04:00
|
|
|
// Panel_ED2208::_exec_transfer always sends the full 400x600 frame
|
|
|
|
|
// regardless of dirty region — the dirty rect only gates whether
|
|
|
|
|
// a transfer happens at all. ~30ms at 4MHz SPI, fine for photo frame.
|
2026-08-03 10:56:55 -04:00
|
|
|
_display->display();
|
|
|
|
|
}
|
|
|
|
|
|