diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2a93d37fd..b752701920 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -686,7 +686,7 @@ jobs: ram_usage: ${{ steps.extract.outputs.ram_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }} cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} - skip: ${{ steps.check-script.outputs.skip }} + skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }} steps: - name: Check out target branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -705,10 +705,39 @@ jobs: echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" fi - # All remaining steps only run if script exists + # Check if test files exist on the target branch for the requested + # components and platform. When a PR adds new test files for a platform, + # the target branch won't have them yet, so skip instead of failing. + # This check must be done here (not in determine-jobs.py) because + # determine-jobs runs on the PR branch and cannot see what the target + # branch has. + - name: Check for test files on target branch + id: check-tests + if: steps.check-script.outputs.skip != 'true' + run: | + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + found=false + for component in $(echo "$components" | jq -r '.[]'); do + # Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml) + for f in tests/components/${component}/test*.${platform}.yaml; do + if [ -f "$f" ]; then + found=true + break 2 + fi + done + done + if [ "$found" = false ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + # All remaining steps only run if script and tests exist - name: Generate cache key id: cache-key - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | # Get the commit SHA of the target branch target_sha=$(git rev-parse HEAD) @@ -735,14 +764,14 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} - name: Cache status - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then echo "✓ Cache hit! Using cached memory analysis results." @@ -752,21 +781,21 @@ jobs: fi - name: Restore Python - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} - name: Build, compile, and analyze memory - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' id: build run: | . venv/bin/activate @@ -800,7 +829,7 @@ jobs: --platform "$platform" - name: Save memory analysis to cache - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json @@ -808,7 +837,7 @@ jobs: - name: Extract memory usage for outputs id: extract - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ -f memory-analysis-target.json ]; then ram=$(jq -r '.ram_bytes' memory-analysis-target.json) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index d1754aaad7..f54151b746 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -21,6 +21,7 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) +MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) def _remove_id_if_disabled(value): @@ -53,6 +54,22 @@ CONFIG_SCHEMA = cv.All( ) +@automation.register_action( + "safe_mode.mark_successful", + MarkSuccessfulAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(SafeModeComponent), + } + ), +) +async def safe_mode_mark_successful_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(parent)) + return var + + @coroutine_with_priority(CoroPriority.APPLICATION) async def to_code(config): if not config[CONF_DISABLED]: diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index 1d82ac45f1..dee02c64a0 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -1,20 +1,22 @@ #pragma once #include "esphome/core/defines.h" - -#ifdef USE_SAFE_MODE_CALLBACK -#include "safe_mode.h" - #include "esphome/core/automation.h" +#include "safe_mode.h" namespace esphome::safe_mode { +#ifdef USE_SAFE_MODE_CALLBACK class SafeModeTrigger final : public Trigger<> { public: explicit SafeModeTrigger(SafeModeComponent *parent) { parent->add_on_safe_mode_callback([this]() { trigger(); }); } }; +#endif // USE_SAFE_MODE_CALLBACK + +template class MarkSuccessfulAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->mark_successful(); } +}; } // namespace esphome::safe_mode - -#endif // USE_SAFE_MODE_CALLBACK diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index aa4fdb8291..fe2acd9612 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -63,18 +63,22 @@ void SafeModeComponent::dump_config() { float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } +void SafeModeComponent::mark_successful() { + this->clean_rtc(); + this->boot_successful_ = true; +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + // Mark OTA partition as valid to prevent rollback + esp_ota_mark_app_valid_cancel_rollback(); +#endif + // Disable loop since we no longer need to check + this->disable_loop(); +} + void SafeModeComponent::loop() { if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) { // successful boot, reset counter ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); - this->clean_rtc(); - this->boot_successful_ = true; -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) - // Mark OTA partition as valid to prevent rollback - esp_ota_mark_app_valid_cancel_rollback(); -#endif - // Disable loop since we no longer need to check - this->disable_loop(); + this->mark_successful(); } } diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 902b8c415d..1b28ea28f2 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -31,6 +31,8 @@ class SafeModeComponent final : public Component { void on_safe_shutdown() override; + void mark_successful(); + #ifdef USE_SAFE_MODE_CALLBACK void add_on_safe_mode_callback(std::function &&callback) { this->safe_mode_callback_.add(std::move(callback)); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index cbf7d7d80f..8911bf15e0 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -470,10 +470,6 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { return LOG_STR("Unspecified"); } -// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). -// All listener notifications should be deferred to wifi_loop_() via pending_ flags -// to avoid stack overflow. Currently only connect_state is deferred; disconnect, -// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2cc05928af..1c5490a3ac 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -13,6 +13,19 @@ #include #include +#ifdef USE_BK72XX +extern "C" { +#include +} +#endif + +#ifdef USE_RTL87XX +extern "C" { +#include +#include +} +#endif + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -760,10 +773,22 @@ bssid_t WiFiComponent::wifi_bssid() { } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } const char *WiFiComponent::wifi_ssid_to(std::span buffer) { - // TODO: Find direct LibreTiny API to avoid Arduino String allocation +#ifdef USE_BK72XX + LinkStatusTypeDef link_status{}; + bk_wlan_get_link_status(&link_status); + size_t len = strnlen(reinterpret_cast(link_status.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), link_status.ssid, len); +#elif defined(USE_RTL87XX) + rtw_wifi_setting_t setting{}; + wifi_get_setting("wlan0", &setting); + size_t len = strnlen(reinterpret_cast(setting.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), setting.ssid, len); +#else + // LN882X: wifi_get_sta_conn_info() provides direct pointer access String ssid = WiFi.SSID(); size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); memcpy(buffer.data(), ssid.c_str(), len); +#endif buffer[len] = '\0'; return buffer.data(); } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3bd4c1c670..e27c2cdfc6 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -727,8 +727,17 @@ void Application::yield_with_select_(uint32_t delay_ms) { #error "Application placement new requires Itanium C++ ABI (GCC/Clang)" #endif static_assert(std::is_default_constructible::value, "Application must be default-constructible"); +// __USER_LABEL_PREFIX__ is "_" on Mach-O (macOS) and empty on ELF (embedded targets). +// String literal concatenation produces the correct platform-specific mangled symbol. +// Two-level macro needed: # stringifies before expansion, so the +// indirection forces __USER_LABEL_PREFIX__ to expand first. +#define ESPHOME_STRINGIFY_IMPL_(x) #x +#define ESPHOME_STRINGIFY_(x) ESPHOME_STRINGIFY_IMPL_(x) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE"); +alignas(Application) char app_storage[sizeof(Application)] asm( + ESPHOME_STRINGIFY_(__USER_LABEL_PREFIX__) "_ZN7esphome3AppE"); +#undef ESPHOME_STRINGIFY_ +#undef ESPHOME_STRINGIFY_IMPL_ #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/tests/components/safe_mode/common-enabled.yaml b/tests/components/safe_mode/common-enabled.yaml index c24f49e6b6..43025c60db 100644 --- a/tests/components/safe_mode/common-enabled.yaml +++ b/tests/components/safe_mode/common-enabled.yaml @@ -16,3 +16,7 @@ button: switch: - platform: safe_mode name: Safe Mode Switch + +esphome: + on_boot: + - safe_mode.mark_successful diff --git a/tests/components/wifi/test.bk72xx-ard.yaml b/tests/components/wifi/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.ln882x-ard.yaml b/tests/components/wifi/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.rtl87xx-ard.yaml b/tests/components/wifi/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/wifi/test.rtl87xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml