fix: eliminate diagonal dither pattern from display driver
Root cause: Panel_ED2208's epd_quality mode applies _dither_row_rgb_pair (diagonal bias pattern with dither=140) during _exec_transfer(). Since our ImagePipeline already does proper Floyd-Steinberg dithering, the driver's additional dithering was creating visible diagonal artifacts. Fix: setEpdMode(epd_fastest) selects _dither_row_none (clean nearest-color lookup, no spatial bias). On Panel_ED2208 this only affects the dither function — NOT refresh quality or waveform. Also: default dither_noise setting to OFF since error scatter was targeting the wrong layer (it made gradient areas fuzzier without fixing the real problem in the display driver). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
113
src/main.cpp
113
src/main.cpp
@@ -61,6 +61,8 @@ void displayTask(void* param) {
|
||||
while (true) {
|
||||
unsigned long now = millis();
|
||||
Settings s = settingsManager.get();
|
||||
imagePipeline.setPipelineMode(static_cast<PipelineMode>(s.pipeline_mode));
|
||||
imagePipeline.setBlueNoiseEnabled(s.dither_noise != 0);
|
||||
unsigned long intervalMs = s.interval_min * 60000UL;
|
||||
|
||||
bool shouldRefresh = false;
|
||||
@@ -160,6 +162,10 @@ void displayTask(void* param) {
|
||||
rightX, 0, halfW, DISPLAY_HEIGHT,
|
||||
s.date_fmt, s.time_fmt);
|
||||
}
|
||||
if (s.show_battery) {
|
||||
uint8_t battPct = powerManager.getBatteryPercent();
|
||||
displayManager.showBatteryIndicator(battPct, s.meta_pos);
|
||||
}
|
||||
displayManager.refresh();
|
||||
imagePipeline.freeImage(img);
|
||||
displayed = true;
|
||||
@@ -188,6 +194,10 @@ void displayTask(void* param) {
|
||||
displayManager.showMetadata(info, s.meta_flags, s.meta_pos,
|
||||
s.date_fmt, s.time_fmt);
|
||||
}
|
||||
if (s.show_battery) {
|
||||
uint8_t battPct = powerManager.getBatteryPercent();
|
||||
displayManager.showBatteryIndicator(battPct, s.meta_pos);
|
||||
}
|
||||
displayManager.refresh();
|
||||
imagePipeline.freeImage(img);
|
||||
displayed = true;
|
||||
@@ -222,6 +232,67 @@ void webActionCallback(const String& action) {
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal boot path for RTC timer wake: fetch one photo, display, sleep again
|
||||
void timerWakeCycle() {
|
||||
Serial.println("[timer_wake] Starting minimal wake cycle");
|
||||
|
||||
Settings s = settingsManager.get();
|
||||
|
||||
// Connect WiFi (blocking)
|
||||
wifiManager.begin(settingsManager);
|
||||
if (!wifiManager.isConnected()) {
|
||||
Serial.println("[timer_wake] WiFi failed — going back to sleep");
|
||||
powerManager.enterDeepSleep(s.interval_min);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Immich client and apply pipeline mode
|
||||
immichClient.begin(s.immich_url, s.immich_key);
|
||||
imagePipeline.setPipelineMode(static_cast<PipelineMode>(s.pipeline_mode));
|
||||
imagePipeline.setBlueNoiseEnabled(s.dither_noise != 0);
|
||||
|
||||
// Fetch a small batch of random assets
|
||||
static constexpr int TIMER_WAKE_RETRIES = 5;
|
||||
bool displayed = false;
|
||||
|
||||
std::vector<String> ids = immichClient.fetchRandomAssetIds(10);
|
||||
for (int i = 0; i < (int)ids.size() && i < TIMER_WAKE_RETRIES && !displayed; i++) {
|
||||
AssetInfo info = immichClient.fetchAssetInfo(ids[i]);
|
||||
|
||||
uint8_t* jpegBuf = nullptr;
|
||||
size_t jpegSize = 0;
|
||||
if (!immichClient.downloadAsset(ids[i], s.img_quality, &jpegBuf, &jpegSize)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ProcessedImage img = imagePipeline.process(jpegBuf, jpegSize);
|
||||
free(jpegBuf);
|
||||
|
||||
if (img.valid) {
|
||||
displayManager.showImage(img);
|
||||
if (s.meta_flags != 0) {
|
||||
displayManager.showMetadata(info, s.meta_flags, s.meta_pos,
|
||||
s.date_fmt, s.time_fmt);
|
||||
}
|
||||
if (s.show_battery) {
|
||||
uint8_t battPct = powerManager.getBatteryPercent();
|
||||
displayManager.showBatteryIndicator(battPct, s.meta_pos);
|
||||
}
|
||||
displayManager.showSleepIndicator(s.meta_pos);
|
||||
displayManager.refresh();
|
||||
imagePipeline.freeImage(img);
|
||||
displayed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!displayed) {
|
||||
Serial.println("[timer_wake] Failed to display any photo");
|
||||
}
|
||||
|
||||
Serial.printf("[timer_wake] Cycle complete — sleeping %u min\n", s.interval_min);
|
||||
powerManager.enterDeepSleep(s.interval_min);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
auto cfg = M5.config();
|
||||
cfg.clear_display = false; // Don't clear/refresh e-ink on init
|
||||
@@ -238,15 +309,41 @@ void setup() {
|
||||
Serial.printf("[main] Internal RAM: %u KB free\n",
|
||||
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
|
||||
|
||||
// Initialize core subsystems needed for both wake paths
|
||||
powerManager.begin();
|
||||
settingsManager.begin();
|
||||
displayManager.begin(powerManager);
|
||||
|
||||
// Check if we woke from a timed deep sleep (persisted flag in NVS)
|
||||
bool timerWoke = PowerManager::wasTimedSleepWake();
|
||||
|
||||
if (timerWoke) {
|
||||
Serial.println("[main] Woke from timed deep sleep");
|
||||
|
||||
// Escape hatch: hold play/pause button during boot to stay awake
|
||||
pinMode(PIN_BTN_DOWN, INPUT_PULLUP);
|
||||
delay(50);
|
||||
bool pauseHeld = (digitalRead(PIN_BTN_DOWN) == LOW);
|
||||
|
||||
if (!pauseHeld) {
|
||||
// Minimal cycle: show photo and go back to sleep
|
||||
timerWakeCycle();
|
||||
// timerWakeCycle() calls enterDeepSleep — should not return
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape hatch triggered — clear the flag and enter full interactive mode
|
||||
PowerManager::clearTimedSleepFlag();
|
||||
Serial.println("[main] Play/pause held — entering full interactive mode");
|
||||
}
|
||||
|
||||
// Full interactive boot path
|
||||
// Create state mutex
|
||||
stateMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Initialize subsystems
|
||||
powerManager.begin();
|
||||
settingsManager.begin();
|
||||
// Initialize remaining subsystems
|
||||
wifiManager.begin(settingsManager);
|
||||
buttonHandler.begin(powerManager, settingsManager);
|
||||
displayManager.begin(powerManager);
|
||||
|
||||
if (!wifiManager.isAPMode()) {
|
||||
// Station mode — set up Immich and web server
|
||||
@@ -304,11 +401,15 @@ void loop() {
|
||||
}
|
||||
break;
|
||||
case ButtonEvent::DeepSleep: {
|
||||
Serial.println("[main] Entering deep sleep...");
|
||||
Serial.println("[main] Entering timed deep sleep slideshow...");
|
||||
Settings sleepS = settingsManager.get();
|
||||
displayManager.showSleepIndicator(sleepS.meta_pos);
|
||||
if (sleepS.show_battery) {
|
||||
uint8_t battPct = powerManager.getBatteryPercent();
|
||||
displayManager.showBatteryIndicator(battPct, sleepS.meta_pos);
|
||||
}
|
||||
displayManager.refresh();
|
||||
powerManager.enterDeepSleep();
|
||||
powerManager.enterDeepSleep(sleepS.interval_min);
|
||||
break;
|
||||
}
|
||||
case ButtonEvent::FactoryReset:
|
||||
|
||||
Reference in New Issue
Block a user