mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 04:02:54 +00:00
[epaper_spi] Run the loop continuously between the steps of an update (#20554)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
co-authored by
clydebarrow
parent
1a28a9220b
commit
818d43f742
@@ -133,7 +133,9 @@ void EPaperBase::update() {
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
// The wait starts when the step's delay ends, or now for a wait in the middle of a step
|
||||
const uint32_t now = millis();
|
||||
this->waiting_for_idle_start_ = static_cast<int32_t>(this->delay_until_ - now) > 0 ? this->delay_until_ : now;
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
@@ -147,19 +149,30 @@ void EPaperBase::loop() {
|
||||
auto now = millis();
|
||||
// using modulus arithmetic to handle wrap-around
|
||||
int diff = now - this->delay_until_;
|
||||
if (diff < 0)
|
||||
if (diff < 0) {
|
||||
// Running the loop continuously cannot shorten a delay longer than one loop interval
|
||||
if (-diff > static_cast<int>(App.get_loop_interval())) {
|
||||
this->high_frequency_loop_.stop();
|
||||
} else {
|
||||
this->high_frequency_loop_.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this->waiting_for_idle_) {
|
||||
if (this->is_idle_()) {
|
||||
this->waiting_for_idle_ = false;
|
||||
this->high_frequency_loop_.start();
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
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) {
|
||||
const uint32_t busy_for = now - this->waiting_for_idle_start_;
|
||||
if (busy_for > this->busy_timeout_seconds_ * 1000u) {
|
||||
this->abandon_update_();
|
||||
return;
|
||||
}
|
||||
if (busy_for > FAST_BUSY_POLL_MS)
|
||||
this->high_frequency_loop_.stop();
|
||||
#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_());
|
||||
@@ -170,6 +183,8 @@ void EPaperBase::loop() {
|
||||
}
|
||||
}
|
||||
this->process_state_();
|
||||
if (this->is_failed())
|
||||
this->high_frequency_loop_.stop();
|
||||
}
|
||||
|
||||
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
|
||||
@@ -275,16 +290,20 @@ void EPaperBase::process_state_() {
|
||||
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
|
||||
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
|
||||
this->state_ = state;
|
||||
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
|
||||
// allow subclasses to nominate delays
|
||||
if (delay == 0)
|
||||
delay = this->next_delay_;
|
||||
this->next_delay_ = 0;
|
||||
this->delay_until_ = millis() + delay;
|
||||
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
|
||||
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
|
||||
TRUEFALSE(this->waiting_for_idle_));
|
||||
// Each step of an update otherwise waits for the next loop iteration; loop() slows down again for long waits
|
||||
if (state == EPaperState::IDLE) {
|
||||
this->high_frequency_loop_.stop();
|
||||
this->disable_loop();
|
||||
} else {
|
||||
this->high_frequency_loop_.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/split_buffer/split_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
using namespace display;
|
||||
@@ -31,6 +32,8 @@ static constexpr uint8_t SWAP_XY = 4;
|
||||
|
||||
static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
|
||||
static constexpr size_t MAX_TRANSFER_SIZE = 128;
|
||||
// Longer busy waits are the waveform or a stuck pin, and are polled at the normal loop pace
|
||||
static constexpr uint32_t FAST_BUSY_POLL_MS = 50;
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
|
||||
class EPaperBase : public Display,
|
||||
@@ -202,6 +205,7 @@ class EPaperBase : public Display,
|
||||
GPIOPin *reset_pin_{};
|
||||
std::vector<GPIOPin *> enable_pins_{};
|
||||
bool waiting_for_idle_{};
|
||||
HighFrequencyLoopRequester high_frequency_loop_;
|
||||
uint32_t delay_until_{}; // timestamp until which to delay processing
|
||||
uint16_t next_delay_{}; // milliseconds to delay before next state
|
||||
uint8_t transform_{};
|
||||
|
||||
@@ -50,6 +50,14 @@ class RecordingPin : public GPIOPin {
|
||||
bool level{true};
|
||||
};
|
||||
|
||||
/// 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};
|
||||
};
|
||||
|
||||
/// SPI delegate that records what reaches the bus, filing each payload under the command it
|
||||
/// followed; commands are the bytes written while D/C is low. Optionally burns wall-clock time on
|
||||
/// each data row, so a transfer can be driven past its yield deadline.
|
||||
|
||||
@@ -7,14 +7,8 @@
|
||||
#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};
|
||||
};
|
||||
// Other test files define fixtures with the same names; keep these to this file
|
||||
namespace {
|
||||
|
||||
template<typename T> void loop_until_idle(T &display) {
|
||||
const uint32_t deadline = millis() + 1000;
|
||||
@@ -142,4 +136,5 @@ TEST(EPaperBusyTimeout, AbandonedUpdatePowersThePanelOff) {
|
||||
<< "the panel was not powered off (POF) when the update was abandoned";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#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 {
|
||||
// Other test files define fixtures with the same names; keep these to this file
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t MALFORMED_INIT_SEQUENCE[] = {0x01};
|
||||
|
||||
class TestableSSD1683 : public EPaperSSD1683 {
|
||||
public:
|
||||
explicit TestableSSD1683(const uint8_t *init_sequence = nullptr, size_t init_sequence_length = 0)
|
||||
: EPaperSSD1683("test", 16, 2, init_sequence, init_sequence_length) {}
|
||||
// The request count is shared by the whole test binary, so never leave it raised
|
||||
~TestableSSD1683() { this->high_frequency_loop_.stop(); }
|
||||
|
||||
void install(spi::SPIDelegate *delegate) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(&this->dc);
|
||||
this->set_busy_pin(&this->busy);
|
||||
this->set_auto_clear(false);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
}
|
||||
|
||||
void draw_whole_panel() {
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
void draw_nothing() {
|
||||
this->x_low_ = this->width_;
|
||||
this->y_low_ = this->height_;
|
||||
this->x_high_ = 0;
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
/// Runs the loop for the given time, or until the display is idle.
|
||||
void loop_for(uint32_t ms) {
|
||||
const uint32_t until = millis() + ms;
|
||||
while (this->state_ != EPaperState::IDLE && millis() < until)
|
||||
this->loop();
|
||||
}
|
||||
|
||||
EPaperState state() const { return this->state_; }
|
||||
|
||||
RecordingPin dc, reset;
|
||||
SwitchableBusyPin busy;
|
||||
};
|
||||
|
||||
/// With no long waits the loop runs continuously from the start of an update to its end, then goes back to its
|
||||
/// normal pace.
|
||||
TEST(EPaperFastLoop, LoopRunsContinuouslyForShortStepsAndStopsAfterward) {
|
||||
TestableSSD1683 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus);
|
||||
ASSERT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
|
||||
display.draw_whole_panel();
|
||||
display.update();
|
||||
bool always_fast = true;
|
||||
const uint32_t deadline = millis() + 1000;
|
||||
while (display.state() != EPaperState::IDLE && millis() < deadline) {
|
||||
always_fast &= HighFrequencyLoopRequester::is_high_frequency();
|
||||
display.loop();
|
||||
}
|
||||
|
||||
ASSERT_EQ(display.state(), EPaperState::IDLE);
|
||||
EXPECT_TRUE(always_fast);
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency()) << "the loop was left running continuously";
|
||||
}
|
||||
|
||||
/// An update with nothing drawn ends straight away and does not leave the loop running continuously.
|
||||
TEST(EPaperFastLoop, UpdateWithNothingDrawnStopsTheFastLoop) {
|
||||
TestableSSD1683 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus);
|
||||
|
||||
display.draw_nothing();
|
||||
display.update();
|
||||
display.loop();
|
||||
|
||||
ASSERT_EQ(display.state(), EPaperState::IDLE);
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
}
|
||||
|
||||
/// A display marked failed part way through an update is no longer looped, so it must stop the fast loop itself.
|
||||
TEST(EPaperFastLoop, FailedDisplayStopsTheFastLoop) {
|
||||
TestableSSD1683 display(MALFORMED_INIT_SEQUENCE, sizeof(MALFORMED_INIT_SEQUENCE));
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus);
|
||||
|
||||
display.draw_whole_panel();
|
||||
display.update();
|
||||
const uint32_t deadline = millis() + 1000;
|
||||
while (!display.is_failed() && millis() < deadline)
|
||||
display.loop();
|
||||
|
||||
ASSERT_TRUE(display.is_failed());
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
}
|
||||
|
||||
/// A long busy wait, such as the waveform or a stuck busy pin, is polled at the normal pace. Once the pin clears,
|
||||
/// the next steps run continuously again.
|
||||
TEST(EPaperFastLoop, LongBusyWaitUsesTheNormalPace) {
|
||||
TestableSSD1683 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus);
|
||||
|
||||
display.busy.busy = true;
|
||||
display.draw_whole_panel();
|
||||
display.update();
|
||||
display.loop_for(FAST_BUSY_POLL_MS + 20);
|
||||
|
||||
ASSERT_NE(display.state(), EPaperState::IDLE);
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
|
||||
display.busy.busy = false;
|
||||
display.loop();
|
||||
EXPECT_TRUE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
display.loop_for(1000);
|
||||
EXPECT_EQ(display.state(), EPaperState::IDLE);
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
}
|
||||
|
||||
/// A delay longer than a loop interval, such as a long reset pulse, is waited out at the normal pace.
|
||||
TEST(EPaperFastLoop, LongDelayUsesTheNormalPace) {
|
||||
TestableSSD1683 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus);
|
||||
display.set_reset_pin(&display.reset);
|
||||
display.set_reset_duration(200);
|
||||
|
||||
display.draw_whole_panel();
|
||||
display.update();
|
||||
display.loop_for(50);
|
||||
|
||||
ASSERT_EQ(display.state(), EPaperState::RESET_END);
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
|
||||
display.loop_for(1000);
|
||||
EXPECT_EQ(display.state(), EPaperState::IDLE);
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
}
|
||||
|
||||
/// A step that takes several loop passes gets the fast loop back once a long busy wait before it has ended.
|
||||
TEST(EPaperFastLoop, FastLoopResumesWhenALongBusyWaitEnds) {
|
||||
TestableSSD1683 display;
|
||||
// every row of data overruns a transfer slice, so the transfer takes a loop pass per row
|
||||
RecordingDelegate bus(&display.dc, MAX_TRANSFER_TIME + 1);
|
||||
display.install(&bus);
|
||||
|
||||
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::TRANSFER_DATA);
|
||||
display.busy.busy = true;
|
||||
display.loop_for(FAST_BUSY_POLL_MS + 20);
|
||||
ASSERT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
|
||||
display.busy.busy = false;
|
||||
display.loop();
|
||||
ASSERT_EQ(display.state(), EPaperState::TRANSFER_DATA) << "the transfer should take more than one pass";
|
||||
EXPECT_TRUE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
}
|
||||
|
||||
class TestableT133A01 : public EPaperT133A01 {
|
||||
public:
|
||||
TestableT133A01() : EPaperT133A01("test", 8, 2, nullptr, 0) {}
|
||||
~TestableT133A01() { this->high_frequency_loop_.stop(); }
|
||||
|
||||
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_));
|
||||
}
|
||||
|
||||
void start_transfer() { this->set_state_(EPaperState::TRANSFER_DATA); }
|
||||
|
||||
RecordingPin dc, cs, cs1;
|
||||
SwitchableBusyPin busy;
|
||||
};
|
||||
|
||||
/// A busy wait that starts in the middle of a step is polled continuously for its first moments, however long
|
||||
/// the step has already run.
|
||||
TEST(EPaperFastLoop, WaitInTheMiddleOfAStepIsPolledFast) {
|
||||
TestableT133A01 display;
|
||||
TimedSPIDelegate delegate(MAX_TRANSFER_TIME + 5);
|
||||
display.install(&delegate);
|
||||
|
||||
display.start_transfer();
|
||||
const uint32_t until = millis() + FAST_BUSY_POLL_MS + 20;
|
||||
while (millis() < until) {
|
||||
}
|
||||
// the transfer waits for the busy pin after its first command, then sends a row and yields
|
||||
display.loop();
|
||||
display.busy.busy = true;
|
||||
display.loop();
|
||||
|
||||
EXPECT_TRUE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
Reference in New Issue
Block a user