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:
2026-08-03 19:52:22 -04:00
parent fd7643db6a
commit 065d80ca89
20 changed files with 1100 additions and 138 deletions

View File

@@ -22,7 +22,14 @@ void DisplayManager::begin(PowerManager& power) {
// Set rotation for landscape (device physically rotated)
_display->setRotation(3);
Serial.printf("[display] Initialized: %dx%d, rotation=%d\n",
// 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",
_display->width(), _display->height(), _display->getRotation());
}
@@ -235,44 +242,114 @@ void DisplayManager::showMetadataInRegion(const AssetInfo& info, uint8_t metaFla
}
}
void DisplayManager::drawBadge(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
_display->fillRect(x, y, w, h, TFT_BLACK);
}
void DisplayManager::showSleepIndicator(MetaPosition metaPos) {
_power->enableEPDPower();
// Pick a corner opposite to metadata position
static constexpr uint16_t MARGIN = 6;
uint16_t tx, ty;
lgfx::textdatum_t datum;
static constexpr uint16_t MARGIN = 8;
static constexpr uint16_t PAD_X = 6;
static constexpr uint16_t PAD_Y = 4;
_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;
switch (metaPos) {
case MetaPosition::OverlayTopLeft:
case MetaPosition::CaptionTop:
tx = DISPLAY_WIDTH - MARGIN;
ty = DISPLAY_HEIGHT - MARGIN;
datum = bottom_right;
bx = DISPLAY_WIDTH - badgeW - MARGIN;
by = DISPLAY_HEIGHT - badgeH - MARGIN;
break;
case MetaPosition::OverlayTopRight:
tx = MARGIN;
ty = DISPLAY_HEIGHT - MARGIN;
datum = bottom_left;
bx = MARGIN;
by = DISPLAY_HEIGHT - badgeH - MARGIN;
break;
case MetaPosition::OverlayBottomLeft:
tx = DISPLAY_WIDTH - MARGIN;
ty = MARGIN;
datum = top_right;
bx = DISPLAY_WIDTH - badgeW - MARGIN;
by = MARGIN;
break;
case MetaPosition::OverlayBottomRight:
case MetaPosition::CaptionBottom:
default:
tx = MARGIN;
ty = MARGIN;
datum = top_left;
bx = MARGIN;
by = MARGIN;
break;
}
drawBadge(bx, by, badgeW, badgeH);
_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);
_display->setTextSize(1);
_display->setTextColor(TFT_BLACK);
_display->setTextDatum(datum);
_display->drawString("SLP", tx, ty);
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
drawBadge(bx, by, badgeW, badgeH);
// 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);
}
void DisplayManager::refresh() {