Merge pull request #21254 from esphome/bump-2026.10.0b6

2026.10.0b6
This commit is contained in:
Jesse Hills
2026-10-21 16:39:54 +13:00
committed by GitHub
8 changed files with 101 additions and 57 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.10.0b5
PROJECT_NUMBER = 2026.10.0b6
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.24.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.25.0
RUN \
platformio settings set enable_telemetry No \
+3 -2
View File
@@ -98,8 +98,9 @@ enum OTAType : uint8_t {
// The OTA backend method surface. Exactly one backend exists per build,
// selected in ota_backend_factory.h where this concept is asserted on
// make_ota_backend()'s return type. Semantics beyond the signatures:
// - begin: prepare for an image of the given size; ota_type defaults to an
// app update, so both call forms must be accepted.
// - begin: prepare for an image of the given size; 0 means the size is unknown
// (web_server upload) and the backend allows the largest image it can take.
// ota_type defaults to an app update, so both call forms must be accepted.
// - set_update_md5: expected digest of the incoming image, hex string.
// - write: consume the next chunk; end: finalize and mark bootable.
// - abort: safe to call in any state, including after end().
@@ -9,6 +9,12 @@
#include <Updater.h>
// LittleFS region bounds, provided by the arduino-pico linker script.
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern "C" uint8_t _FS_start;
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern "C" uint8_t _FS_end;
namespace esphome::ota {
static const char *const TAG = "ota";
@@ -19,8 +25,11 @@ OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type
if (ota_type != OTA_TYPE_UPDATE_APP) {
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
}
// OTA size of 0 is not currently handled, but
// web_server is not supported for RP2040, so this is not an issue.
// 0 means the size is unknown (web_server upload). The updater only checks the size
// against the LittleFS region it stages the image in, so ask for all of it.
if (image_size == 0) {
image_size = reinterpret_cast<uintptr_t>(&_FS_end) - reinterpret_cast<uintptr_t>(&_FS_start);
}
bool ret = Update.begin(image_size, U_FLASH);
if (ret) {
rp2::preferences_prevent_write(true);
@@ -34,7 +43,7 @@ OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type
return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
if (error == UPDATE_ERROR_FLASH_CONFIG)
return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
if (error == UPDATE_ERROR_SPACE)
if (error == UPDATE_ERROR_SPACE || error == UPDATE_ERROR_SIZE)
return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE;
ESP_LOGE(TAG, "Begin error: %d", error);
@@ -57,7 +57,7 @@ namespace esphome::web_server_idf {
static const char *const TAG = "web_server_idf";
// Only send_json_() may hold the JSON arena: every other frame in this file is capped below one
// Only prepare_json_() may hold the JSON arena: every other frame in this file is capped below one
// arena. Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it.
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
#pragma GCC diagnostic error "-Wstack-usage=2048"
@@ -1121,68 +1121,88 @@ void AsyncEventSourceResponse::loop() {
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the arena and the JSON buffer
#pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the JSON arena
#endif
bool AsyncEventSourceResponse::send_json_(void *source, message_generator_t *generator) {
// The arena lives only in this frame, so no call chain ever holds two of them
AsyncEventSourceResponse::JsonPrepareResult AsyncEventSourceResponse::prepare_json_(void *source,
message_generator_t *generator,
char *buffer, size_t *size_out) {
// Log lines emitted while the generator runs are dropped for this session: on the large path they would fill
// tail_ before it is overwritten, and on the small path they would go out ahead of the event being built.
SendGuard guard{*this};
json::JsonArena<json::JSON_ARENA_SIZE> arena;
json::JsonBuilder builder(&arena);
generator(this->web_server_, source, builder);
char buf[JSON_BUF_SIZE];
const size_t len = builder.serialize_to(buf, sizeof(buf));
if (len < sizeof(buf)) {
return this->try_send_nodefer(buf, len, "state");
*size_out = builder.serialize_to(buffer, JSON_BUF_SIZE);
if (*size_out < JSON_BUF_SIZE) {
return JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER;
}
// Too large for the stack: the tail holds the whole chunk and loop() drains it. Serialized
// JSON has no raw line break, so the body is one data line.
if (!this->ready_to_send_()) {
return false;
}
{
SendGuard guard{*this};
char prefix[PREFIX_BUF_SIZE];
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
char prefix[PREFIX_BUF_SIZE];
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
// Grow the tail until the document fits. Nothing has reached the wire, so a document that
// cannot be held costs only this event, and the tail grown for it is released.
size_t json_len = 0;
size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
for (;;) {
if (!this->reserve_tail_(cap)) {
this->tail_alloc_failed_(cap);
return false; // stays deferred, retried on a later pass
}
const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
if (json_len < room) {
break;
}
if (cap >= TAIL_MAX_SIZE) {
ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", room);
this->tail_.reset();
this->tail_cap_ = 0;
this->send_failure_started_ms_ = 0;
return true; // would never fit, reported as sent
}
cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
// Nothing reaches the wire until the complete document and its framing are owned by the caller.
size_t json_len = 0;
size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
for (;;) {
if (!this->reserve_tail_(cap)) {
*size_out = cap;
return JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY;
}
const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
write_chunk_header(prefix, total - CHUNK_HDR_LEN - CHUNK_END_LEN);
uint8_t *dst = this->tail_.get();
std::memcpy(dst, prefix, prefix_len);
std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
this->tail_len_ = total;
this->tail_sent_ = 0;
const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
if (json_len < room) {
break;
}
if (cap >= TAIL_MAX_SIZE) {
*size_out = room;
return JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE;
}
cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
}
drain_tail_();
return true;
const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
write_chunk_header(prefix, total - CHUNK_HDR_LEN - CHUNK_END_LEN);
uint8_t *dst = this->tail_.get();
std::memcpy(dst, prefix, prefix_len);
std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
this->tail_len_ = total;
this->tail_sent_ = 0;
return JsonPrepareResult::JSON_PREPARE_RESULT_TAIL;
}
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
#pragma GCC diagnostic pop
#endif
bool AsyncEventSourceResponse::send_json_(void *source, message_generator_t *generator) {
if (!this->ready_to_send_()) {
return false;
}
char buffer[JSON_BUF_SIZE];
size_t size;
const auto result = this->prepare_json_(source, generator, buffer, &size);
switch (result) {
case JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER:
return this->try_send_nodefer(buffer, size, "state");
case JsonPrepareResult::JSON_PREPARE_RESULT_TAIL:
this->drain_tail_();
return true;
case JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY: {
SendGuard guard{*this};
this->tail_alloc_failed_(size);
return false;
}
case JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE: {
SendGuard guard{*this};
ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", size);
this->tail_.reset();
this->tail_cap_ = 0;
this->send_failure_started_ms_ = 0;
return true;
}
}
return false;
}
void AsyncEventSourceResponse::tail_alloc_failed_(size_t cap) {
// Same stall clock as a socket that stops draining, so a session cannot retry forever
const uint32_t now = App.get_loop_component_start_time();
@@ -317,6 +317,17 @@ class AsyncEventSourceResponse {
size_t sent);
// Send a state event; JSON too large for the stack buffer is serialized into tail_ instead
bool send_json_(void *source, message_generator_t *generator);
enum class JsonPrepareResult : uint8_t {
JSON_PREPARE_RESULT_BUFFER,
JSON_PREPARE_RESULT_TAIL,
JSON_PREPARE_RESULT_NO_MEMORY,
JSON_PREPARE_RESULT_TOO_LARGE,
};
// Keep the arena out of the caller's frame so it is gone before the state send.
// size_out: BUFFER = JSON length, NO_MEMORY = requested tail capacity, TOO_LARGE = available JSON room;
// unused for TAIL.
JsonPrepareResult __attribute__((noinline))
prepare_json_(void *source, message_generator_t *generator, char *buffer, size_t *size_out);
// Warn once, and close the session once the stall timeout passes with no memory for the tail
void tail_alloc_failed_(size_t cap);
void request_close_();
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.10.0b5"
__version__ = "2026.10.0b6"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@@ -6,3 +6,6 @@ web_server:
username: admin
password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
type: basic
ota:
- platform: web_server