esphome / issues

Issue Tracker for ESPHome
https://esphome.io/
290 stars 36 forks source link

Some Waveshare ePaper Screens are always powered on #4739

Closed phoenixswiss closed 1 year ago

phoenixswiss commented 1 year ago

The problem

The waveshare 7.5v2 displays are always on (not asleep) which is damaging the display over time. I've already lost a driver hat and a waveshare 7.5v2 screen running esphome with this epaper screen type.

If you use a "strong" light source such as a smartphone LED and point it directly at the display (~1cm) after the display has refreshed ~3 seconds ago, it will go dark at this point.

But it shouldn't, because the refresh of the epaper display has already been completed and the pixels shouldn't react to light after this step. I suspected that ESPhome would never put the screen into deep sleep mode. Waveshare warns of display damage if the display is left on for an extended period of time.

https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT_(B)_Manual#ESP32.2F8266:~:text=Note%20that%20the%20screen%20cannot%20be%20powered%20on%20for%20a%20long%20time.%20When%20the%20screen%20is%20not%20refreshed%2C%20please%20set%20the%20screen%20to%20sleep%20mode%20or%20power%20off%20it.%20Otherwise%2C%20the%20screen%20will%20remain%20in%20a%20high%20voltage%20state%20for%20a%20long%20time%2C%20which%20will%20damage%20the%20e%2DPaper%20and%20cannot%20be%20repaired!

As you can see at https://esphome.io/api/waveshare__epaper_8cpp_source.html#l00111:~:text=%C2%A0void%20HOT%20WaveshareEPaper7P5InV2%3A%3Adisplay()%20%7B the waveshare 7.5v2 display will not be powered off (command(0x02)) after refeshing the content.

The display is in power-on mode for the whole time. This will certainly damage the epaper display. I've added the necessary commands to test the deep sleep mode of the display and my smartphone LED check described above does not work anymore (= the screen does not go dark on the pointed spot anymore).

This should be fixed in the code.

    on_state: 
      then:
        - if:
            condition:
              - lambda: 'return !(id(display_update_in_progress) == 1);'
            then:
              - globals.set:
                  id: display_update_in_progress
                  value: "1"
              - logger.log: "Pre display update"
              - display.page.show: overview
              - lambda: |- 
                  id(epaper_display).initialize(); <<<<<<<<<<<<<<< must be called in this test code because the display could be powered off 
              - component.update: epaper_display
              - logger.log: "Post display call, should take 4 seconds now"
              - delay: 4s
              - logger.log: "delay over"
              - lambda: |- 
                  id(epaper_display).command(0x02); <<<<<<<<<<<<<<< will set the display to sleep.
              - logger.log: "After display update"
              - delay: 4s
              - globals.set:
                  id: display_update_in_progress
                  value: "0"
[...]

Which version of ESPHome has the issue?

<= 2023.7.0

What type of installation are you using?

Home Assistant Add-on

Which version of Home Assistant has the issue?

2023.7.3

What platform are you using?

ESP32

Board

Lolin D32 Pro

Component causing the issue

ESPHome display component

Example YAML snippet

spi:
  clk_pin: 18
  mosi_pin: 23
  miso_pin: 19

display:
  - platform: waveshare_epaper
    id: epaper_display
    reset_duration: 2ms
    setup_priority: 1000
    cs_pin: 5
    dc_pin: 0
    busy_pin: 15
    reset_pin: 2
    model: 7.50inv2
    update_interval: never 

    pages:
      - id: overview
        lambda: |-
          auto time = id(sntp_time).now();

          // Top status header
          it.print(6, 4, id(font_11pt), "Home Assistant");
          it.printf(95, 4, id(font_11pt), "(ESPhome version: %s | SSID: %s)",id(version_id).state.c_str(),id(ssid_id).state.c_str());
          it.print(680, 4, id(font_11pt), "ref.:");
          it.strftime(705, 4, id(font_11pt), "%d.%m.%Y %H:%M", id(sntp_time).now());
          it.line(2, 16, 798, 16);
          it.line(2, 17, 798, 17);

...

  - platform: homeassistant
    id: esp_waterme_s210000001_moisture
    name: "esp_waterme_s210000001_moisture"
    entity_id: sensor.esp_waterme_s210000001_moisture
    accuracy_decimals: 1
    on_value: 
      then:
         - component.update: epaper_display

