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>
This commit is contained in:
2026-08-03 15:53:06 -04:00
parent 392061bfa0
commit fe20841d9e
13 changed files with 515 additions and 72 deletions

View File

@@ -106,7 +106,7 @@ void displayTask(void* param) {
// Fetch asset info for portrait detection and metadata
AssetInfo info = immichClient.fetchAssetInfo(assetId);
// Download photo
// Download primary photo
uint8_t* jpegBuf = nullptr;
size_t jpegSize = 0;
bool downloaded = immichClient.downloadAsset(
@@ -121,20 +121,77 @@ void displayTask(void* param) {
(unsigned)(jpegSize / 1024),
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
ProcessedImage img = imagePipeline.process(jpegBuf, jpegSize);
ProcessedImage img = {nullptr, 0, 0, false};
if (info.isPortrait) {
// Try to find a second portrait for side-by-side display
Serial.println("[display_task] Portrait detected, searching for pair");
String pairId = photoQueue.findNextPortrait();
if (pairId.length() > 0) {
// Fetch info for second portrait (for metadata)
AssetInfo info2 = immichClient.fetchAssetInfo(pairId);
// Download the second portrait
uint8_t* jpeg2Buf = nullptr;
size_t jpeg2Size = 0;
bool dl2 = immichClient.downloadAsset(
pairId, s.img_quality, &jpeg2Buf, &jpeg2Size);
if (dl2 && jpeg2Buf != nullptr) {
Serial.printf("[display_task] Portrait pair: %u KB + %u KB\n",
(unsigned)(jpegSize / 1024),
(unsigned)(jpeg2Size / 1024));
img = imagePipeline.processPortraitPair(
jpegBuf, jpegSize, jpeg2Buf, jpeg2Size);
free(jpeg2Buf);
if (img.valid) {
uint16_t halfW = (DISPLAY_WIDTH - PORTRAIT_GAP_PX) / 2;
uint16_t rightX = halfW + PORTRAIT_GAP_PX;
displayManager.showImage(img);
if (s.meta_flags != 0) {
displayManager.showMetadataInRegion(
info, s.meta_flags, s.meta_pos,
0, 0, halfW, DISPLAY_HEIGHT,
s.date_fmt, s.time_fmt);
displayManager.showMetadataInRegion(
info2, s.meta_flags, s.meta_pos,
rightX, 0, halfW, DISPLAY_HEIGHT,
s.date_fmt, s.time_fmt);
}
displayManager.refresh();
imagePipeline.freeImage(img);
displayed = true;
}
} else {
Serial.println("[display_task] Pair download failed, showing solo");
img = imagePipeline.process(jpegBuf, jpegSize);
}
} else {
// No pair found — show solo portrait (will get L/R letterbox)
img = imagePipeline.process(jpegBuf, jpegSize);
}
} else {
// Normal landscape processing
img = imagePipeline.process(jpegBuf, jpegSize);
}
free(jpegBuf);
Serial.printf("[display_task] Post-process PSRAM free: %u KB\n",
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
if (img.valid) {
if (img.valid && !displayed) {
displayManager.showImage(img);
if (s.meta_flags != 0) {
displayManager.showMetadata(info, s.meta_flags, s.meta_pos);
displayManager.showMetadata(info, s.meta_flags, s.meta_pos,
s.date_fmt, s.time_fmt);
}
displayManager.refresh();
imagePipeline.freeImage(img);
displayed = true;
} else {
} else if (!img.valid && !displayed) {
Serial.println("[display_task] Processing failed, trying next");
}
}
@@ -206,7 +263,8 @@ void setup() {
if (wifiManager.isAPMode()) {
displayManager.showSetupScreen();
} else {
displayManager.showMessage("Immich Frame", "Loading photos...");
// Skip "Loading photos" screen — the display task will show the first photo
// directly, avoiding a redundant 17s e-ink refresh cycle on boot.
// Create display task on Core 1
xTaskCreatePinnedToCore(displayTask, "display", 32768, nullptr, 1,
@@ -245,7 +303,14 @@ void loop() {
xSemaphoreGive(stateMutex);
}
break;
case ButtonEvent::DeepSleep:
case ButtonEvent::DeepSleep: {
Serial.println("[main] Entering deep sleep...");
Settings sleepS = settingsManager.get();
displayManager.showSleepIndicator(sleepS.meta_pos);
displayManager.refresh();
powerManager.enterDeepSleep();
break;
}
case ButtonEvent::FactoryReset:
break;
case ButtonEvent::None: