Files
MeshCore-Solo/src/helpers/ui/GxEPDDisplay.h
T
JakubandClaude Opus 5 de16dbe32a fix(eink): drive the whole panel on every update, not just the changes
The screen faded as the device was used: text went grey a few updates
after it was drawn, while whatever had just changed stayed crisp. Only a
full refresh brought it back, and the next few updates ate it again.

A partial update is differential -- the controller drives only the pixels
that differ between its "current" (0x24) and "previous" (0x26) RAM and
leaves the rest to hold their own charge, which this panel doesn't do
well. Priming the previous-image RAM with the inverse of the incoming
frame makes every pixel a difference, so all of them get driven to their
target, in both directions.

The inverse matters: priming with flat white makes only white->black a
difference, so ink is re-driven but never erased and every screen ever
shown accumulates as a ghost. Confirmed on hardware, both ways round.

Costs one extra full-screen RAM write (a few ms of SPI). The refresh
itself is unchanged -- the waveform clocks the whole panel regardless of
how many pixels it drives -- so nothing got slower. Clearing ghosts is
still the periodic full refresh's job (Settings > Full rfsh), which can
now stay off.

The helper needs GxEPD2_BW's private _buffer/_page_height, so it goes in
the patched copy of the header, which every e-ink build now includes
unconditionally rather than only screenshot-enabled ones. All e-ink
variants pin GxEPD2 1.6.2, the version that copy tracks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:02:28 +02:00

148 lines
5.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <SPI.h>
#include <Wire.h>
#define ENABLE_GxEPD2_GFX 0
// Always use the patched copy of GxEPD2_BW.h from lib/GxEPD2-patch/src/: it
// adds writeInverseForRedrive() (needed by every e-ink build, see endFrame())
// and, under ENABLE_SCREENSHOT, getBuffer()/getBufferSize(). Both need the
// class's private framebuffer, so they have to live in the header itself. The
// include guard (_GxEPD2_BW_H_) prevents the installed library version from
// being compiled as well; the copy tracks the 1.6.2 pin every e-ink variant's
// lib_deps uses.
#include "../../../lib/GxEPD2-patch/src/GxEPD2_BW.h"
#include <GxEPD2_3C.h>
#include <GxEPD2_4C.h>
#include <GxEPD2_7C.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#include <CRC32.h>
#include "DisplayDriver.h"
#include "MiscFixedFont.h"
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 0
#endif
// Panel native dimensions before rotation. Derived from the model class when
// EINK_DISPLAY_MODEL is set; override with EINK_PANEL_W/H for other panels.
#if defined(EINK_DISPLAY_MODEL)
#ifndef EINK_PANEL_W
#define EINK_PANEL_W EINK_DISPLAY_MODEL::WIDTH
#endif
#ifndef EINK_PANEL_H
#define EINK_PANEL_H EINK_DISPLAY_MODEL::HEIGHT
#endif
#else
#ifndef EINK_PANEL_W
#define EINK_PANEL_W 200
#define EINK_PANEL_H 200
#endif
#endif
// Odd rotations (1, 3) swap width and height.
#define EINK_DISP_W ((DISPLAY_ROTATION & 1) ? EINK_PANEL_H : EINK_PANEL_W)
#define EINK_DISP_H ((DISPLAY_ROTATION & 1) ? EINK_PANEL_W : EINK_PANEL_H)
class GxEPDDisplay : public DisplayDriver {
#if defined(EINK_DISPLAY_MODEL)
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> display;
#else
GxEPD2_BW<GxEPD2_150_BN, 200> display;
#endif
bool _init = false;
bool _isOn = false;
bool _single_font = true; // e-ink is single-font too now (misc-fixed 6x9), matching the OLED driver
uint16_t _curr_color;
CRC32 display_crc;
int last_display_crc_value = 0;
int _text_sz = 1;
uint8_t _full_refresh_interval = 0;
uint8_t _partial_count = 0;
int16_t drawGlyph(int16_t x, int16_t y, uint32_t cp, int sc);
uint8_t glyphXAdvance(uint32_t cp, int sc);
int scale() const { return (width() >= height()) ? 2 : 1; }
public:
#if defined(EINK_DISPLAY_MODEL)
GxEPDDisplay() : DisplayDriver(EINK_DISP_W, EINK_DISP_H),
display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
#else
GxEPDDisplay() : DisplayDriver(EINK_DISP_W, EINK_DISP_H),
display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
#endif
#ifdef ENABLE_SCREENSHOT
const uint8_t* getBuffer() override { return display.getBuffer(); }
uint16_t getBufferSize() override { return display.getBufferSize(); }
uint8_t getDisplayType() override { return 1; } // 1 = e-ink
// Return GxEPD2's own reported dimensions (uses WIDTH_VISIBLE, not the full physical WIDTH
// stored in DisplayDriver). After setRotation(1): width()=HEIGHT, height()=WIDTH_VISIBLE.
int screenshotWidth() override { return (int)display.width(); }
int screenshotHeight() override { return (int)display.height(); }
uint8_t screenshotRotation() override { return (uint8_t)display.getRotation(); }
#endif
// Line height and approx. char width for each font size:
// 1 = FreeSans9pt (lineH=16, charW≈9)
// 2 = FreeSansBold12pt (lineH=20, charW≈12)
// 3 = FreeSans18pt (lineH=28, charW≈17)
// Built-in font scale used for size 4 (huge clock digits): 6×8 cell × 7 = 42×56.
static const int BIG_TEXT_SCALE = 7;
// Size 1 scales with orientation (portrait 1×, landscape 2×); size 2 is always 12×16;
// size 3 is FreeSans18pt (~17×28); size 4 is the built-in font at 7× (~42×56),
// used only for the stacked HH/MM clock on portrait e-ink. Landscape = width >= height.
int getCharWidth() const override {
int sc = scale();
if (_text_sz == 4) return 6 * BIG_TEXT_SCALE;
if (_text_sz == 3) return 17;
if (_text_sz == 2) return 12 * sc;
return 6 * sc; // misc-fixed 6x9 is 6px wide
}
int getLineHeight() const override {
int sc = scale();
if (_text_sz == 4) return 8 * BIG_TEXT_SCALE;
if (_text_sz == 3) return 28;
if (_text_sz == 2) return 16 * sc;
return (_single_font ? 9 : 8) * sc; // misc-fixed 6x9 box height
}
void setSingleFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 6x9
bool isSingleFont() const override { return _single_font; }
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override {
if (_single_font) {
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
} else {
translateUTF8Static(dest, src, dest_size);
}
}
bool begin();
bool isOn() override { return _isOn; }
bool isEink() override { return true; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
uint16_t getCodepointWidth(uint32_t cp) override {
if (_single_font && _text_sz == 1) return glyphXAdvance(cp, scale());
return DisplayDriver::getCodepointWidth(cp);
}
void setDisplayRotation(uint8_t rot) override;
void setFullRefreshInterval(uint8_t n) override { _full_refresh_interval = n; _partial_count = 0; }
void endFrame() override;
};