Anything in the logs that might be useful for us?

No response

Additional information

No response

wtremmel commented 1 year ago

So you mean simply changing that function you highlighted should solve this? I did and my display still works :-) not sure if the problem is gone....

void HOT WaveshareEPaper7P5InV2::display() {
  uint32_t buf_len = this->get_buffer_length_();
  // COMMAND DATA START TRANSMISSION NEW DATA
  this->command(0x13);
  delay(2);
  for (uint32_t i = 0; i < buf_len; i++) {
    this->data(~(this->buffer_[i]));
  }

  // COMMAND DISPLAY REFRESH
  this->command(0x12);
  delay(100);  // NOLINT
  this->wait_until_idle_();
  this->command(0x02);
}
phoenixswiss commented 1 year ago

@wtremmel: Did you build a custom component with the changes, or how did you modify the function?

Update: I've changed the file in the running docker container.

phoenixswiss commented 1 year ago

@wtremmel: Unfortunately you can't just add the line this->command(0x02); to the display function. You have to modify it quite a bit. Here is the updated version of the code that works with the waveshare 7.5v2 now:

void WaveshareEPaper7P5InV2::initialize() {

  // COMMAND POWER SETTING
  this->command(0x01);
  this->data(0x07);
  this->data(0x07);
  this->data(0x3f);
  this->data(0x3f);

  //we don't want the display to be powered on at this point
  //this->command(0x04); 

  delay(100);  // NOLINT
  this->wait_until_idle_();

  // COMMAND VCOM AND DATA INTERVAL SETTING
  this->command(0x50);
  this->data(0x10);
  this->data(0x07);

  // COMMAND TCON SETTING
  this->command(0x60);
  this->data(0x22);

  // COMMAND PANEL SETTING
  this->command(0x00);
  this->data(0x1F);

  // COMMAND RESOLUTION SETTING
  this->command(0x61);
  this->data(0x03);
  this->data(0x20);
  this->data(0x01);
  this->data(0xE0);

  // COMMAND DUAL SPI MM_EN, DUSPI_EN
  this->command(0x15);
  this->data(0x00);

  // COMMAND POWER DOWN DRIVER HAT 
  // This command will turn off booster, controller, source driver, gate driver, VCOM, and 
  // temperature sensor, but register data will be kept until VDD turned OFF or Deep Sleep Mode. 
  // Source/Gate/Border/VCOM will be released to floating.
  this->command(0x02);
}
void HOT WaveshareEPaper7P5InV2::display() {
  uint32_t buf_len = this->get_buffer_length_();

  // COMMAND POWER ON
  ESP_LOGI(TAG, "Power on the display and hat");

  // This command will turn on booster, controller, regulators, and temperature sensor will be 
  // activated for one-time sensing before enabling booster. When all voltages are ready, the 
  // BUSY_N signal will return to high.
  this->command(0x04);
  delay(200);  // NOLINT
  this->wait_until_idle_();

  // COMMAND DATA START TRANSMISSION NEW DATA
  this->command(0x13);
  delay(2);
  for (uint32_t i = 0; i < buf_len; i++) {
    this->data(~(this->buffer_[i]));
  }

  delay(100);  // NOLINT
  this->wait_until_idle_();

  // COMMAND DISPLAY REFRESH
  this->command(0x12);
  delay(100);  // NOLINT
  this->wait_until_idle_();

  // we have to wait and feed the watchdog because wait_until_idle_() will exit
  // before the screen refresh has been completed. If we don't busy waiting here for at least 4 sec.
  // the power off command doesn't work.
  arch_feed_wdt();
  delay(3000);  // NOLINT
  arch_feed_wdt();
  delay(3000);  // NOLINT
  arch_feed_wdt();
  ESP_LOGI(TAG, "Before command(0x02) (>> power off)");
  this->command(0x02);
  this->wait_until_idle_();
  ESP_LOGI(TAG, "After command(0x02) (>> power off)");
}

Use a 'strong' light source such as a smartphone LED and point it directly at the display (~1cm) BEFORE changing the code. It should go dark at the point you pointed the smartphone LED at.

