mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[epaper_spi] Abandon an update when the busy pin never clears (#20555)
This commit is contained in:
@@ -131,9 +131,7 @@ void EPaperBase::update() {
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
#endif
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
@@ -156,6 +154,10 @@ void EPaperBase::loop() {
|
||||
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
|
||||
#endif
|
||||
} else {
|
||||
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
|
||||
this->abandon_update_();
|
||||
return;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (now - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
|
||||
@@ -168,6 +170,20 @@ void EPaperBase::loop() {
|
||||
this->process_state_();
|
||||
}
|
||||
|
||||
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
|
||||
void EPaperBase::abandon_update_() {
|
||||
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
|
||||
this->epaper_state_to_string_());
|
||||
this->status_set_warning(LOG_STR("Busy pin timeout"));
|
||||
if (this->state_ == EPaperState::TRANSFER_DATA)
|
||||
this->abort_transfer();
|
||||
// Leave the panel powered down rather than with its high-voltage drivers enabled
|
||||
this->power_off();
|
||||
this->full_update_requested_ = true;
|
||||
this->current_data_index_ = 0;
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the state machine.
|
||||
* Typical state sequence:
|
||||
@@ -242,6 +258,7 @@ void EPaperBase::process_state_() {
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->status_clear_warning();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
|
||||
break;
|
||||
|
||||
@@ -127,6 +127,7 @@ class EPaperBase : public Display,
|
||||
virtual bool initialise(bool partial);
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
void abandon_update_();
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
@@ -143,6 +144,8 @@ class EPaperBase : public Display,
|
||||
* @return true if done, false if it should be called next loop
|
||||
*/
|
||||
virtual bool transfer_data() = 0;
|
||||
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
|
||||
virtual void abort_transfer() {}
|
||||
/**
|
||||
* Refresh the screen after data transfer
|
||||
*/
|
||||
@@ -193,18 +196,20 @@ class EPaperBase : public Display,
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
|
||||
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t waiting_for_idle_last_print_{};
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#endif
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
uint32_t update_start_time_{};
|
||||
#endif
|
||||
|
||||
// properties with specific initialisers go last
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
uint32_t reset_duration_{10};
|
||||
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
|
||||
uint16_t busy_timeout_seconds_{120};
|
||||
uint8_t full_update_every_{1};
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -364,6 +364,15 @@ bool HOT EPaperT133A01::transfer_data() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperT133A01::abort_transfer() {
|
||||
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
|
||||
if (this->current_data_index_ == 0)
|
||||
return;
|
||||
this->disable();
|
||||
this->cs_pin_->digital_write(true);
|
||||
this->cs1_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
void EPaperT133A01::dump_config() {
|
||||
EPaperBase::dump_config();
|
||||
LOG_PIN(" CS Pin: ", this->cs_pin_);
|
||||
|
||||
@@ -46,6 +46,7 @@ class EPaperT133A01 : public EPaperBase {
|
||||
void deep_sleep() override;
|
||||
|
||||
bool transfer_data() override;
|
||||
void abort_transfer() override;
|
||||
|
||||
/**
|
||||
* Send a command (and optional data) selecting one or both controllers.
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "../common.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_ssd1683.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_t133a01.h"
|
||||
|
||||
namespace esphome::epaper_spi::testing {
|
||||
|
||||
/// A busy pin the test can hold at "busy" and release.
|
||||
class SwitchableBusyPin : public RecordingPin {
|
||||
public:
|
||||
bool digital_read() override { return this->busy; }
|
||||
|
||||
bool busy{false};
|
||||
};
|
||||
|
||||
template<typename T> void loop_until_idle(T &display) {
|
||||
const uint32_t deadline = millis() + 1000;
|
||||
while (display.state() != EPaperState::IDLE && millis() < deadline)
|
||||
display.loop();
|
||||
}
|
||||
|
||||
class TestableSSD1683 : public EPaperSSD1683 {
|
||||
public:
|
||||
TestableSSD1683() : EPaperSSD1683("test", 16, 2, nullptr, 0) {}
|
||||
|
||||
void install(spi::SPIDelegate *delegate) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(&this->dc);
|
||||
this->set_busy_pin(&this->busy);
|
||||
this->set_full_update_every(5);
|
||||
this->set_auto_clear(false);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
this->busy_timeout_seconds_ = 0; // abandon as soon as the stuck pin is seen, so the test does not wait
|
||||
}
|
||||
|
||||
void draw_whole_panel() {
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
EPaperState state() const { return this->state_; }
|
||||
uint8_t update_count() const { return this->update_count_; }
|
||||
void set_update_count(uint8_t count) { this->update_count_ = count; }
|
||||
|
||||
RecordingPin dc;
|
||||
SwitchableBusyPin busy;
|
||||
};
|
||||
|
||||
/// If the busy pin never clears, the update is abandoned instead of waiting for ever: the display goes back to
|
||||
/// idle with a warning. Once the pin works again the next update is accepted and is a full one, even though nothing
|
||||
/// was drawn since, and completing it clears the warning.
|
||||
TEST(EPaperBusyTimeout, StuckBusyPinAbandonsTheUpdateAndRecovers) {
|
||||
TestableSSD1683 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus);
|
||||
display.set_update_count(3);
|
||||
|
||||
// Stick the pin after the data has been sent, when the dirty area has already been emptied
|
||||
display.draw_whole_panel();
|
||||
display.update();
|
||||
const uint32_t deadline = millis() + 1000;
|
||||
while (display.state() <= EPaperState::TRANSFER_DATA && millis() < deadline)
|
||||
display.loop();
|
||||
ASSERT_EQ(display.state(), EPaperState::POWER_ON);
|
||||
display.busy.busy = true;
|
||||
loop_until_idle(display);
|
||||
|
||||
EXPECT_EQ(display.state(), EPaperState::IDLE) << "the update was not abandoned";
|
||||
EXPECT_TRUE(display.status_has_warning());
|
||||
|
||||
display.busy.busy = false;
|
||||
display.update();
|
||||
EXPECT_NE(display.state(), EPaperState::IDLE) << "a later update was refused";
|
||||
loop_until_idle(display);
|
||||
|
||||
EXPECT_EQ(display.state(), EPaperState::IDLE) << "the later update did not complete";
|
||||
EXPECT_EQ(display.update_count(), 1) << "the later update was not a full one";
|
||||
EXPECT_FALSE(display.status_has_warning());
|
||||
}
|
||||
|
||||
class TestableT133A01 : public EPaperT133A01 {
|
||||
public:
|
||||
TestableT133A01() : EPaperT133A01("test", 8, 2, nullptr, 0) {}
|
||||
|
||||
void install(spi::SPIDelegate *delegate) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(&this->dc);
|
||||
this->set_cs_pins(&this->cs, &this->cs1);
|
||||
this->set_busy_pin(&this->busy);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
this->busy_timeout_seconds_ = 0; // abandon as soon as the stuck pin is seen, so the test does not wait
|
||||
}
|
||||
|
||||
void start_transfer() { this->set_state_(EPaperState::TRANSFER_DATA); }
|
||||
EPaperState state() const { return this->state_; }
|
||||
|
||||
RecordingPin dc, cs, cs1;
|
||||
SwitchableBusyPin busy;
|
||||
};
|
||||
|
||||
/// The T133A01 waits for the busy pin with a data stream open. Abandoning the update there must close the SPI
|
||||
/// transaction and release both chip selects, or the next command would block.
|
||||
TEST(EPaperBusyTimeout, AbandonedT133A01TransferReleasesTheBus) {
|
||||
TestableT133A01 display;
|
||||
TimedSPIDelegate delegate(MAX_TRANSFER_TIME + 5);
|
||||
display.install(&delegate);
|
||||
|
||||
display.start_transfer();
|
||||
display.loop();
|
||||
ASSERT_EQ(display.state(), EPaperState::TRANSFER_DATA);
|
||||
ASSERT_FALSE(display.cs.level) << "the transfer should have yielded with its data stream open";
|
||||
|
||||
display.busy.busy = true;
|
||||
loop_until_idle(display);
|
||||
|
||||
ASSERT_EQ(display.state(), EPaperState::IDLE) << "the update was not abandoned";
|
||||
EXPECT_EQ(delegate.begin_count, delegate.end_count) << "SPI transaction left open";
|
||||
EXPECT_TRUE(display.cs.level) << "CS left asserted";
|
||||
EXPECT_TRUE(display.cs1.level) << "CS1 left asserted";
|
||||
}
|
||||
|
||||
/// Abandoning an update powers the panel off, so it is not left with its high-voltage drivers enabled.
|
||||
TEST(EPaperBusyTimeout, AbandonedUpdatePowersThePanelOff) {
|
||||
TestableT133A01 display;
|
||||
RecordingDelegate bus(&display.dc, MAX_TRANSFER_TIME + 5); // burn time so the transfer yields mid-stream
|
||||
display.install(&bus);
|
||||
|
||||
display.start_transfer();
|
||||
display.loop();
|
||||
ASSERT_EQ(display.state(), EPaperState::TRANSFER_DATA);
|
||||
|
||||
display.busy.busy = true;
|
||||
loop_until_idle(display);
|
||||
ASSERT_EQ(display.state(), EPaperState::IDLE) << "the update was not abandoned";
|
||||
|
||||
EXPECT_NE(std::find(bus.commands.begin(), bus.commands.end(), 0x02), bus.commands.end())
|
||||
<< "the panel was not powered off (POF) when the update was abandoned";
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
Reference in New Issue
Block a user