mirror of
https://github.com/esphome/esphome.git
synced 2026-07-11 01:15:33 +00:00
Merge branch 'compile-time-has-overridden-loop' into integration
# Conflicts: # esphome/core/application.cpp
This commit is contained in:
@@ -43,3 +43,4 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await improv_base.setup_improv_core(var, config, "improv_serial")
|
||||
cg.add_define("USE_IMPROV_SERIAL")
|
||||
|
||||
@@ -2122,7 +2122,7 @@ bool WiFiComponent::can_proceed() {
|
||||
#endif
|
||||
|
||||
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
bool WiFiComponent::is_connected() {
|
||||
bool WiFiComponent::is_connected() const {
|
||||
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
||||
this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_;
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ class WiFiComponent : public Component {
|
||||
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
|
||||
bool is_connected();
|
||||
bool is_connected() const;
|
||||
|
||||
void set_power_save_mode(WiFiPowerSaveMode power_save);
|
||||
void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; }
|
||||
@@ -677,7 +677,7 @@ class WiFiComponent : public Component {
|
||||
bool wifi_apply_hostname_();
|
||||
bool wifi_sta_connect_(const WiFiAP &ap);
|
||||
void wifi_pre_setup_();
|
||||
WiFiSTAConnectStatus wifi_sta_connect_status_();
|
||||
WiFiSTAConnectStatus wifi_sta_connect_status_() const;
|
||||
bool wifi_scan_start_(bool passive);
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
|
||||
@@ -623,7 +623,7 @@ void WiFiComponent::wifi_pre_setup_() {
|
||||
this->wifi_mode_(false, false);
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
station_status_t status = wifi_station_get_connect_status();
|
||||
if (status == STATION_GOT_IP)
|
||||
return WiFiSTAConnectStatus::CONNECTED;
|
||||
|
||||
@@ -921,7 +921,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
if (s_sta_connected && this->got_ipv4_address_) {
|
||||
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) {
|
||||
|
||||
@@ -634,7 +634,7 @@ void WiFiComponent::wifi_pre_setup_() {
|
||||
// Make sure WiFi is in clean state before anything starts
|
||||
this->wifi_mode_(false, false);
|
||||
}
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
// Use state machine instead of querying WiFi.status() directly
|
||||
// State is updated in main loop from queued events, ensuring thread safety
|
||||
switch (s_sta_state) {
|
||||
|
||||
@@ -120,7 +120,7 @@ const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
// Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino
|
||||
// framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif
|
||||
// (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's
|
||||
|
||||
@@ -82,7 +82,10 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::register_component_(Component *comp) {
|
||||
void Application::register_component_impl_(Component *comp, bool has_loop) {
|
||||
if (has_loop) {
|
||||
comp->component_state_ |= COMPONENT_HAS_LOOP;
|
||||
}
|
||||
this->components_.push_back(comp);
|
||||
#ifdef USE_SETUP_HEAP_STATS
|
||||
if (global_setup_heap_stats == nullptr) {
|
||||
@@ -93,7 +96,6 @@ void Application::register_component_(Component *comp) {
|
||||
global_setup_heap_stats->record_component_registered(comp);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::setup() {
|
||||
ESP_LOGI(TAG, "Running through setup()");
|
||||
ESP_LOGV(TAG, "Sorting components by setup priority");
|
||||
@@ -402,16 +404,8 @@ void Application::teardown_components(uint32_t timeout_ms) {
|
||||
}
|
||||
|
||||
void Application::calculate_looping_components_() {
|
||||
// Count total components that need looping
|
||||
size_t total_looping = 0;
|
||||
for (auto *obj : this->components_) {
|
||||
if (obj->has_overridden_loop()) {
|
||||
total_looping++;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize FixedVector with exact size - no reallocation possible
|
||||
this->looping_components_.init(total_looping);
|
||||
// FixedVector capacity was pre-initialized by codegen with the exact count
|
||||
// of components that override loop(), computed at C++ compile time.
|
||||
|
||||
// Add all components with loop override that aren't already LOOP_DONE
|
||||
// Some components (like logger) may call disable_loop() during initialization
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
@@ -671,7 +672,14 @@ class Application {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
void register_component_(Component *comp);
|
||||
/// Register a component, detecting loop() override at compile time.
|
||||
/// The template resolves &T::loop vs &Component::loop as a constexpr bool
|
||||
/// and forwards it to register_component_impl_ which stores it in component_state_.
|
||||
template<typename T> void register_component_(T *comp) {
|
||||
this->register_component_impl_(comp, !std::is_same_v<decltype(&T::loop), decltype(&Component::loop)>);
|
||||
}
|
||||
|
||||
void register_component_impl_(Component *comp, bool has_loop);
|
||||
|
||||
void calculate_looping_components_();
|
||||
void add_looping_components_by_state_(bool match_loop_done);
|
||||
|
||||
@@ -510,18 +510,6 @@ void Component::set_setup_priority(float priority) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Component::has_overridden_loop() const {
|
||||
#if defined(USE_HOST) || defined(CLANG_TIDY)
|
||||
return true;
|
||||
#else
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpmf-conversions"
|
||||
bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop);
|
||||
#pragma GCC diagnostic pop
|
||||
return loop_overridden;
|
||||
#endif
|
||||
}
|
||||
|
||||
PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {}
|
||||
|
||||
void PollingComponent::call_setup() {
|
||||
|
||||
@@ -76,6 +76,8 @@ inline constexpr uint8_t STATUS_LED_MASK = 0x18;
|
||||
inline constexpr uint8_t STATUS_LED_OK = 0x00;
|
||||
inline constexpr uint8_t STATUS_LED_WARNING = 0x08;
|
||||
inline constexpr uint8_t STATUS_LED_ERROR = 0x10;
|
||||
// Component loop override flag uses bit 5 (set at registration time)
|
||||
inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
|
||||
|
||||
// Remove before 2026.8.0
|
||||
enum class RetryResult { DONE, RETRY };
|
||||
@@ -271,7 +273,7 @@ class Component {
|
||||
*/
|
||||
void status_momentary_error(const char *name, uint32_t length = 5000);
|
||||
|
||||
bool has_overridden_loop() const;
|
||||
bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; }
|
||||
|
||||
/** Set where this component was loaded from for some debug messages.
|
||||
*
|
||||
@@ -510,7 +512,8 @@ class Component {
|
||||
/// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE)
|
||||
/// Bit 3: STATUS_LED_WARNING
|
||||
/// Bit 4: STATUS_LED_ERROR
|
||||
/// Bits 5-7: Unused - reserved for future expansion
|
||||
/// Bit 5: Has overridden loop() (set at registration time)
|
||||
/// Bits 6-7: Unused - reserved for future expansion
|
||||
uint8_t component_state_{0x00};
|
||||
volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context
|
||||
};
|
||||
|
||||
@@ -504,6 +504,39 @@ async def _add_controller_registry_define() -> None:
|
||||
cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_looping_components() -> None:
|
||||
# Emit a constexpr that computes the looping component count at C++ compile time
|
||||
# and pre-init the FixedVector with the exact capacity. Uses std::is_same_v to
|
||||
# detect loop() overrides. The constexpr goes in main.cpp's global section where
|
||||
# all component types are in scope. calculate_looping_components_() then skips
|
||||
# the counting pass and only does the two population passes.
|
||||
entries = CORE.data.get("looping_component_entries", [])
|
||||
if not entries:
|
||||
return
|
||||
|
||||
# Build constexpr sum for the exact count
|
||||
terms = [
|
||||
f"(!std::is_same_v<decltype(&{cpp_type}::loop), decltype(&Component::loop)>)"
|
||||
for cpp_type in entries
|
||||
]
|
||||
constexpr_expr = " + \\\n ".join(terms)
|
||||
cg.add_global(
|
||||
cg.RawStatement(
|
||||
f"static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT = \\\n"
|
||||
f" {constexpr_expr};"
|
||||
)
|
||||
)
|
||||
|
||||
# Pre-init FixedVector with exact capacity so calculate_looping_components_()
|
||||
# can skip the counting pass
|
||||
cg.add(
|
||||
cg.RawExpression(
|
||||
"App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(cg.global_ns.namespace("esphome").using)
|
||||
@@ -527,6 +560,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
CORE.add_job(_add_platform_defines)
|
||||
CORE.add_job(_add_controller_registry_define)
|
||||
CORE.add_job(_add_looping_components)
|
||||
|
||||
CORE.add_job(_add_automations, config)
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#define USE_HOMEASSISTANT_TIME
|
||||
#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT
|
||||
#define USE_IMAGE
|
||||
#define USE_IMPROV_SERIAL
|
||||
#define USE_IMPROV_SERIAL_NEXT_URL
|
||||
#define USE_INFRARED
|
||||
#define USE_IR_RF
|
||||
|
||||
@@ -80,6 +80,11 @@ async def register_component(var, config):
|
||||
add(var.set_component_source(LogStringLiteral(name)))
|
||||
|
||||
add(App.register_component_(var))
|
||||
|
||||
# Collect C++ type for compile-time looping component count
|
||||
comp_entries = CORE.data.setdefault("looping_component_entries", [])
|
||||
comp_entries.append(str(var.base.type))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_component(monkeypatch):
|
||||
var = Mock(base="foo.bar")
|
||||
base_mock = Mock()
|
||||
base_mock.__str__ = lambda self: "foo.bar"
|
||||
base_mock.type = Mock()
|
||||
base_mock.type.__str__ = lambda self: "foo::Bar"
|
||||
var = Mock(base=base_mock)
|
||||
|
||||
app_mock = Mock(register_component_=Mock(return_value=var))
|
||||
monkeypatch.setattr(ch, "App", app_mock)
|
||||
@@ -46,7 +50,11 @@ async def test_register_component__no_component_id(monkeypatch):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_component__with_setup_priority(monkeypatch):
|
||||
var = Mock(base="foo.bar")
|
||||
base_mock = Mock()
|
||||
base_mock.__str__ = lambda self: "foo.bar"
|
||||
base_mock.type = Mock()
|
||||
base_mock.type.__str__ = lambda self: "foo::Bar"
|
||||
var = Mock(base=base_mock)
|
||||
|
||||
app_mock = Mock(register_component_=Mock(return_value=var))
|
||||
monkeypatch.setattr(ch, "App", app_mock)
|
||||
|
||||
Reference in New Issue
Block a user