After applying the code above, the display shouldn't react to a strong light source.

wtremmel commented 1 year ago

Still reacts to light. This is what I see in the log:

[20:47:10][I][waveshare_epaper:1460]: Power on the display and hat
[20:47:11][E][waveshare_epaper:119]: Timeout while displaying image!
[20:47:13][E][waveshare_epaper:119]: Timeout while displaying image!
[20:47:17][I][waveshare_epaper:1492]: Before command(0x02) (>> power off)
[20:47:17][I][waveshare_epaper:1495]: After command(0x02) (>> power off)
[20:47:17][W][component:204]: Component waveshare_epaper.display took a long time for an operation (7.30 s).
[20:47:17][W][component:205]: Components should block for at most 20-30ms.
phoenixswiss commented 1 year ago

@wtremmel please increase the delay with an additional busy wait block. Don't increase the already defined delays, otherwise the watchdog will be triggered.

I had the same issue. 6000ms was ok for my setup.


delay(3000);  // NOLINT
arch_feed_wdt();

   // we have to wait and feed the watchdog because wait_until_idle_() will exit
  // before the screen refresh has been completed. If we don't busy waiting here for at least 4 sec.
  // the power off command doesn't work.
  arch_feed_wdt();
  delay(3000);  // NOLINT
  arch_feed_wdt();
  delay(3000);  // NOLINT
  arch_feed_wdt();

  ++++
  delay(3000);  // NOLINT
  arch_feed_wdt();
DAVe3283 commented 1 year ago

Shouldn't this be done with callbacks, not a whole bunch of waiting? Keeping the processor tied up in one component for multiple seconds will prevent latency-sensitive stuff (e.g. audio, Bluetooth) from working.

phoenixswiss commented 1 year ago

Absolutely, busy waiting is bad practice, but at this stage my changes above are just hot fixes to keep my hardware from dying...

wtremmel commented 1 year ago

I confirm it works and is no longer sensitive to light. Now - how to proceed? Any chance to implement this "properly" so it will get accepted as a pull request?

From the log:

[10:13:30][I][waveshare_epaper:1460]: Power on the display and hat
[10:13:31][E][waveshare_epaper:119]: Timeout while displaying image!
[10:13:33][E][waveshare_epaper:119]: Timeout while displaying image!
[10:13:42][I][waveshare_epaper:1494]: Before command(0x02) (>> power off)
[10:13:42][I][waveshare_epaper:1497]: After command(0x02) (>> power off)
[10:13:42][W][component:204]: Component waveshare_epaper.display took a long time for an operation (12.31 s).
[10:13:42][W][component:205]: Components should block for at most 20-30ms.
DAVe3283 commented 1 year ago

If the waveshare_epaper component is already doing a bunch of waiting, it might get accepted with one more sleep() call, especially with a note this is to prevent wear/damage to the display. I don't see any listed codeowners for that component we can ask, and most of the history is one-off contributors.

I guess if either of you want to take a crack at converting it to use callbacks that would be the best way forward. I don't have a waveshare (or any other e-ink) to be able to test with, and all the components I maintain are fast enough I have never needed to do a callback before. I probably won't be able to help much here, sorry.

wtremmel commented 1 year ago

If you could point me to same example code I might give it a try. Disclaimer: I am not a developer, I know how to code, but thats all :-)

phoenixswiss commented 1 year ago

I assume we can't use deferred callbacks here because there is no state which will trigger the callback. It's like fire and forget. The method this->wait_untilidle() doesn't really work on the 7.5v2 screens. This is some sort of a blocking callback.

The callback would only work if the display actually informs the system when its done refreshing its content.

According to the specifications (https://www.waveshare.com/w/upload/2/29/7.5inch_e-paper-b-Specification.pdf on page 14 After Display Refresh command, BUSY signal will become “0” until display update is finished.), the busy pin should be 1 if the refresh has been completed. This doesn't work. The display is still refreshing when the busy pin is 0. Inverting the busy pin doesn't solve the issue.

Update: I will modify the this->wait_untilidle() to check something. One moment please ;-)

phoenixswiss commented 1 year ago

So I've found some additional problems in the ESPhome code/documentation.

