mirror of
https://github.com/esphome/esphome.git
synced 2026-10-21 13:34:12 +00:00
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user