diff --git a/libs/display/FreeInkDisplay/include/FreeInkDisplay.h b/libs/display/FreeInkDisplay/include/FreeInkDisplay.h index 20388339..85c14486 100644 --- a/libs/display/FreeInkDisplay/include/FreeInkDisplay.h +++ b/libs/display/FreeInkDisplay/include/FreeInkDisplay.h @@ -131,6 +131,8 @@ class FreeInkDisplay { bool supportsBusyGrayscaleStaging() const; void prepareGrayscaleTarget(); bool supportsStripGrayscale() const; + // True only when the runtime-selected panel driver accepts absolute LUT plane encoding. + bool supportsAbsoluteGrayscale() const; // True when displayGrayscaleBase() defers the base activation so the gray // planes join it in one waveform (Paper Mono) - see PanelDriver. bool combinesGrayscaleBase() const; @@ -255,6 +257,7 @@ class FreeInkDisplay { // EXPERIMENTAL: Windowed update - display only a rectangular region void displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen = false); void displayGrayBuffer(bool turnOffScreen = false, const unsigned char* lut = nullptr, bool factoryMode = false); + void displayAbsoluteGrayBuffer(bool turnOffScreen = false); void displayGrayCalibration(uint16_t customX, uint16_t customY, uint16_t customW, uint16_t customH); void refreshDisplay(RefreshMode mode = FAST_REFRESH, bool turnOffScreen = false); diff --git a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp index 5970fd28..5465e251 100644 --- a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp +++ b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp @@ -796,6 +796,19 @@ void FreeInkDisplay::displayGrayBuffer(bool turnOffScreen, const unsigned char* _driver->displayGray(_bus, frameBuffer, turnOffScreen, lut, factoryMode); } +void FreeInkDisplay::displayAbsoluteGrayBuffer(bool turnOffScreen) { +#if defined(SSD1677_PROBE_DEBUG) && SSD1677_PROBE_DEBUG + Serial.printf("[EPD] displayAbsoluteGrayBuffer\n"); +#endif + // Inverted mode deliberately renders a crisp BW page. Writing normal + // grayscale planes afterward would partially undo the output inversion. + if (_inverted) return; + syncPendingAsync(); + _shadowValid = false; + _redRamSynced = false; // grayscale leaves RED holding a gray plane, not the BW baseline + _driver->displayGrayAbsolute(_bus, frameBuffer, turnOffScreen); +} + void FreeInkDisplay::displayGrayCalibration(uint16_t customX, uint16_t customY, uint16_t customW, uint16_t customH) { if (_inverted) return; syncPendingAsync(); @@ -871,6 +884,10 @@ bool FreeInkDisplay::supportsStripGrayscale() const { return !_inverted && _driver && _driver->supportsStripGrayscale(); } +bool FreeInkDisplay::supportsAbsoluteGrayscale() const { + return !_inverted && _driver && _driver->supportsAbsoluteGrayscale(); +} + bool FreeInkDisplay::combinesGrayscaleBase() const { return _driver && _driver->combinesGrayscaleBase(); } void FreeInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { diff --git a/libs/display/FreeInkDisplay/src/driver/PanelDriver.h b/libs/display/FreeInkDisplay/src/driver/PanelDriver.h index 1318b971..986ae3c3 100644 --- a/libs/display/FreeInkDisplay/src/driver/PanelDriver.h +++ b/libs/display/FreeInkDisplay/src/driver/PanelDriver.h @@ -111,6 +111,11 @@ class PanelDriver { // grayscale pass. Drivers with a dedicated grayscale-base waveform must // override this even when their normal display path supports deferral. virtual bool supportsAsyncGrayscaleBase() const { return false; } + + // True when this controller accepts absolute selector planes via displayGrayAbsolute() + // This is a runtime capability because one firmware image may include several drivers + // and driver configurations and select the controller during boot. + virtual bool supportsAbsoluteGrayscale() const { return false; } // True when displayGrayscaleBase() DEFERS the base activation so the gray // planes join it in a single waveform (Paper Mono). Hosts should then route the // grayscale base through displayGrayscaleBase() instead of display(): a @@ -163,6 +168,10 @@ class PanelDriver { (void)factoryMode; display(bus, fb, nullptr, RefreshMode::Fast, turnOff); } + virtual void displayGrayAbsolute(EpdBus& bus, const uint8_t* fb, bool turnOff) + { + displayGray(bus, fb, turnOff, nullptr, false); + } // Diagnostic four-gray comparison. The full frame is first rendered with // the controller's flashing OTP waveform; `custom*` is then rebuilt with the // driver's non-flashing grayscale path. Default drivers keep the OTP frame. diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp index 6041d623..78f51362 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.cpp @@ -65,6 +65,8 @@ const Ssd1677Config& ssd1677DefaultConfig() { 0xC0, // borderWaveformGray: written explicitly with the external AA LUT (vendor // reference stage 2); same value the B/W paths leave in the register, so // the wire state is unchanged — just no longer relying on carry-over + false, // grayPowerUpFirst + true, // enableAbsoluteLut }; return cfg; } @@ -95,6 +97,7 @@ static const Ssd1677Config& ssd1677StickyConfig() { // black under the grayscale LUT (black frame on every AA/cover refresh) true, // grayPowerUpFirst: vendor sequences power down after every refresh, so // settle the rails before the short gray LUT phases (see Ssd1677Config) + true, // enableAbsoluteLut }; return cfg; } @@ -583,11 +586,13 @@ void Ssd1677Driver::displayGray(EpdBus& bus, const uint8_t* fb, bool turnOff, co // ignore RED RAM and break 4-level grayscale. bus.cmd(CMD_DISPLAY_UPDATE_CTRL1); bus.data(CTRL1_NORMAL); + // Keep rails on after the factory gray paint. Running ANALOG_OFF/CLOCK_OFF in + // the same activation can disturb settled gray particles and reintroduce smear. bus.cmd(CMD_DISPLAY_UPDATE_CTRL2); - bus.data(0xC7); // CLOCK_ON|ANALOG_ON|DISPLAY_START|ANALOG_OFF|CLOCK_OFF + bus.data(0xCC); // CLOCK_ON|ANALOG_ON|MODE_SELECT|DISPLAY_START bus.cmd(CMD_MASTER_ACTIVATION); - bus.waitBusy("factory_gray"); - _isScreenOn = false; // 0xC7 always powers down after the update + bus.waitRefreshComplete("factory_gray"); + _isScreenOn = true; } else { // Settled rails before the gray waveform (no-op where the panel is already // on, i.e. the X4's fast path). refresh() then runs the 0xCC external-LUT @@ -599,6 +604,11 @@ void Ssd1677Driver::displayGray(EpdBus& bus, const uint8_t* fb, bool turnOff, co setCustomLut(bus, false, nullptr); } +void Ssd1677Driver::displayGrayAbsolute(EpdBus& bus, const uint8_t* fb, bool turnOff) +{ + displayGray(bus, fb, turnOff, lut_factory_quality, true); +} + void Ssd1677Driver::cleanupGrayscaleBuffers(EpdBus& bus, const uint8_t* bw) { if (!bw) return; setRamArea(bus, 0, 0, _w, _h); diff --git a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h index 8f49284b..a9c42e6a 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h +++ b/libs/display/FreeInkDisplay/src/driver/Ssd1677Driver.h @@ -53,6 +53,7 @@ struct Ssd1677Config { // collapsing toward B/W). The X4 keeps the panel powered between fast // refreshes, so it never needs this and keeps stock behavior. bool grayPowerUpFirst = false; + bool enableAbsoluteLut = false; }; // Standard config (Xteink X4 / GDEQ0426T82). Panel mounting (mirror/180°) is NOT @@ -86,11 +87,13 @@ class Ssd1677Driver : public PanelDriver { bool supportsStripGrayscale() const override { return true; } bool supportsAsyncGrayscaleBase() const override { return true; } + bool supportsAbsoluteGrayscale() const override { return _cfg.enableAbsoluteLut; } void copyGrayscaleLsb(EpdBus& bus, const uint8_t* lsb) override; void copyGrayscaleMsb(EpdBus& bus, const uint8_t* msb) override; void writeGrayscalePlaneStrip(EpdBus& bus, GrayPlane plane, const uint8_t* rows, uint16_t yStart, uint16_t numRows) override; void displayGray(EpdBus& bus, const uint8_t* fb, bool turnOff, const unsigned char* lut, bool factoryMode) override; + void displayGrayAbsolute(EpdBus& bus, const uint8_t* fb, bool turnOff) override; void cleanupGrayscaleBuffers(EpdBus& bus, const uint8_t* bw) override; // No grayscaleRevert override. Grayscale exits via cleanupGrayscaleBuffers