First of all, the busy pin on the 7.5v2 screens needs to be inverted. It was only mentioned for the gdew0154m09 displays in the ESPhome documentation https://esphome.io/components/display/waveshare_epaper.html#:~:text=Warning,in%20the%20config.

Second, I've increased the timeouts in the wait_untilidle() function to 10000ms. In my setup it takes about 6400ms for the refresh process.

display:
  - platform: waveshare_epaper
    id: epaper_display
    reset_duration: 2ms
    setup_priority: 1000
    cs_pin: 5
    dc_pin: 0
    busy_pin: 
      number: 15
      inverted: True
    reset_pin: 2
    model: 7.50inv2
    update_interval: never 

bool WaveshareEPaper::wait_until_idle_() {
  if (this->busy_pin_ == nullptr) {
    return true;
  }

  const uint32_t start = millis();
  while (this->busy_pin_->digital_read()) {
    this->command(0x71);
    if (millis() - start > 10000) { // hardcoded 10000ms here
      ESP_LOGE(TAG, "Timeout while displaying image!");
      return false;
    }
    App.feed_wdt(); // keep the watchdog happy
    delay(10);
  }
  return true;
}

void WaveshareEPaper7P5InV2::initialize() {

  // COMMAND POWER SETTING
  this->command(0x01);
  this->data(0x07);
  this->data(0x07);
  this->data(0x3f);
  this->data(0x3f);

  //we don't want the display to be powered at this point
  //this->command(0x04); 

  delay(100);  // NOLINT
  this->wait_until_idle_();

  // COMMAND VCOM AND DATA INTERVAL SETTING
  this->command(0x50);
  this->data(0x10);
  this->data(0x07);

  // COMMAND TCON SETTING
  this->command(0x60);
  this->data(0x22);

  // COMMAND PANEL SETTING
  this->command(0x00);
  this->data(0x1F);

  // COMMAND RESOLUTION SETTING
  this->command(0x61);
  this->data(0x03);
  this->data(0x20);
  this->data(0x01);
  this->data(0xE0);

  // COMMAND DUAL SPI MM_EN, DUSPI_EN
  this->command(0x15);
  this->data(0x00);

  // COMMAND POWER DRIVER HAT DOWN
  // This command will turn off booster, controller, source driver, gate driver, VCOM, and 
  // temperature sensor, but register data will be kept until VDD turned OFF or Deep Sleep Mode. 
  // Source/Gate/Border/VCOM will be released to floating.
  this->command(0x02);
}

void HOT WaveshareEPaper7P5InV2::display() {
  uint32_t buf_len = this->get_buffer_length_();

  // COMMAND POWER ON
  ESP_LOGI(TAG, "Power on the display and hat");

  // This command will turn on booster, controller, regulators, and temperature sensor will be 
  // activated for one-time sensing before enabling booster. When all voltages are ready, the 
  // BUSY_N signal will return to high.
  this->command(0x04);
  delay(200);  // NOLINT
  this->wait_until_idle_();

  // COMMAND DATA START TRANSMISSION NEW DATA
  this->command(0x13);
  delay(2);
  for (uint32_t i = 0; i < buf_len; i++) {
    this->data(~(this->buffer_[i]));
  }

  delay(100);  // NOLINT
  this->wait_until_idle_();

  // COMMAND DISPLAY REFRESH
  this->command(0x12);
  delay(100);  // NOLINT
  this->wait_until_idle_();

  ESP_LOGI(TAG, "Before command(0x02) (>> power off)");
  this->command(0x02);
  this->wait_until_idle_();
  ESP_LOGI(TAG, "After command(0x02) (>> power off)");
}

@wtremmel Could you please check this new modified code?

With this part fixed, we can now implement a proper deferred callback function. How have you implemented this in your code @DAVe3283?

wtremmel commented 1 year ago

Nope, does not work - it powers on the display and then it resets:

[19:57:09][D][main:650]: Display regular change triggered
[19:57:09][I][waveshare_epaper:1460]: Power on the display and hat
WARNING wemos-s3-pro-test.local: Connection error occurred: [Errno 104] Connection reset by peer
INFO Processing unexpected disconnect from ESPHome API for wemos-s3-pro-test.local
WARNING Disconnected from API
INFO Successfully connected to wemos-s3-pro-test.local

