diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5692194a91..f97818cfb1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -55,6 +55,15 @@ bool Component::cancel_interval(const std::string &name) { // NOLINT return App.scheduler.cancel_interval(this, name); } +void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, + std::function &&f, float backoff_increase_factor) { // NOLINT + App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); +} + +bool Component::cancel_retry(const std::string &name) { // NOLINT + return App.scheduler.cancel_retry(this, name); +} + void Component::set_timeout(const std::string &name, uint32_t timeout, std::function &&f) { // NOLINT return App.scheduler.set_timeout(this, name, timeout, std::move(f)); } @@ -120,6 +129,10 @@ void Component::set_timeout(uint32_t timeout, std::function &&f) { // N void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT App.scheduler.set_interval(this, "", interval, std::move(f)); } +void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, + float backoff_increase_factor) { // NOLINT + App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); +} bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::can_proceed() { return true; } bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; } diff --git a/esphome/core/component.h b/esphome/core/component.h index a1afc17c2c..c3a4ac3782 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -61,6 +61,8 @@ extern const uint32_t STATUS_LED_OK; extern const uint32_t STATUS_LED_WARNING; extern const uint32_t STATUS_LED_ERROR; +enum RetryResult { DONE, RETRY }; + class Component { public: /** Where the component's initialization should happen. @@ -180,7 +182,35 @@ class Component { */ bool cancel_interval(const std::string &name); // NOLINT - void set_timeout(uint32_t timeout, std::function &&f); // NOLINT + /** Set an retry function with a unique name. Empty name means no cancelling possible. + * + * This will call f. If f returns RetryResult::RETRY f is called again after initial_wait_time ms. + * f should return RetryResult::DONE if no repeat is required. The initial wait time will be increased + * by backoff_increase_factor for each iteration. Default is doubling the time between iterations + * Can be cancelled via cancel_retry(). + * + * IMPORTANT: Do not rely on this having correct timing. This is only called from + * loop() and therefore can be significantly delayed. + * + * @param name The identifier for this retry function. + * @param initial_wait_time The time in ms before f is called again + * @param max_attempts The maximum number of retries + * @param f The function (or lambda) that should be called + * @param backoff_increase_factor time between retries is increased by this factor on every retry + * @see cancel_retry() + */ + void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT + std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT + + void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT + float backoff_increase_factor = 1.0f); // NOLINT + + /** Cancel a retry function. + * + * @param name The identifier for this retry function. + * @return Whether a retry function was deleted. + */ + bool cancel_retry(const std::string &name); // NOLINT /** Set a timeout function with a unique name. * @@ -198,6 +228,8 @@ class Component { */ void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT + /** Cancel a timeout function. * * @param name The identifier for this timeout function. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index a6d3e0307e..3fe07f94b5 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -32,7 +32,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u item->timeout = timeout; item->last_execution = now; item->last_execution_major = this->millis_major_; - item->f = std::move(func); + item->void_callback = std::move(func); item->remove = false; this->push_(std::move(item)); } @@ -65,13 +65,47 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, item->last_execution_major = this->millis_major_; if (item->last_execution > now) item->last_execution_major--; - item->f = std::move(func); + item->void_callback = std::move(func); item->remove = false; this->push_(std::move(item)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::INTERVAL); } + +void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, + uint8_t max_attempts, std::function &&func, + float backoff_increase_factor) { + const uint32_t now = this->millis_(); + + if (!name.empty()) + this->cancel_retry(component, name); + + if (initial_wait_time == SCHEDULER_DONT_RUN) + return; + + ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u,max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), + initial_wait_time, max_attempts, backoff_increase_factor); + + auto item = make_unique(); + item->component = component; + item->name = name; + item->type = SchedulerItem::RETRY; + item->interval = initial_wait_time; + item->retry_countdown = max_attempts; + item->backoff_multiplier = backoff_increase_factor; + item->last_execution = now - initial_wait_time; + item->last_execution_major = this->millis_major_; + if (item->last_execution > now) + item->last_execution_major--; + item->retry_callback = std::move(func); + item->remove = false; + this->push_(std::move(item)); +} +bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { + return this->cancel_item_(component, name, SchedulerItem::RETRY); +} + optional HOT Scheduler::next_schedule_in() { if (this->empty_()) return {}; @@ -95,10 +129,9 @@ void IRAM_ATTR HOT Scheduler::call() { ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); while (!this->empty_()) { auto item = std::move(this->items_[0]); - const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; - ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", type, item->name.c_str(), - item->interval, item->last_execution, item->last_execution_major, item->next_execution(), - item->next_execution_major()); + ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(), + item->name.c_str(), item->interval, item->last_execution, item->last_execution_major, + item->next_execution(), item->next_execution_major()); this->pop_raw_(); old_items.push_back(std::move(item)); @@ -129,6 +162,7 @@ void IRAM_ATTR HOT Scheduler::call() { } while (!this->empty_()) { + RetryResult retry_result = RETRY; // use scoping to indicate visibility of `item` variable { // Don't copy-by value yet @@ -147,17 +181,19 @@ void IRAM_ATTR HOT Scheduler::call() { } #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; - ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(), - item->interval, item->last_execution, now); + ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", item->get_type_str(), + item->name.c_str(), item->interval, item->last_execution, now); #endif - // Warning: During f(), a lot of stuff can happen, including: + // Warning: During callback(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled { WarnIfComponentBlockingGuard guard{item->component}; - item->f(); + if (item->type == SchedulerItem::RETRY) + retry_result = item->retry_callback(); + else + item->void_callback(); } } @@ -175,13 +211,16 @@ void IRAM_ATTR HOT Scheduler::call() { continue; } - if (item->type == SchedulerItem::INTERVAL) { + if (item->type == SchedulerItem::INTERVAL || + (item->type == SchedulerItem::RETRY && (--item->retry_countdown > 0 && retry_result != RetryResult::DONE))) { if (item->interval != 0) { const uint32_t before = item->last_execution; const uint32_t amount = (now - item->last_execution) / item->interval; item->last_execution += amount * item->interval; if (item->last_execution < before) item->last_execution_major++; + if (item->type == SchedulerItem::RETRY) + item->interval *= item->backoff_multiplier; } this->push_(std::move(item)); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index d1839cb4a7..dc96d58329 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -15,6 +15,10 @@ class Scheduler { void set_interval(Component *component, const std::string &name, uint32_t interval, std::function &&func); bool cancel_interval(Component *component, const std::string &name); + void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, + std::function &&func, float backoff_increase_factor = 1.0f); + bool cancel_retry(Component *component, const std::string &name); + optional next_schedule_in(); void call(); @@ -25,13 +29,20 @@ class Scheduler { struct SchedulerItem { Component *component; std::string name; - enum Type { TIMEOUT, INTERVAL } type; + enum Type { TIMEOUT, INTERVAL, RETRY } type; union { uint32_t interval; uint32_t timeout; }; uint32_t last_execution; - std::function f; + // Ideally this should be a union or std::variant + // but unions don't work with object like std::function + // union CallBack_{ + std::function void_callback; + std::function retry_callback; + // }; + uint8_t retry_countdown{3}; + float backoff_multiplier{1.0f}; bool remove; uint8_t last_execution_major; @@ -45,6 +56,18 @@ class Scheduler { } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); + const char *get_type_str() { + switch (this->type) { + case SchedulerItem::INTERVAL: + return "interval"; + case SchedulerItem::RETRY: + return "retry"; + case SchedulerItem::TIMEOUT: + return "timeout"; + default: + return ""; + } + } }; uint32_t millis_();