Merge remote-tracking branch 'upstream/dev' into millis64-zephyr-native

# Conflicts:
#	esphome/core/scheduler.cpp
#	esphome/core/scheduler.h
This commit is contained in:
J. Nick Koston
2026-02-27 08:55:47 -10:00
12 changed files with 121 additions and 30 deletions

View File

@@ -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)

View File

@@ -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]:

View File

@@ -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<typename... Ts> class MarkSuccessfulAction : public Action<Ts...>, public Parented<SafeModeComponent> {
public:
void play(const Ts &...x) override { this->parent_->mark_successful(); }
};
} // namespace esphome::safe_mode
#endif // USE_SAFE_MODE_CALLBACK

View File

@@ -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();
}
}

View File

@@ -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<void()> &&callback) {
this->safe_mode_callback_.add(std::move(callback));

View File

@@ -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: {

View File

@@ -13,6 +13,19 @@
#include <FreeRTOS.h>
#include <queue.h>
#ifdef USE_BK72XX
extern "C" {
#include <wlan_ui_pub.h>
}
#endif
#ifdef USE_RTL87XX
extern "C" {
#include <wifi_conf.h>
#include <wifi_structures.h>
}
#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<char, SSID_BUFFER_SIZE> 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<const char *>(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<const char *>(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<size_t>(ssid.length()), SSID_BUFFER_SIZE - 1);
memcpy(buffer.data(), ssid.c_str(), len);
#endif
buffer[len] = '\0';
return buffer.data();
}

View File

@@ -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<Application>::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)

View File

@@ -16,3 +16,7 @@ button:
switch:
- platform: safe_mode
name: Safe Mode Switch
esphome:
on_boot:
- safe_mode.mark_successful

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common.yaml