I will double check if I made a mistake somewhere....

Update: ok, it kind of works that it is powered down (I had by mistake the 7.50inV2alt driver configured), but it still disconnects from the API every time the display refreshes

Update 2: made a mistake in the idle function - forgot the App.feed_wdt(); // keep the watchdog happy statement

wtremmel commented 1 year ago

Ok, forget my last comment. It has been a long day. The code works.

[20:15:57][D][main:650]: Display regular change triggered
[20:15:57][I][waveshare_epaper:1461]: Power on the display and hat
[20:16:03][I][waveshare_epaper:1485]: Before command(0x02) (>> power off)
[20:16:03][I][waveshare_epaper:1488]: After command(0x02) (>> power off)
[20:16:03][W][component:204]: Component interval took a long time for an operation (5.80 s).
[20:16:03][W][component:205]: Components should block for at most 20-30ms.
DAVe3283 commented 1 year ago

I have never personally implemented callbacks, but it looks like the way to do this is with timeouts, at least from looking at the following files:

It would be a bit of a refactor, but using timeouts with callbacks and some global state to ensure there are no race conditions should make it a lot more efficient.

prusayn commented 1 year ago

I noticed the same issue with my 7.5inch e-Paper (G), which is using 7.50inV2alt model in ESPhome. It's quite difficult to replace code when using Home Assistant OS. Maybe custom component would be an acceptable workaround.

wtremmel commented 1 year ago

PR#5239 does not work. I tried using it as external component, and it brings my display into a boot loop where it does not even connect to the network. The display changes a few times from black to white, then stays white and is sensitive to light.

external_components:
  - source: github://pr#5239
    components: [ waveshare_epaper ]

...
display:
  - platform: waveshare_epaper
    id: epaper
    cs_pin: GPIO17
    dc_pin: GPIO16
    reset_pin: GPIO15
    busy_pin: 
      number: GPIO18
      inverted: true
    reset_duration: 2ms
    model: 7.50in-bV3 
phoenixswiss commented 1 year ago

@wtremmel Is the 7.50in-bV3 not for the v3 HARDWARE version of the 7.5in screen?

@lucasprim I've just implemented a new local variant of the 7.50in-bV2 (like the -alt version) which overrides all the default functions, especially the wait_until_idle function.

The code below works fine and fixes the always powered on issue, as it is essentially the same code as the hotfix I created a few weeks ago.

display.py

++ WaveshareEPaper7P5InV2Fixed = waveshare_epaper_ns.class_(
    "WaveshareEPaper7P5InV2Fixed", WaveshareEPaper
)

MODELS = {
++  "7.50inv2Fixed": ("b", WaveshareEPaper7P5InV2Fixed),
}

waveshare_epaper.cpp

++

/* 7.50inV2Fixed */
bool WaveshareEPaper7P5InV2Fixed::wait_until_idle_() {
  if (this->busy_pin_ == nullptr) {
    return true;
  }

  const uint32_t start = millis();
  while (this->busy_pin_->digital_read()) {
    this->command(0x71);
    if (millis() - start > 10000) {
      ESP_LOGE(TAG, "Timeout while displaying image!");
      return false;
    }
    App.feed_wdt();
    delay(10);
  }
  return true;
}
void WaveshareEPaper7P5InV2Fixed::initialize() {

  // COMMAND POWER SETTING
  this->command(0x01);
  this->data(0x07);
  this->data(0x07);
  this->data(0x3f);
  this->data(0x3f);

  //we don't want the display to be powered at this point
  //this->command(0x04); 

  delay(100);  // NOLINT
  this->wait_until_idle_();

  // COMMAND VCOM AND DATA INTERVAL SETTING
  this->command(0x50);
  this->data(0x10);
  this->data(0x07);

  // COMMAND TCON SETTING
  this->command(0x60);
  this->data(0x22);

  // COMMAND PANEL SETTING
  this->command(0x00);
  this->data(0x1F);

  // COMMAND RESOLUTION SETTING
  this->command(0x61);
  this->data(0x03);
  this->data(0x20);
  this->data(0x01);
  this->data(0xE0);

  // COMMAND DUAL SPI MM_EN, DUSPI_EN
  this->command(0x15);
  this->data(0x00);

  // COMMAND POWER DRIVER HAT DOWN
  // This command will turn off booster, controller, source driver, gate driver, VCOM, and 
  // temperature sensor, but register data will be kept until VDD turned OFF or Deep Sleep Mode. 
  // Source/Gate/Border/VCOM will be released to floating.
  this->command(0x02);
}

