diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d9662483bf..127ef44cd8 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 9ff393b397..182c37ba40 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -102,7 +102,7 @@ def customise_schema(config): """ config = cv.Schema( { - cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"), }, extra=cv.ALLOW_EXTRA, )(config) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py index 9f0b673d69..42a5a7da72 100644 --- a/esphome/components/epaper_spi/models/spectra_e6.py +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -32,11 +32,15 @@ class SpectraE6(EpaperModel): spectra_e6 = SpectraE6("spectra-e6") -spectra_e6.extend( - "Seeed-reTerminal-E1002", +spectra_e6_7p3 = spectra_e6.extend( + "7.3in-Spectra-E6", width=800, height=480, data_rate="20MHz", +) + +spectra_e6_7p3.extend( + "Seeed-reTerminal-E1002", cs_pin=10, dc_pin=11, reset_pin=12, diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 139d695990..fa33739639 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -87,6 +87,29 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_ } } // namespace +void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) { + // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions + // + // The race condition occurs because close() initiates lwIP teardown while + // the TCP/IP thread can still receive packets, causing assertions when + // recv_tcp() sees partially-torn-down state. + // + // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE + // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR) + // to allow the FIN packet to be sent cleanly during close(). + // + // Note: This function may be called with an already-closed socket if the network + // stack closed it. In that case, shutdown() will fail but close() is safe to call. + // + // See: https://github.com/esphome/esphome-webserver/issues/163 + + // Attempt shutdown - ignore errors as socket may already be closed + shutdown(sockfd, SHUT_RD); + + // Always close - safe even if socket is already closed by network stack + close(sockfd); +} + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -115,6 +138,8 @@ void AsyncWebServer::begin() { config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) config.lru_purge_enable = this->lru_purge_enable_; + // Use custom close function that shuts down before closing to prevent lwIP race conditions + config.close_fn = AsyncWebServer::safe_close_with_shutdown; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -506,17 +531,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); int fd = rsp->fd_.exchange(0); // Atomically get and clear fd - - if (fd > 0) { - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); - // Immediately shut down the socket to prevent lwIP from delivering more data - // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack - // tries to deliver queued data after the session is marked as dead - // See: https://github.com/esphome/esphome/issues/11936 - shutdown(fd, SHUT_RDWR); - // Note: We don't close() the socket - httpd owns it and will close it - } - // Session will be cleaned up in the main loop to avoid race conditions + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Mark as dead - will be cleaned up in the main loop + // Note: We don't delete or remove from set here to avoid race conditions + // httpd will call our custom close_fn (safe_close_with_shutdown) which handles + // shutdown() before close() to prevent lwIP race conditions } // helper for allowing only unique entries in the queue diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index b9f690b462..a139e9e4df 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -209,6 +209,7 @@ class AsyncWebServer { static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; + static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif diff --git a/requirements_test.txt b/requirements_test.txt index e238faa77e..7f6d3f8e26 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.14.5 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3b756095a1..b07a249c8d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -462,7 +462,7 @@ class Int64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -482,7 +482,7 @@ class UInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -522,7 +522,7 @@ class Fixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -1106,7 +1106,7 @@ class SFixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -1150,7 +1150,7 @@ class SInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -2546,7 +2546,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); }