void HOT WaveshareEPaper7P5InV2Fixed::display() {
  uint32_t buf_len = this->get_buffer_length_();

  // COMMAND POWER ON
  ESP_LOGI(TAG, "Power on the display and hat");

  // This command will turn on booster, controller, regulators, and temperature sensor will be 
  // activated for one-time sensing before enabling booster. When all voltages are ready, the 
  // BUSY_N signal will return to high.
  this->command(0x04);
  delay(200);  // NOLINT
  this->wait_until_idle_();

  // COMMAND DATA START TRANSMISSION NEW DATA
  this->command(0x13);
  delay(2);
  for (uint32_t i = 0; i < buf_len; i++) {
    this->data(~(this->buffer_[i]));
  }

  delay(100);  // NOLINT
  this->wait_until_idle_();

  // COMMAND DISPLAY REFRESH
  this->command(0x12);
  delay(100);  // NOLINT
  this->wait_until_idle_();

  ESP_LOGI(TAG, "Before command(0x02) (>> power off)");
  this->command(0x02);
  this->wait_until_idle_();
  ESP_LOGI(TAG, "After command(0x02) (>> power off)");
}

int WaveshareEPaper7P5InV2Fixed::get_width_internal() { return 800; }
int WaveshareEPaper7P5InV2Fixed::get_height_internal() { return 480; }
void WaveshareEPaper7P5InV2Fixed::dump_config() {
  LOG_DISPLAY("", "Waveshare E-Paper", this);
  ESP_LOGCONFIG(TAG, "  Model: 7.5inV2Fixed");
  LOG_PIN("  Reset Pin: ", this->reset_pin_);
  LOG_PIN("  DC Pin: ", this->dc_pin_);
  LOG_PIN("  Busy Pin: ", this->busy_pin_);
  LOG_UPDATE_INTERVAL(this);
}

waveshare_epaper.h

++

class WaveshareEPaper7P5InV2Fixed : public WaveshareEPaper {
 public:
  bool wait_until_idle_();

  void initialize() override;

  void display() override;

  void dump_config() override;

 protected:
  int get_width_internal() override;

  int get_height_internal() override;
};

esphome device yaml

THE BUSY PIN MUST BE INVERTED ON 7.50inv2 ePaper Screens!

display:
  - platform: waveshare_epaper
    id: epaper_display
    reset_duration: 2ms
    setup_priority: 1000
    cs_pin: 5
    dc_pin: 0
    busy_pin: 
      number: 15
      inverted: True
    reset_pin: 2
    model: 7.50inv2Fixed
    update_interval: never 
wtremmel commented 1 year ago

@wtremmel Is the 7.50in-bV3 not for the v3 HARDWARE version of the 7.5in screen?

the -bV3 driver worked with my display (as does the 7.50inv2 and 7.50inV2alt) of course with constantly powered. The PR completely broke everything - like I said the display flickered a few times and then nothing, not even wifi.

If you put your WaveshareEPaper7P5InV2Fixed into a PR I am happy to test it.

phoenixswiss commented 1 year ago

@wtremmel I've never done a PR request for esphome before. Would you mind to test it before I open a PR? You can find the modified changes here https://github.com/phoenixswiss/esphome

wtremmel commented 1 year ago

Testet it. Works. Here is my config for reference:

external_components:
  - source:
      type: git
      url: https://github.com/phoenixswiss/esphome
      ref: dev
    components: [ waveshare_epaper ]
...
display:
  - platform: waveshare_epaper
    id: epaper
    cs_pin: GPIO17
    dc_pin: GPIO16
    reset_pin: GPIO15
    busy_pin: 
      number: GPIO18
      inverted: true
    reset_duration: 2ms
    model: 7.50inv2fixed
    update_interval: never
    rotation: 0
    pages:
...

btw - my full config can be found here: https://github.com/wtremmel/ch5-esphome/blob/main/wemos-s3-pro-test.yaml