diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 37f82e2755e..d6d401ee661 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695 +ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 9c7f051e053..38e93c4f175 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 841c297bce4..8718772f537 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,7 @@ jobs: python-version: - "3.11" - "3.13" + - "3.14" os: - ubuntu-latest - macOS-latest diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 51ea4085e0c..376825bad60 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7e03e2a5f99..ba5c32e0167 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 991e053d5ae..6d89060b0df 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.0 + rev: v0.15.1 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index 25e6dc1b296..2aa0656343c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet +esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_stats/* @bdraco esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti diff --git a/Doxyfile b/Doxyfile index efc2f464e3e..1a9e0b4e10f 100644 --- a/Doxyfile +++ b/Doxyfile @@ -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.2.0-dev +PROJECT_NUMBER = 2026.3.0-dev # 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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 348a503bc8f..540d28be7f1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b ARG BUILD_TYPE FROM base-source-${BUILD_TYPE} AS base -RUN git config --system --add safe.directory "*" +RUN git config --system --add safe.directory "*" \ + && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation # (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) @@ -23,7 +24,7 @@ RUN if command -v apk > /dev/null; then \ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 -RUN pip install --no-cache-dir -U pip uv==0.6.14 +RUN pip install --no-cache-dir -U pip uv==0.10.1 COPY requirements.txt / diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 66866615a63..3bdf555ae39 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -256,7 +256,7 @@ SYMBOL_PATTERNS = { "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], # Order matters! More specific categories must come before general ones. # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern - "mdns_lib": ["mdns"], + "mdns_lib": ["mdns", "packet$"], # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols "memory_mgmt": [ "mem_", @@ -794,7 +794,6 @@ SYMBOL_PATTERNS = { "s_dp", "s_ni", "s_reg_dump", - "packet$", "d_mult_table", "K", "fcstab", diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e51689fd077..4d564af9e2a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -219,35 +219,8 @@ void APIConnection::loop() { this->process_batch_(); } - switch (this->active_iterator_) { - case ActiveIterator::LIST_ENTITIES: - if (this->iterator_storage_.list_entities.completed()) { - this->destroy_active_iterator_(); - if (this->flags_.state_subscription) { - this->begin_iterator_(ActiveIterator::INITIAL_STATE); - } - } else { - this->process_iterator_batch_(this->iterator_storage_.list_entities); - } - break; - case ActiveIterator::INITIAL_STATE: - if (this->iterator_storage_.initial_state.completed()) { - this->destroy_active_iterator_(); - // Process any remaining batched messages immediately - if (!this->deferred_batch_.empty()) { - this->process_batch_(); - } - // Now that everything is sent, enable immediate sending for future state changes - this->flags_.should_try_send_immediately = true; - // Release excess memory from buffers that grew during initial sync - this->deferred_batch_.release_buffer(); - this->helper_->release_buffers(); - } else { - this->process_iterator_batch_(this->iterator_storage_.initial_state); - } - break; - case ActiveIterator::NONE: - break; + if (this->active_iterator_ != ActiveIterator::NONE) { + this->process_active_iterator_(); } if (this->flags_.sent_ping) { @@ -283,6 +256,49 @@ void APIConnection::loop() { #endif } +void APIConnection::process_active_iterator_() { + // Caller ensures active_iterator_ != NONE + if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) { + if (this->iterator_storage_.list_entities.completed()) { + this->destroy_active_iterator_(); + if (this->flags_.state_subscription) { + this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } + } else { + this->process_iterator_batch_(this->iterator_storage_.list_entities); + } + } else { // INITIAL_STATE + if (this->iterator_storage_.initial_state.completed()) { + this->destroy_active_iterator_(); + // Process any remaining batched messages immediately + if (!this->deferred_batch_.empty()) { + this->process_batch_(); + } + // Now that everything is sent, enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); + } else { + this->process_iterator_batch_(this->iterator_storage_.initial_state); + } + } +} + +void APIConnection::process_iterator_batch_(ComponentIterator &iterator) { + size_t initial_size = this->deferred_batch_.size(); + size_t max_batch = this->get_max_batch_size_(); + while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { + iterator.advance(); + } + + // If the batch is full, process it immediately + // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() + if (this->deferred_batch_.size() >= max_batch) { + this->process_batch_(); + } +} + bool APIConnection::send_disconnect_response_() { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response @@ -1494,7 +1510,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; char peername[socket::SOCKADDR_STR_LEN]; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; @@ -1848,6 +1864,8 @@ void APIConnection::on_fatal_error() { this->flags_.remove = true; } +void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); } + void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, uint8_t aux_data_index) { // Check if we already have a message of this type for this entity @@ -1864,7 +1882,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_ } } // No existing item found (or event), add new one - items.push_back({entity, message_type, estimated_size, aux_data_index}); + this->push_item({entity, message_type, estimated_size, aux_data_index}); } void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { @@ -1872,7 +1890,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me // This avoids expensive vector::insert which shifts all elements // Note: We only ever have one high-priority message at a time (ping OR disconnect) // If we're disconnecting, pings are blocked, so this simple swap is sufficient - items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED}); + this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED}); if (items.size() > 1) { // Swap the new high-priority item to the front std::swap(items.front(), items.back()); @@ -1905,10 +1923,6 @@ bool APIConnection::schedule_batch_() { } void APIConnection::process_batch_() { - // Ensure MessageInfo remains trivially destructible for our placement new approach - static_assert(std::is_trivially_destructible::value, - "MessageInfo must remain trivially destructible with this placement-new approach"); - if (this->deferred_batch_.empty()) { this->flags_.batch_scheduled = false; return; @@ -1933,6 +1947,10 @@ void APIConnection::process_batch_() { for (size_t i = 0; i < num_items; i++) { total_estimated_size += this->deferred_batch_[i].estimated_size; } + // Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch + if (total_estimated_size > MAX_BATCH_PACKET_SIZE) { + total_estimated_size = MAX_BATCH_PACKET_SIZE; + } this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); @@ -1956,7 +1974,20 @@ void APIConnection::process_batch_() { return; } - size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); + // Multi-message path — heavy stack frame isolated in separate noinline function + this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size); +} + +// Separated from process_batch_() so the single-message fast path gets a minimal +// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. +void APIConnection::process_batch_multi_(std::vector &shared_buf, size_t num_items, uint8_t header_padding, + uint8_t footer_size) { + // Ensure MessageInfo remains trivially destructible for our placement new approach + static_assert(std::is_trivially_destructible::value, + "MessageInfo must remain trivially destructible with this placement-new approach"); + + const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); + const uint8_t frame_overhead = header_padding + footer_size; // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; @@ -1983,7 +2014,7 @@ void APIConnection::process_batch_() { // Message was encoded successfully // payload_size is header_padding + actual payload size + footer_size - uint16_t proto_payload_size = payload_size - header_padding - footer_size; + uint16_t proto_payload_size = payload_size - frame_overhead; // Use placement new to construct MessageInfo in pre-allocated stack array // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, @@ -1999,42 +2030,38 @@ void APIConnection::process_batch_() { current_offset = shared_buf.size() + footer_size; } - if (items_processed == 0) { - this->deferred_batch_.clear(); - return; - } + if (items_processed > 0) { + // Add footer space for the last message (for Noise protocol MAC) + if (footer_size > 0) { + shared_buf.resize(shared_buf.size() + footer_size); + } - // Add footer space for the last message (for Noise protocol MAC) - if (footer_size > 0) { - shared_buf.resize(shared_buf.size() + footer_size); - } - - // Send all collected messages - APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, - std::span(message_info, items_processed)); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); - } + // Send all collected messages + APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, + std::span(message_info, items_processed)); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); + } #ifdef HAS_PROTO_MESSAGE_DUMP - // Log messages after send attempt for VV debugging - // It's safe to use the buffer for logging at this point regardless of send result - for (size_t i = 0; i < items_processed; i++) { - const auto &item = this->deferred_batch_[i]; - this->log_batch_item_(item); - } + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + for (size_t i = 0; i < items_processed; i++) { + const auto &item = this->deferred_batch_[i]; + this->log_batch_item_(item); + } #endif - // Handle remaining items more efficiently - if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning - this->deferred_batch_.remove_front(items_processed); - // Reschedule for remaining items - this->schedule_batch_(); - } else { - // All items processed - this->clear_batch_(); + // Partial batch — remove processed items and reschedule + if (items_processed < this->deferred_batch_.size()) { + this->deferred_batch_.remove_front(items_processed); + this->schedule_batch_(); + return; + } } + + // All items processed (or none could be processed) + this->clear_batch_(); } // Dispatch message encoding based on message_type diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index abcb162865f..e34bed8ada0 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -15,6 +15,10 @@ #include #include +namespace esphome { +class ComponentIterator; +} // namespace esphome + namespace esphome::api { // Keepalive timeout in milliseconds @@ -366,20 +370,13 @@ class APIConnection final : public APIServerConnectionBase { return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; } - // Helper method to process multiple entities from an iterator in a batch - template void process_iterator_batch_(Iterator &iterator) { - size_t initial_size = this->deferred_batch_.size(); - size_t max_batch = this->get_max_batch_size_(); - while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { - iterator.advance(); - } + // Process active iterator (list_entities/initial_state) during connection setup. + // Extracted from loop() — only runs during initial handshake, NONE in steady state. + void __attribute__((noinline)) process_active_iterator_(); - // If the batch is full, process it immediately - // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() - if (this->deferred_batch_.size() >= max_batch) { - this->process_batch_(); - } - } + // Helper method to process multiple entities from an iterator in a batch. + // Takes ComponentIterator base class reference to avoid duplicate template instantiations. + void process_iterator_batch_(ComponentIterator &iterator); #ifdef USE_BINARY_SENSOR static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); @@ -544,6 +541,8 @@ class APIConnection final : public APIServerConnectionBase { uint8_t aux_data_index = AUX_DATA_UNUSED); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size); + // Single push_back site to avoid duplicate _M_realloc_insert instantiation + void push_item(const BatchItem &item); // Clear all items void clear() { @@ -551,8 +550,8 @@ class APIConnection final : public APIServerConnectionBase { batch_start_time = 0; } - // Remove processed items from the front - void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } + // Remove processed items from the front — noinline to keep memmove out of warm callers + void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } @@ -624,6 +623,8 @@ class APIConnection final : public APIServerConnectionBase { bool schedule_batch_(); void process_batch_(); + void process_batch_multi_(std::vector &shared_buf, size_t num_items, uint8_t header_padding, + uint8_t footer_size) __attribute__((noinline)); void clear_batch_() { this->deferred_batch_.clear(); this->flags_.batch_scheduled = false; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index c1641b398aa..1ae848deadf 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - // During handshake phase, process as many actions as possible until we can't progress - // socket_->ready() stays true until next main loop, but state_action() will return - // WOULD_BLOCK when no more data is available to read - while (state_ != State::DATA && this->socket_->ready()) { + // Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once + // the rx buffer is consumed. Re-checking each iteration would block handshake writes + // that must follow reads, deadlocking the handshake. state_action() will return + // WOULD_BLOCK when no more data is available to read. + bool socket_ready = this->socket_->ready(); + while (state_ != State::DATA && socket_ready) { APIError err = state_action_(); if (err == APIError::WOULD_BLOCK) { break; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index ed3cc8934eb..5069dbf68b5 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -295,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe buf_start[header_offset] = 0x00; // indicator // Encode varints directly into buffer - ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - ProtoVarInt(msg.message_type) - .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); + encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); // Add iovec for this message (header + payload) size_t msg_len = static_cast(total_header_len + msg.payload_size); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 53b41a5c149..67a117e68fd 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -28,11 +28,7 @@ static const char *const TAG = "api"; // APIServer APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -APIServer::APIServer() { - global_api_server = this; - // Pre-allocate shared write buffer - shared_write_buffer_.reserve(64); -} +APIServer::APIServer() { global_api_server = this; } void APIServer::setup() { ControllerRegistry::register_controller(this); @@ -117,37 +113,7 @@ void APIServer::setup() { void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { - while (true) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - - auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); - if (!sock) - break; - - char peername[socket::SOCKADDR_STR_LEN]; - sock->getpeername_to(peername); - - // Check if we're at the connection limit - if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); - // Immediately close - socket destructor will handle cleanup - sock.reset(); - continue; - } - - ESP_LOGD(TAG, "Accept %s", peername); - - auto *conn = new APIConnection(std::move(sock), this); - this->clients_.emplace_back(conn); - conn->start(); - - // First client connected - clear warning and update timestamp - if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { - this->status_clear_warning(); - this->last_connected_ = App.get_loop_component_start_time(); - } - } + this->accept_new_connections_(); } if (this->clients_.empty()) { @@ -178,46 +144,88 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; + // Common case: process active client if (!client->flags_.remove) { - // Common case: process active client client->loop(); + } + // Handle disconnection promptly - close socket to free LWIP PCB + // resources and prevent retransmit crashes on ESP8266. + if (client->flags_.remove) { + // Rare case: handle disconnection (don't increment - swapped element needs processing) + this->remove_client_(client_index); + } else { client_index++; + } + } +} + +void APIServer::remove_client_(size_t client_index) { + auto &client = this->clients_[client_index]; + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->unregister_active_action_calls_for_connection(client.get()); +#endif + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Save client info before closing socket and removal for the trigger + char peername_buf[socket::SOCKADDR_STR_LEN]; + std::string client_name(client->get_name()); + std::string client_peername(client->get_peername_to(peername_buf)); +#endif + + // Close socket now (was deferred from on_fatal_error to allow getpeername) + client->helper_->close(); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Last client disconnected - set warning and start tracking for reboot timeout + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->status_set_warning(); + this->last_connected_ = App.get_loop_component_start_time(); + } + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Fire trigger after client is removed so api.connected reflects the true state + this->client_disconnected_trigger_.trigger(client_name, client_peername); +#endif +} + +void __attribute__((flatten)) APIServer::accept_new_connections_() { + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + + auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + + // Check if we're at the connection limit + if (this->clients_.size() >= this->max_connections_) { + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); + // Immediately close - socket destructor will handle cleanup + sock.reset(); continue; } - // Rare case: handle disconnection -#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - this->unregister_active_action_calls_for_connection(client.get()); -#endif - ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + ESP_LOGD(TAG, "Accept %s", peername); -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Save client info before closing socket and removal for the trigger - char peername_buf[socket::SOCKADDR_STR_LEN]; - std::string client_name(client->get_name()); - std::string client_peername(client->get_peername_to(peername_buf)); -#endif + auto *conn = new APIConnection(std::move(sock), this); + this->clients_.emplace_back(conn); + conn->start(); - // Close socket now (was deferred from on_fatal_error to allow getpeername) - client->helper_->close(); - - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - - // Last client disconnected - set warning and start tracking for reboot timeout - if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + // First client connected - clear warning and update timestamp + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); this->last_connected_ = App.get_loop_component_start_time(); } - -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Fire trigger after client is removed so api.connected reflects the true state - this->client_disconnected_trigger_.trigger(client_name, client_peername); -#endif - // Don't increment client_index since we need to process the swapped element } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6ab3cdc5764..323acc2efb3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -234,6 +234,11 @@ class APIServer : public Component, #endif protected: + // Accept incoming socket connections. Only called when socket has pending connections. + void __attribute__((noinline)) accept_new_connections_(); + // Remove a disconnected client by index. Swaps with last element and pops. + void __attribute__((noinline)) remove_client_(size_t client_index); + #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, const psk_t &active_psk, bool make_active); @@ -263,7 +268,11 @@ class APIServer : public Component, // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; - std::vector shared_write_buffer_; // Shared proto write buffer for all connections + // Shared proto write buffer for all connections. + // Not pre-allocated: all send paths call prepare_first_message_buffer() which + // reserves the exact needed size. Pre-allocating here would cause heap fragmentation + // since the buffer would almost always reallocate on first use. + std::vector shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index bef36dd015f..90769f9a81e 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -94,7 +94,6 @@ class ListEntitiesIterator : public ComponentIterator { bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; - bool completed() { return this->state_ == IteratorState::NONE; } protected: APIConnection *client_; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 74aca551033..bd72decb72e 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -150,7 +150,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { break; } default: - ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer)); + ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer)); return; } } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 2dc92e46a7d..b386dfaff65 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) { return count; } +/// Encode a varint directly into a pre-allocated buffer. +/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate). +inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) { + while (val > 0x7F) { + *buffer++ = static_cast(val | 0x80); + val >>= 7; + } + *buffer = static_cast(val); +} + /* * StringRef Ownership Model for API Protocol Messages * =================================================== @@ -93,6 +103,7 @@ class ProtoVarInt { ProtoVarInt() : value_(0) {} explicit ProtoVarInt(uint64_t value) : value_(value) {} + /// Parse a varint from buffer. consumed must be a valid pointer (not null). static optional parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { #ifdef ESPHOME_DEBUG_API assert(consumed != nullptr); @@ -148,50 +159,6 @@ class ProtoVarInt { return decode_zigzag64(this->value_); } #endif - /** - * Encode the varint value to a pre-allocated buffer without bounds checking. - * - * @param buffer The pre-allocated buffer to write the encoded varint to - * @param len The size of the buffer in bytes - * - * @note The caller is responsible for ensuring the buffer is large enough - * to hold the encoded value. Use ProtoSize::varint() to calculate - * the exact size needed before calling this method. - * @note No bounds checking is performed for performance reasons. - */ - void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) { - uint64_t val = this->value_; - if (val <= 0x7F) { - buffer[0] = val; - return; - } - size_t i = 0; - while (val && i < len) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - buffer[i++] = temp | 0x80; - } else { - buffer[i++] = temp; - } - } - } - void encode(std::vector &out) { - uint64_t val = this->value_; - if (val <= 0x7F) { - out.push_back(val); - return; - } - while (val) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - out.push_back(temp | 0x80); - } else { - out.push_back(temp); - } - } - } protected: uint64_t value_; @@ -251,8 +218,20 @@ class ProtoWriteBuffer { public: ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} void write(uint8_t value) { this->buffer_->push_back(value); } - void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } - void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } + void encode_varint_raw(uint32_t value) { + while (value > 0x7F) { + this->buffer_->push_back(static_cast(value | 0x80)); + value >>= 7; + } + this->buffer_->push_back(static_cast(value)); + } + void encode_varint_raw_64(uint64_t value) { + while (value > 0x7F) { + this->buffer_->push_back(static_cast(value | 0x80)); + value >>= 7; + } + this->buffer_->push_back(static_cast(value)); + } /** * Encode a field key (tag/wire type combination). * @@ -302,13 +281,13 @@ class ProtoWriteBuffer { if (value == 0 && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 - this->encode_varint_raw(ProtoVarInt(value)); + this->encode_varint_raw_64(value); } void encode_bool(uint32_t field_id, bool value, bool force = false) { if (!value && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->write(0x01); + this->buffer_->push_back(value ? 0x01 : 0x00); } void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) @@ -933,13 +912,15 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa this->buffer_->resize(this->buffer_->size() + varint_length_bytes); // Write the length varint directly - ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes); + encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); // Now encode the message content - it will append to the buffer value.encode(*this); +#ifdef ESPHOME_DEBUG_API // Verify that the encoded size matches what we calculated assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); +#endif } // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 3c9f33835a5..6f8577ca7be 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -88,7 +88,6 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_UPDATE bool on_update(update::UpdateEntity *entity) override; #endif - bool completed() { return this->state_ == IteratorState::NONE; } protected: APIConnection *client_; diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 790cd62db0e..ddb669e0eb9 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include + #include "esphome/core/helpers.h" namespace esphome { @@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const { } bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { - if (this->buffer_length_ > 0) { - // Buffer currently has data, so reallocation is impossible + if (this->buffer_ == nullptr) { + return this->allocate_buffer_(new_buffer_size); + } + + if (new_buffer_size < this->buffer_length_) { + // New size is too small to hold existing data return false; } - this->deallocate_buffer_(); - return this->allocate_buffer_(new_buffer_size); + + // Shift existing data to the start of the buffer so realloc preserves it + if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) { + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); + this->data_start_ = this->buffer_; + } + + RAMAllocator allocator; + uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size); + if (new_buffer == nullptr) { + // Reallocation failed, but the original buffer is still valid + return false; + } + + this->buffer_ = new_buffer; + this->data_start_ = this->buffer_; + this->buffer_size_ = new_buffer_size; + return true; } bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { @@ -115,7 +137,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ if (pre_shift) { // Shift data in buffer to start if (this->buffer_length_ > 0) { - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); } this->data_start_ = this->buffer_; } @@ -150,7 +172,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (post_shift) { // Shift unwritten data to the start of the buffer - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); this->data_start_ = this->buffer_; } diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index edb484e7d2c..24c0670d1a0 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -56,6 +56,9 @@ class AudioTransferBuffer { /// @return True if there is data, false otherwise. virtual bool has_buffered_data() const; + /// @brief Reallocates the transfer buffer, preserving any existing data. + /// @param new_buffer_size The new size in bytes. Must be at least as large as available(). + /// @return True if successful, false otherwise. On failure, the original buffer remains valid. bool reallocate(size_t new_buffer_size); protected: diff --git a/esphome/components/bk72xx/boards.py b/esphome/components/bk72xx/boards.py index 3850dbe2667..4bee69fe6da 100644 --- a/esphome/components/bk72xx/boards.py +++ b/esphome/components/bk72xx/boards.py @@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "cbu": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7231t-qfn32-tuya": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7231n-qfn32-tuya": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = { }, "cb3s": { "WIRE1_SCL": 20, - "WIRE1_SDA_0": 21, - "WIRE1_SDA_1": 21, + "WIRE1_SDA": 21, "SERIAL1_RX": 10, "SERIAL1_TX": 11, "SERIAL2_TX": 0, @@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7252": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "cb3se": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE2_SCL": 0, "WIRE2_SDA": 1, "SERIAL1_RX": 10, diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 37b884e6caf..10b29a72c61 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -59,10 +59,10 @@ namespace bl0942 { // // Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4 -static const float BL0942_PREF = 596; // taken from tasmota -static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218 -static const float BL0942_IREF = 251213.46469622; // 305978/1.218 -static const float BL0942_EREF = 3304.61127328; // Measured +static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF +static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider +static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt +static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF struct DataPacket { uint8_t frame_header; @@ -86,11 +86,11 @@ enum LineFrequency : uint8_t { class BL0942 : public PollingComponent, public uart::UARTDevice { public: - void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } - void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } - void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } - void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } - void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; } void set_address(uint8_t address) { this->address_ = address; } void set_reset(bool reset) { this->reset_ = reset; } diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index d7d99721709..c781252de3a 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -6,8 +6,9 @@ */ #include "bmp3xx_base.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include namespace esphome { @@ -26,46 +27,18 @@ static const LogString *chip_type_to_str(uint8_t chip_type) { } } +// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32 +PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", ""); + static const LogString *oversampling_to_str(Oversampling oversampling) { - switch (oversampling) { - case Oversampling::OVERSAMPLING_NONE: - return LOG_STR("None"); - case Oversampling::OVERSAMPLING_X2: - return LOG_STR("2x"); - case Oversampling::OVERSAMPLING_X4: - return LOG_STR("4x"); - case Oversampling::OVERSAMPLING_X8: - return LOG_STR("8x"); - case Oversampling::OVERSAMPLING_X16: - return LOG_STR("16x"); - case Oversampling::OVERSAMPLING_X32: - return LOG_STR("32x"); - default: - return LOG_STR(""); - } + return OversamplingStrings::get_log_str(static_cast(oversampling), OversamplingStrings::LAST_INDEX); } +// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128 +PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *iir_filter_to_str(IIRFilter filter) { - switch (filter) { - case IIRFilter::IIR_FILTER_OFF: - return LOG_STR("OFF"); - case IIRFilter::IIR_FILTER_2: - return LOG_STR("2x"); - case IIRFilter::IIR_FILTER_4: - return LOG_STR("4x"); - case IIRFilter::IIR_FILTER_8: - return LOG_STR("8x"); - case IIRFilter::IIR_FILTER_16: - return LOG_STR("16x"); - case IIRFilter::IIR_FILTER_32: - return LOG_STR("32x"); - case IIRFilter::IIR_FILTER_64: - return LOG_STR("64x"); - case IIRFilter::IIR_FILTER_128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return IIRFilterStrings::get_log_str(static_cast(filter), IIRFilterStrings::LAST_INDEX); } void BMP3XXComponent::setup() { diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index 67c67718625..c4a96ebc39b 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -11,57 +11,26 @@ */ #include "bmp581_base.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::bmp581_base { static const char *const TAG = "bmp581"; +// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128 +PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *oversampling_to_str(Oversampling oversampling) { - switch (oversampling) { - case Oversampling::OVERSAMPLING_NONE: - return LOG_STR("None"); - case Oversampling::OVERSAMPLING_X2: - return LOG_STR("2x"); - case Oversampling::OVERSAMPLING_X4: - return LOG_STR("4x"); - case Oversampling::OVERSAMPLING_X8: - return LOG_STR("8x"); - case Oversampling::OVERSAMPLING_X16: - return LOG_STR("16x"); - case Oversampling::OVERSAMPLING_X32: - return LOG_STR("32x"); - case Oversampling::OVERSAMPLING_X64: - return LOG_STR("64x"); - case Oversampling::OVERSAMPLING_X128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return OversamplingStrings::get_log_str(static_cast(oversampling), OversamplingStrings::LAST_INDEX); } +// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128 +PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *iir_filter_to_str(IIRFilter filter) { - switch (filter) { - case IIRFilter::IIR_FILTER_OFF: - return LOG_STR("OFF"); - case IIRFilter::IIR_FILTER_2: - return LOG_STR("2x"); - case IIRFilter::IIR_FILTER_4: - return LOG_STR("4x"); - case IIRFilter::IIR_FILTER_8: - return LOG_STR("8x"); - case IIRFilter::IIR_FILTER_16: - return LOG_STR("16x"); - case IIRFilter::IIR_FILTER_32: - return LOG_STR("32x"); - case IIRFilter::IIR_FILTER_64: - return LOG_STR("64x"); - case IIRFilter::IIR_FILTER_128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return IIRFilterStrings::get_log_str(static_cast(filter), IIRFilterStrings::LAST_INDEX); } void BMP581Component::dump_config() { diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp index d17a4e2707d..97cdd2e1043 100644 --- a/esphome/components/camera/buffer_impl.cpp +++ b/esphome/components/camera/buffer_impl.cpp @@ -3,18 +3,22 @@ namespace esphome::camera { BufferImpl::BufferImpl(size_t size) { - this->data_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->data_ = allocator.allocate(size); this->size_ = size; } BufferImpl::BufferImpl(CameraImageSpec *spec) { - this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + RAMAllocator allocator; + this->data_ = allocator.allocate(spec->bytes_per_image()); this->size_ = spec->bytes_per_image(); } BufferImpl::~BufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->size_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->size_); + } } } // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h index 46398295fa2..5e42df79572 100644 --- a/esphome/components/camera/buffer_impl.h +++ b/esphome/components/camera/buffer_impl.h @@ -18,7 +18,6 @@ class BufferImpl : public Buffer { ~BufferImpl() override; protected: - RAMAllocator allocator_; size_t size_{}; uint8_t *data_{}; }; diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp index db84026496a..f12c66f203f 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.cpp +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -4,7 +4,8 @@ namespace esphome::camera_encoder { bool EncoderBufferImpl::set_buffer_size(size_t size) { if (size > this->capacity_) { - uint8_t *p = this->allocator_.reallocate(this->data_, size); + RAMAllocator allocator; + uint8_t *p = allocator.reallocate(this->data_, size); if (p == nullptr) return false; @@ -16,8 +17,10 @@ bool EncoderBufferImpl::set_buffer_size(size_t size) { } EncoderBufferImpl::~EncoderBufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->capacity_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->capacity_); + } } } // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h index 13eccb7d564..d394daff14f 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.h +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -16,7 +16,6 @@ class EncoderBufferImpl : public camera::EncoderBuffer { ~EncoderBufferImpl() override; protected: - RAMAllocator allocator_; size_t capacity_{}; size_t size_{}; uint8_t *data_{}; diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 8d88a10b27a..5af6ab29a2e 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { - std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) - std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) + const auto &ssid = request->arg("ssid"); + const auto &psk = request->arg("psk"); ESP_LOGI(TAG, "Requested WiFi Settings Change:\n" " SSID='%s'\n" @@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ssid.c_str(), psk.c_str()); #ifdef USE_ESP8266 // ESP8266 is single-threaded, call directly - wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); #else // Defer save to main loop thread to avoid NVS operations from HTTP thread - this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif request->redirect(ESPHOME_F("/?save")); } diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 716d2703901..ece7cca4825 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() { } void LinearCombinationComponent::handle_new_value(float value) { - // Multiplies each sensor state by a configured coeffecient and then sums + // Multiplies each sensor state by a configured coefficient and then sums if (!std::isfinite(value)) return; diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index f5255fec03a..0204162e8d9 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -15,6 +17,8 @@ from esphome.const import ( ) from esphome.core.entity_helpers import inherit_property_from +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@Cat-Ion", "@kahrendt"] combination_ns = cg.esphome_ns.namespace("combination") @@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_( "SumCombinationComponent", cg.Component, sensor.Sensor ) -CONF_COEFFECIENT = "coeffecient" +CONF_COEFFICIENT = "coefficient" +CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0 CONF_ERROR = "error" CONF_KALMAN = "kalman" CONF_LINEAR = "linear" @@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema( } ) -LINEAR_SOURCE_SCHEMA = cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), - } + +def _migrate_coeffecient(config): + """Migrate deprecated 'coeffecient' spelling to 'coefficient'.""" + if CONF_COEFFECIENT in config: + if CONF_COEFFICIENT in config: + raise cv.Invalid( + f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'" + ) + _LOGGER.warning( + "'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0", + CONF_COEFFECIENT, + CONF_COEFFICIENT, + ) + config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT) + elif CONF_COEFFICIENT not in config: + raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option") + return config + + +LINEAR_SOURCE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_), + cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_), + } + ), + _migrate_coeffecient, ) SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( @@ -162,12 +190,12 @@ async def to_code(config): ) cg.add(var.add_source(source, error)) elif config[CONF_TYPE] == CONF_LINEAR: - coeffecient = await cg.templatable( - source_conf[CONF_COEFFECIENT], + coefficient = await cg.templatable( + source_conf[CONF_COEFFICIENT], [(float, "x")], cg.float_, ) - cg.add(var.add_source(source, coeffecient)) + cg.add(var.add_source(source, coefficient)) else: cg.add(var.add_source(source)) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 41774f3d71a..648fe7decff 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_MQTT_ID, + CONF_MQTT_JSON_STATE_PAYLOAD, CONF_ON_IDLE, CONF_ON_OPEN, CONF_POSITION, @@ -119,6 +120,9 @@ _COVER_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), + cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All( + cv.requires_component("mqtt"), cv.boolean + ), cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic @@ -148,6 +152,22 @@ _COVER_SCHEMA = ( _COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) +def _validate_mqtt_state_topics(config): + if config.get(CONF_MQTT_JSON_STATE_PAYLOAD): + if CONF_POSITION_STATE_TOPIC in config: + raise cv.Invalid( + f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'" + ) + if CONF_TILT_STATE_TOPIC in config: + raise cv.Invalid( + f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'" + ) + return config + + +_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics) + + def cover_schema( class_: MockObjClass, *, @@ -195,6 +215,9 @@ async def setup_cover_core_(var, config): position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC) ) is not None: cg.add(mqtt_.set_custom_position_command_topic(position_command_topic)) + if config.get(CONF_MQTT_JSON_STATE_PAYLOAD): + cg.add_define("USE_MQTT_COVER_JSON") + cg.add(mqtt_.set_use_json_format(True)) if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None: cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic)) if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None: diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index a4b6468b497..1a07ec4f3ac 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -1,6 +1,7 @@ #include "debug_component.h" #ifdef USE_ESP8266 #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include extern "C" { @@ -19,27 +20,38 @@ namespace debug { static const char *const TAG = "debug"; +// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback +// clang-format off +PROGMEM_STRING_TABLE(ResetReasonStrings, + "Power On", // 0 = REASON_DEFAULT_RST + "Hardware Watchdog", // 1 = REASON_WDT_RST + "Exception", // 2 = REASON_EXCEPTION_RST + "Software Watchdog", // 3 = REASON_SOFT_WDT_RST + "Software/System restart", // 4 = REASON_SOFT_RESTART + "Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE + "External System", // 6 = REASON_EXT_SYS_RST + "Unknown" // 7 = fallback +); +// clang-format on +static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices"); +static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices"); +static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices"); +static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices"); +static_assert(REASON_SOFT_RESTART == 4, "Reset reason enum values must match table indices"); +static_assert(REASON_DEEP_SLEEP_AWAKE == 5, "Reset reason enum values must match table indices"); +static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices"); + +// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback +PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN"); +static_assert(FM_QIO == 0, "Flash mode enum values must match table indices"); +static_assert(FM_QOUT == 1, "Flash mode enum values must match table indices"); +static_assert(FM_DIO == 2, "Flash mode enum values must match table indices"); +static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices"); + // Get reset reason string from reason code (no heap allocation) // Returns LogString* pointing to flash (PROGMEM) on ESP8266 static const LogString *get_reset_reason_str(uint32_t reason) { - switch (reason) { - case REASON_DEFAULT_RST: - return LOG_STR("Power On"); - case REASON_WDT_RST: - return LOG_STR("Hardware Watchdog"); - case REASON_EXCEPTION_RST: - return LOG_STR("Exception"); - case REASON_SOFT_WDT_RST: - return LOG_STR("Software Watchdog"); - case REASON_SOFT_RESTART: - return LOG_STR("Software/System restart"); - case REASON_DEEP_SLEEP_AWAKE: - return LOG_STR("Deep-Sleep Wake"); - case REASON_EXT_SYS_RST: - return LOG_STR("External System"); - default: - return LOG_STR("Unknown"); - } + return ResetReasonStrings::get_log_str(static_cast(reason), ResetReasonStrings::LAST_INDEX); } // Size for core version hex buffer @@ -92,23 +104,9 @@ size_t DebugComponent::get_device_info_(std::span constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - const LogString *flash_mode; - switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) - case FM_QIO: - flash_mode = LOG_STR("QIO"); - break; - case FM_QOUT: - flash_mode = LOG_STR("QOUT"); - break; - case FM_DIO: - flash_mode = LOG_STR("DIO"); - break; - case FM_DOUT: - flash_mode = LOG_STR("DOUT"); - break; - default: - flash_mode = LOG_STR("UNKNOWN"); - } + const LogString *flash_mode = FlashModeStrings::get_log_str( + static_cast(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance) + FlashModeStrings::LAST_INDEX); uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance) ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 13f66691b22..2657071f45a 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_( ) Transform = epaper_spi_ns.enum("Transform") -EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) -EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) - - # Import all models dynamically from the models package for module_info in pkgutil.iter_modules(models.__path__): importlib.import_module(f".models.{module_info.name}", package=__package__) diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp new file mode 100644 index 00000000000..bd83105dd77 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -0,0 +1,231 @@ +#include "epaper_weact_3c.h" +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_weact_3c"; + +// SSD1680 3-color display notes: +// - Buffer uses 1 bit per pixel, 8 pixels per byte +// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer second half (red_offset): Red plane (1=red, 0=no red) +// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) +// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) + +void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + + // Calculate position in the 1-bit buffer + const uint32_t pos = (x / 8) + (y * this->row_width_); + const uint8_t bit = 0x80 >> (x & 0x07); + const uint32_t red_offset = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + // Split at halfway point (382 = (255*3)/2) + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + + // Update black/white plane (first half of buffer) + if (is_white) { + // White pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; + } else { + // Black pixel - set bit in black plane + this->buffer_[pos] |= bit; + } + + // Update red plane (second half of buffer) + // Red if red component is dominant (r > g+b) + if (color.r > color.g + color.b) { + // Red pixel - set bit in red plane + this->buffer_[red_offset + pos] |= bit; + } else { + // Not red - clear bit in red plane + this->buffer_[red_offset + pos] &= ~bit; + } +} + +void EPaperWeAct3C::fill(Color color) { + // For 3-color e-paper with 1-bit buffer format: + // - Black buffer: 1=black, 0=white + // - Red buffer: 1=red, 0=no red + // The buffer is stored as two halves: [black plane][red plane] + const size_t half_buffer = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + bool is_red = color.r > color.g + color.b; + + // Fill both planes + if (is_white) { + // White - both planes = 0x00 + this->buffer_.fill(0x00); + } else if (is_red) { + // Red - black plane = 0x00, red plane = 0xFF + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0x00; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0xFF; + } else { + // Black - black plane = 0xFF, red plane = 0x00 + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0xFF; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0x00; + } +} + +void EPaperWeAct3C::clear() { + // Clear buffer to white, just like real paper. + this->fill(COLOR_ON); +} + +void EPaperWeAct3C::set_window_() { + // For full screen refresh, we always start from (0,0) + // The y_low_/y_high_ values track the dirty region for optimization, + // but for display refresh we need to write from the beginning + uint16_t x_start = 0; + uint16_t x_end = this->width_ - 1; + uint16_t y_start = 0; + uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display + + // Set RAM X address boundaries (0x44) + // X coordinates are byte-aligned (divided by 8) + this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)}); + + // Set RAM Y address boundaries (0x45) + // Format: Y start (LSB, MSB), Y end (LSB, MSB) + this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)}); + + // Reset RAM X counter to start (0x4E) - 1 byte + this->cmd_data(0x4E, {(uint8_t) (x_start / 8)}); + + // Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB) + this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)}); +} + +bool HOT EPaperWeAct3C::transfer_data() { + const uint32_t start_time = millis(); + const size_t buffer_length = this->buffer_length_; + const size_t half_buffer = buffer_length / 2u; + + ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); + + // Use a local buffer for SPI transfers + static constexpr size_t MAX_TRANSFER_SIZE = 128; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + + // First, send the RED buffer (0x26 = WRITE_COLOR) + // The red plane is in the second half of our buffer + // NOTE: Must set RAM window first to reset address counters! + if (this->current_data_index_ < half_buffer) { + if (this->current_data_index_ == 0) { + ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)"); + this->set_window_(); // Reset RAM X/Y counters to start position + this->command(0x26); + } + + this->start_data_(); + size_t red_offset = half_buffer; + while (this->current_data_index_ < half_buffer) { + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_); + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + // Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK) + // The black plane is in the first half of our buffer + if (this->current_data_index_ < buffer_length) { + if (this->current_data_index_ == half_buffer) { + ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)"); + + // Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior) + // this->set_window(); + this->command(0x24); + // Continue using current_data_index_, but we need to map it to the start of the buffer + } + + this->start_data_(); + while (this->current_data_index_ < buffer_length) { + size_t remaining = buffer_length - this->current_data_index_; + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining); + + // Calculate offset into the BLACK buffer (which is at the start of this->buffer_) + // current_data_index_ goes from half_buffer to buffer_length + size_t buffer_offset = this->current_data_index_ - half_buffer; + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[buffer_offset + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + this->current_data_index_ = 0; + ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer); + return true; +} + +void EPaperWeAct3C::refresh_screen(bool partial) { + // SSD1680 refresh sequence: + // Reset RAM X/Y address counters to 0,0 so display reads from start + // 0x4E: RAM X counter - 1 byte (X / 8) + // 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB) + this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte) + this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes) + + // Send UPDATE_FULL command (0x22) with display update control parameter + // Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7} + // 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content + this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7 + this->command(0x20); // Activate display update + + // COMMAND TERMINATE FRAME READ WRITE (required by SSD1680) + // Removed 0xFF based on working reference implementation + // this->command(0xFF); +} + +void EPaperWeAct3C::power_on() { + // Power on sequence - send command to turn on power + // According to SSD1680 spec: 0x22, 0xF8 powers on the display + this->cmd_data(0x22, {0xF8}); // Power on + this->command(0x20); // Activate +} + +void EPaperWeAct3C::power_off() { + // Power off sequence - send command to turn off power + // According to SSD1680 spec: 0x22, 0x83 powers off the display + this->cmd_data(0x22, {0x83}); // Power off + this->command(0x20); // Activate +} + +void EPaperWeAct3C::deep_sleep() { + // Deep sleep sequence + this->cmd_data(0x10, {0x01}); // Deep sleep mode +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_weact_3c.h b/esphome/components/epaper_spi/epaper_weact_3c.h new file mode 100644 index 00000000000..2df6f1ba097 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.h @@ -0,0 +1,39 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * WeAct 3-color e-paper displays (SSD1683 controller). + * Supports multiple sizes: 2.9" (128x296), 4.2" (400x300), etc. + * + * Color scheme: Black, White, Red (BWR) + * Buffer layout: 1 bit per pixel, separate planes + * - Buffer first half: Black/White plane (1=black, 0=white) + * - Buffer second half: Red plane (1=red, 0=no red) + * - Total buffer: width * height / 4 bytes (2 * width * height / 8) + */ +class EPaperWeAct3C : public EPaperBase { + public: + EPaperWeAct3C(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = this->row_width_ * height * 2; + } + + void fill(Color color) override; + void clear() override; + + protected: + void set_window_(); + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + void draw_pixel_at(int x, int y, Color color) override; + + bool transfer_data() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/jd79660.py b/esphome/components/epaper_spi/models/jd79660.py index 2d8830ebd26..a0457c5812a 100644 --- a/esphome/components/epaper_spi/models/jd79660.py +++ b/esphome/components/epaper_spi/models/jd79660.py @@ -84,3 +84,35 @@ jd79660.extend( (0xA5, 0x00,), ), ) + +# Waveshare 7.5-H +# +# Vendor init derived from vendor sample code +# +# Compatible MIT license, see esphome/LICENSE file. +# +# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML. +# +# fmt: off +jd79660.extend( + "Waveshare-7.5in-H", + width=800, + height=480, + + initsequence=( + (0x00, 0x0F, 0x29,), + (0x06, 0x0F, 0x8B, 0x93, 0xA1,), + (0x41, 0x00,), + (0x50, 0x37,), + (0x60, 0x02, 0x02,), + (0x61, 800 // 256, 800 % 256, 480 // 256, 480 % 256,), # RES: 800x480 + (0x62, 0x98, 0x98, 0x98, 0x75, 0xCA, 0xB2, 0x98, 0x7E,), + (0x65, 0x00, 0x00, 0x00, 0x00,), + (0xE7, 0x1C,), + (0xE3, 0x00,), + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), +) diff --git a/esphome/components/epaper_spi/models/weact_bwr.py b/esphome/components/epaper_spi/models/weact_bwr.py new file mode 100644 index 00000000000..21a015b08d9 --- /dev/null +++ b/esphome/components/epaper_spi/models/weact_bwr.py @@ -0,0 +1,75 @@ +"""WeAct Black/White/Red e-paper displays using SSD1683 controller. + +Supported models: +- weact-2.13in-3c: 122x250 pixels (2.13" display) +- weact-2.9in-3c: 128x296 pixels (2.9" display) +- weact-4.2in-3c: 400x300 pixels (4.2" display) + +These displays use SSD1680 or SSD1683 controller and require a specific initialization +sequence. The DRV_OUT_CTL command is calculated from the display height. +""" + +from . import EpaperModel + + +class WeActBWR(EpaperModel): + """Base EpaperModel class for WeAct Black/White/Red displays using SSD1683 controller.""" + + def __init__(self, name, **defaults): + super().__init__(name, "EPaperWeAct3C", **defaults) + + def get_init_sequence(self, config): + """Generate initialization sequence for WeAct BWR displays. + + The initialization sequence is based on SSD1680 and SSD1683 controller datasheet + and the WeAct display specifications. + """ + _, height = self.get_dimensions(config) + # DRV_OUT_CTL: MSB of (height-1), LSB of (height-1), gate setting (0x00) + height_minus_1 = height - 1 + msb = height_minus_1 >> 8 + lsb = height_minus_1 & 0xFF + return ( + # Step 1: Software Reset (0x12) - REQUIRED per SSD1680, but works without it as well, so it's commented out for now + # (0x12,), + # Step 2: Wait 10ms after SWRESET (?) not sure how to implement wht waiting for 10ms after SWRESET, so it's commented out for now + # Step 3: DRV_OUT_CTL - driver output control (height-dependent) + # Format: (command, LSB, MSB, gate setting) + (0x01, lsb, msb, 0x00), + # Step 4: DATA_ENTRY - data entry mode (0x03 = decrement Y, increment X) + (0x11, 0x03), + # Step 5: BORDER_FULL - border waveform control + (0x3C, 0x05), + # Step 6: TEMP_SENS - internal temperature sensor + (0x18, 0x80), + # Step 7: DISPLAY_UPDATE - display update control + (0x21, 0x00, 0x80), + ) + + +# Model: WeAct 2.9" 3C - 128x296 pixels, SSD1680 controller +weact_2p9in3c = WeActBWR( + "weact-2.9in-3c", + width=128, + height=296, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 2.13" 3C - 122x250 pixels, SSD1680 controller +weact_2p13in3c = WeActBWR( + "weact-2.13in-3c", + width=122, + height=250, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 4.2" 3C - 400x300 pixels, SSD1683 controller +weact_4p2in3c = WeActBWR( + "weact-4.2in-3c", + width=400, + height=300, + data_rate="10MHz", + minimum_update_interval="10s", +) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8c052acc87a..b78b945a24b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -645,11 +645,12 @@ def _is_framework_url(source: str) -> bool: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 6), - "latest": cv.Version(3, 3, 6), - "dev": cv.Version(3, 3, 6), + "recommended": cv.Version(3, 3, 7), + "latest": cv.Version(3, 3, 7), + "dev": cv.Version(3, 3, 7), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 7): cv.Version(55, 3, 37), cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 5): cv.Version(55, 3, 35), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), @@ -668,6 +669,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { + cv.Version(3, 3, 7): cv.Version(5, 5, 2), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -691,7 +693,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(5, 5, 2), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { - cv.Version(5, 5, 2): cv.Version(55, 3, 36), + cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), @@ -708,8 +710,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 36), - "latest": cv.Version(55, 3, 36), + "recommended": cv.Version(55, 3, 37), + "latest": cv.Version(55, 3, 37), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } @@ -1436,10 +1438,6 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) - # Set the uv cache inside the data dir so "Clean All" clears it. - # Avoids persistent corrupted cache from mid-stream download failures. - os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache")) - if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 8b066064f24..66367d63ae6 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1686,6 +1686,10 @@ BOARDS = { "name": "Espressif ESP32-C6-DevKitM-1", "variant": VARIANT_ESP32C6, }, + "esp32-c61-devkitc1": { + "name": "Espressif ESP32-C61-DevKitC-1 (4 MB Flash)", + "variant": VARIANT_ESP32C61, + }, "esp32-c61-devkitc1-n8r2": { "name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)", "variant": VARIANT_ESP32C61, @@ -1718,6 +1722,10 @@ BOARDS = { "name": "Espressif ESP32-P4 rev.300 generic", "variant": VARIANT_ESP32P4, }, + "esp32-p4_r3-evboard": { + "name": "Espressif ESP32-P4 Function EV Board v1.6 (rev.301)", + "variant": VARIANT_ESP32P4, + }, "esp32-pico-devkitm-2": { "name": "Espressif ESP32-PICO-DevKitM-2", "variant": VARIANT_ESP32, @@ -2554,6 +2562,10 @@ BOARDS = { "name": "XinaBox CW02", "variant": VARIANT_ESP32, }, + "yb_esp32s3_amp": { + "name": "YelloByte YB-ESP32-S3-AMP", + "variant": VARIANT_ESP32S3, + }, "yb_esp32s3_amp_v2": { "name": "YelloByte YB-ESP32-S3-AMP (Rev.2)", "variant": VARIANT_ESP32S3, @@ -2562,6 +2574,10 @@ BOARDS = { "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", "variant": VARIANT_ESP32S3, }, + "yb_esp32s3_dac": { + "name": "YelloByte YB-ESP32-S3-DAC", + "variant": VARIANT_ESP32S3, + }, "yb_esp32s3_drv": { "name": "YelloByte YB-ESP32-S3-DRV", "variant": VARIANT_ESP32S3, diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7d5af023b48..8d6fdc86f60 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -124,14 +124,11 @@ class ESP32Preferences : public ESPPreferences { return true; ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); - // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; esp_err_t last_err = ESP_OK; uint32_t last_key = 0; - // go through vector from back to front (makes erase easier/more efficient) - for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { - const auto &save = s_pending_save[i]; + for (const auto &save : s_pending_save) { char key_str[KEY_BUFFER_SIZE]; snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); @@ -150,8 +147,9 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } - s_pending_save.erase(s_pending_save.begin() + i); } + s_pending_save.clear(); + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 87b5e2b7383..acbe9d88fcc 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -369,42 +369,9 @@ bool ESP32BLE::ble_dismantle_() { } void ESP32BLE::loop() { - switch (this->state_) { - case BLE_COMPONENT_STATE_OFF: - case BLE_COMPONENT_STATE_DISABLED: - return; - case BLE_COMPONENT_STATE_DISABLE: { - ESP_LOGD(TAG, "Disabling"); - -#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - for (auto *ble_event_handler : this->ble_status_event_handlers_) { - ble_event_handler->ble_before_disabled_event_handler(); - } -#endif - - if (!ble_dismantle_()) { - ESP_LOGE(TAG, "Could not be dismantled"); - this->mark_failed(); - return; - } - this->state_ = BLE_COMPONENT_STATE_DISABLED; - return; - } - case BLE_COMPONENT_STATE_ENABLE: { - ESP_LOGD(TAG, "Enabling"); - this->state_ = BLE_COMPONENT_STATE_OFF; - - if (!ble_setup_()) { - ESP_LOGE(TAG, "Could not be set up"); - this->mark_failed(); - return; - } - - this->state_ = BLE_COMPONENT_STATE_ACTIVE; - return; - } - case BLE_COMPONENT_STATE_ACTIVE: - break; + if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) { + this->loop_handle_state_transition_not_active_(); + return; } BLEEvent *ble_event = this->ble_events_.pop(); @@ -520,6 +487,37 @@ void ESP32BLE::loop() { } } +void ESP32BLE::loop_handle_state_transition_not_active_() { + // Caller ensures state_ != ACTIVE + if (this->state_ == BLE_COMPONENT_STATE_DISABLE) { + ESP_LOGD(TAG, "Disabling"); + +#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT + for (auto *ble_event_handler : this->ble_status_event_handlers_) { + ble_event_handler->ble_before_disabled_event_handler(); + } +#endif + + if (!ble_dismantle_()) { + ESP_LOGE(TAG, "Could not be dismantled"); + this->mark_failed(); + return; + } + this->state_ = BLE_COMPONENT_STATE_DISABLED; + } else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) { + ESP_LOGD(TAG, "Enabling"); + this->state_ = BLE_COMPONENT_STATE_OFF; + + if (!ble_setup_()) { + ESP_LOGE(TAG, "Could not be set up"); + this->mark_failed(); + return; + } + + this->state_ = BLE_COMPONENT_STATE_ACTIVE; + } +} + // Helper function to load new event data based on type void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { event->load_gap_event(e, p); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 1999c870f8b..f1ab81b6dc6 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -155,6 +155,10 @@ class ESP32BLE : public Component { #endif static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + // Handle DISABLE and ENABLE transitions when not in the ACTIVE state. + // Other non-ACTIVE states (e.g. OFF, DISABLED) are currently treated as no-ops. + void __attribute__((noinline)) loop_handle_state_transition_not_active_(); + bool ble_setup_(); bool ble_dismantle_(); bool ble_pre_setup_(); diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522facb..cb494ed1bc1 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0f..a1b1ff94bb3 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789c..c2cdb1660ce 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -77,7 +77,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 1a19472c874..83bc842a3d3 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -338,8 +338,8 @@ void ESP32ImprovComponent::process_incoming_data_() { return; } wifi::WiFiAP sta{}; - sta.set_ssid(command.ssid); - sta.set_password(command.password); + sta.set_ssid(command.ssid.c_str()); + sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 272c7c81ba7..1076bcabdc9 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,8 +1,30 @@ from esphome.components import esp32 import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] +VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} + + +def validate_rmt_not_supported(rmt_only_keys): + """Validate that RMT-only config keys are not used on variants without RMT hardware.""" + rmt_only_keys = set(rmt_only_keys) + + def _validator(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in VARIANTS_NO_RMT: + unsupported = rmt_only_keys.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid( + f"{keys} not available on {variant} (no RMT hardware)" + ) + return config + + return _validator + def validate_clock_resolution(): def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 6d41f6b5b86..1c6943b0037 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, light +from esphome.components import esp32, esp32_rmt, light from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv @@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( + esp32.only_on_variant( + unsupported=list(esp32_rmt.VARIANTS_NO_RMT), + msg_prefix="ESP32 RMT LED strip", + ), light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index f037b881a8f..e749b1f6332 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -33,6 +33,10 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255; #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) +// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false). +// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need +// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so +// 64 words (256 bytes) suffices since most preferences go to RTC memory instead. #ifdef USE_ESP8266_PREFERENCES_FLASH static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; #else @@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { return true; } -// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data) -// This handles virtually all real-world preferences without heap allocation -static constexpr size_t PREF_BUFFER_WORDS = 16; +// Maximum buffer for any single preference - bounded by storage sizes. +// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words). +// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions. +static constexpr size_t PREF_MAX_BUFFER_WORDS = + ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS; class ESP8266PreferenceBackend : public ESPPreferenceBackend { public: @@ -141,15 +147,13 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { bool save(const uint8_t *data, size_t len) override { if (bytes_to_words(len) != this->length_words) return false; - const size_t buffer_size = static_cast(this->length_words) + 1; - SmallBufferWithHeapFallback buffer_alloc(buffer_size); - uint32_t *buffer = buffer_alloc.get(); + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; memset(buffer, 0, buffer_size * sizeof(uint32_t)); - memcpy(buffer, data, len); buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); - return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) : save_to_rtc(this->offset, buffer, buffer_size); } @@ -157,19 +161,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { bool load(uint8_t *data, size_t len) override { if (bytes_to_words(len) != this->length_words) return false; - const size_t buffer_size = static_cast(this->length_words) + 1; - SmallBufferWithHeapFallback buffer_alloc(buffer_size); - uint32_t *buffer = buffer_alloc.get(); - + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) : load_from_rtc(this->offset, buffer, buffer_size); if (!ret) return false; - if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) return false; - memcpy(data, buffer, len); return true; } diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 38489ceb2bd..52f5f44d410 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -130,11 +130,16 @@ ETHERNET_TYPES = { } # PHY types that need compile-time defines for conditional compilation +# Each RMII PHY type gets a define so unused PHY drivers are excluded by the linker _PHY_TYPE_TO_DEFINE = { + "LAN8720": "USE_ETHERNET_LAN8720", + "RTL8201": "USE_ETHERNET_RTL8201", + "DP83848": "USE_ETHERNET_DP83848", + "IP101": "USE_ETHERNET_IP101", + "JL1101": "USE_ETHERNET_JL1101", "KSZ8081": "USE_ETHERNET_KSZ8081", "KSZ8081RNA": "USE_ETHERNET_KSZ8081", "LAN8670": "USE_ETHERNET_LAN8670", - # Add other PHY types here only if they need conditional compilation } SPI_ETHERNET_TYPES = ["W5500", "DM9051"] diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index af7fed608b6..f9d98ad51b6 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -186,31 +186,43 @@ void EthernetComponent::setup() { } #endif #if CONFIG_ETH_USE_ESP32_EMAC +#ifdef USE_ETHERNET_LAN8720 case ETHERNET_TYPE_LAN8720: { this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_RTL8201 case ETHERNET_TYPE_RTL8201: { this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_DP83848 case ETHERNET_TYPE_DP83848: { this->phy_ = esp_eth_phy_new_dp83848(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_IP101 case ETHERNET_TYPE_IP101: { this->phy_ = esp_eth_phy_new_ip101(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_JL1101 case ETHERNET_TYPE_JL1101: { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; } +#endif +#ifdef USE_ETHERNET_KSZ8081 case ETHERNET_TYPE_KSZ8081: case ETHERNET_TYPE_KSZ8081RNA: { this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); break; } +#endif #ifdef USE_ETHERNET_LAN8670 case ETHERNET_TYPE_LAN8670: { this->phy_ = esp_eth_phy_new_lan867x(&phy_config); @@ -343,26 +355,32 @@ void EthernetComponent::loop() { void EthernetComponent::dump_config() { const char *eth_type; switch (this->type_) { +#ifdef USE_ETHERNET_LAN8720 case ETHERNET_TYPE_LAN8720: eth_type = "LAN8720"; break; - +#endif +#ifdef USE_ETHERNET_RTL8201 case ETHERNET_TYPE_RTL8201: eth_type = "RTL8201"; break; - +#endif +#ifdef USE_ETHERNET_DP83848 case ETHERNET_TYPE_DP83848: eth_type = "DP83848"; break; - +#endif +#ifdef USE_ETHERNET_IP101 case ETHERNET_TYPE_IP101: eth_type = "IP101"; break; - +#endif +#ifdef USE_ETHERNET_JL1101 case ETHERNET_TYPE_JL1101: eth_type = "JL1101"; break; - +#endif +#ifdef USE_ETHERNET_KSZ8081 case ETHERNET_TYPE_KSZ8081: eth_type = "KSZ8081"; break; @@ -370,19 +388,22 @@ void EthernetComponent::dump_config() { case ETHERNET_TYPE_KSZ8081RNA: eth_type = "KSZ8081RNA"; break; - +#endif +#if CONFIG_ETH_SPI_ETHERNET_W5500 case ETHERNET_TYPE_W5500: eth_type = "W5500"; break; - - case ETHERNET_TYPE_OPENETH: - eth_type = "OPENETH"; - break; - +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 case ETHERNET_TYPE_DM9051: eth_type = "DM9051"; break; - +#endif +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: + eth_type = "OPENETH"; + break; +#endif #ifdef USE_ETHERNET_LAN8670 case ETHERNET_TYPE_LAN8670: eth_type = "LAN8670"; @@ -686,16 +707,22 @@ void EthernetComponent::dump_connect_params_() { char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " IP Address: %s\n" " Hostname: '%s'\n" " Subnet: %s\n" " Gateway: %s\n" " DNS1: %s\n" - " DNS2: %s", + " DNS2: %s\n" + " MAC Address: %s\n" + " Is Full Duplex: %s\n" + " Link Speed: %u", network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), - network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf)); + network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf), + this->get_eth_mac_address_pretty_into_buffer(mac_buf), + YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); #if USE_NETWORK_IPV6 struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -706,14 +733,6 @@ void EthernetComponent::dump_connect_params_() { ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); } #endif /* USE_NETWORK_IPV6 */ - - char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - ESP_LOGCONFIG(TAG, - " MAC Address: %s\n" - " Is Full Duplex: %s\n" - " Link Speed: %u", - this->get_eth_mac_address_pretty_into_buffer(mac_buf), - YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); } #ifdef USE_ETHERNET_SPI @@ -837,13 +856,15 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { esp_err_t err; - constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; +#ifdef USE_ETHERNET_RTL8201 + constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); } +#endif ESP_LOGD(TAG, "Writing to PHY Register Address: 0x%02" PRIX32 "\n" @@ -852,11 +873,13 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); +#ifdef USE_ETHERNET_RTL8201 if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { ESP_LOGD(TAG, "Select PHY Register Page 0x00"); err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); } +#endif } #endif diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 5a2869c5a7d..b4859c308d1 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -110,6 +110,8 @@ class EthernetComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); + // Remove before 2026.9.0 + ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") std::string get_eth_mac_address_pretty(); const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index d70a2940bc9..c1e0a3dc2e1 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -221,12 +221,17 @@ void Fan::publish_state() { } // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. -constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; +constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB; optional Fan::restore_state_() { FanRestoreState recovered{}; this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); + if (!restored) { + // No valid saved data; ensure preset_mode sentinel is set + recovered.preset_mode = FanRestoreState::NO_PRESET; + } + switch (this->restore_mode_) { case FanRestoreMode::NO_RESTORE: return {}; @@ -264,6 +269,7 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { const auto &preset_modes = traits.supported_preset_modes(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 55d4ba8825e..2caf3a712a2 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -91,11 +91,13 @@ class FanCall { }; struct FanRestoreState { + static constexpr uint8_t NO_PRESET = UINT8_MAX; + bool state; int speed; bool oscillating; FanDirection direction; - uint8_t preset_mode; + uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 9bf58f9d1ea..38e4129e66f 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() { } void HBridgeFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index aee1f651bfd..e5b919e3800 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -9,9 +9,20 @@ #include "esphome/core/defines.h" #include "esphome/core/log.h" +// Include BearSSL error constants for TLS failure diagnostics +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; +#ifdef USE_ESP8266 +static constexpr int RX_BUFFER_SIZE = 512; +static constexpr int TX_BUFFER_SIZE = 512; +// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM +static constexpr int ESP8266_SSL_ERR_OOM = -1000; +#endif std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, @@ -47,7 +58,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique(); WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); - secure_client->setBufferSizes(512, 512); + secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE); secure_client->setInsecure(); } else { stream_ptr = std::make_unique(); @@ -107,13 +118,42 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); App.feed_wdt(); if (container->status_code < 0) { +#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS) + if (secure) { + WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); + int last_error = secure_client->getLastSSLError(); + + if (last_error != 0) { + const LogString *error_msg; + switch (last_error) { + case ESP8266_SSL_ERR_OOM: + error_msg = LOG_STR("Unable to allocate buffer memory"); + break; + case BR_ERR_TOO_LARGE: + error_msg = LOG_STR("Incoming TLS record does not fit in receive buffer (BR_ERR_TOO_LARGE)"); + break; + default: + error_msg = LOG_STR("Unknown SSL error"); + break; + } + ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); + if (last_error == ESP8266_SSL_ERR_OOM) { + ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(), + static_cast(RX_BUFFER_SIZE + TX_BUFFER_SIZE)); + } + } else { + ESP_LOGW(TAG, "Connection failure with no error code"); + } + } +#endif + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(), HTTPClient::errorToString(container->status_code).c_str()); + this->status_momentary_error("failed", 1000); container->end(); return nullptr; } - if (!is_success(container->status_code)) { ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c63e55d159c..85609bd31f4 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -90,16 +90,14 @@ void HttpRequestUpdate::update_task(void *params) { UPDATE_RETURN; } size_t read_index = container->get_bytes_read(); + size_t content_length = container->content_length; + + container->end(); + container.reset(); // Release ownership of the container's shared_ptr bool valid = false; - { // Ensures the response string falls out of scope and deallocates before the task ends - std::string response((char *) data, read_index); - allocator.deallocate(data, container->content_length); - - container->end(); - container.reset(); // Release ownership of the container's shared_ptr - - valid = json::parse_json(response, [this_update](JsonObject root) -> bool { + { // Scope to ensure JsonDocument is destroyed before deallocating buffer + valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool { if (!root[ESPHOME_F("name")].is() || !root[ESPHOME_F("version")].is() || !root[ESPHOME_F("builds")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); @@ -137,6 +135,7 @@ void HttpRequestUpdate::update_task(void *params) { return false; }); } + allocator.deallocate(data, content_length); if (!valid) { ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); @@ -157,17 +156,12 @@ void HttpRequestUpdate::update_task(void *params) { } } - { // Ensures the current version string falls out of scope and deallocates before the task ends - std::string current_version; #ifdef ESPHOME_PROJECT_VERSION - current_version = ESPHOME_PROJECT_VERSION; + this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION; #else - current_version = ESPHOME_VERSION; + this_update->update_info_.current_version = ESPHOME_VERSION; #endif - this_update->update_info_.current_version = current_version; - } - bool trigger_update_available = false; if (this_update->update_info_.latest_version.empty() || diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index b4d99439552..edceb9a3b13 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command switch (command.command) { case improv::WIFI_SETTINGS: { wifi::WiFiAP sta{}; - sta.set_ssid(command.ssid); - sta.set_password(command.password); + sta.set_ssid(command.ssid.c_str()); + sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); @@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command for (auto &scan : results) { if (scan.get_is_hidden()) continue; - const std::string &ssid = scan.get_ssid(); - if (std::find(networks.begin(), networks.end(), ssid) != networks.end()) + const char *ssid_cstr = scan.get_ssid().c_str(); + // Check if we've already sent this SSID + bool duplicate = false; + for (const auto &seen : networks) { + if (strcmp(seen.c_str(), ssid_cstr) == 0) { + duplicate = true; + break; + } + } + if (duplicate) continue; + // Only allocate std::string after confirming it's not a duplicate + std::string ssid(ssid_cstr); // Send each ssid separately to avoid overflowing the buffer char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null *int8_to_str(rssi_buf, scan.get_rssi()) = '\0'; std::vector data = improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false); this->send_response_(data); - networks.push_back(ssid); + networks.push_back(std::move(ssid)); } // Send empty response to signify the end of the list. std::vector data = diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 869d29f92e3..69f8bfc61a9 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -25,8 +25,13 @@ std::string build_json(const json_build_t &f) { } bool parse_json(const std::string &data, const json_parse_t &f) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + return parse_json(reinterpret_cast(data.c_str()), data.size(), f); +} + +bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - JsonDocument doc = parse_json(reinterpret_cast(data.c_str()), data.size()); + JsonDocument doc = parse_json(data, len); if (doc.overflowed() || doc.isNull()) return false; return f(doc.as()); diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 91cc84dc148..c472b9a9ec5 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -18,7 +18,10 @@ namespace json { // Build an allocator for the JSON Library using the RAMAllocator class // This is only compiled when PSRAM is enabled struct SpiRamAllocator : ArduinoJson::Allocator { - void *allocate(size_t size) override { return allocator_.allocate(size); } + void *allocate(size_t size) override { + RAMAllocator allocator; + return allocator.allocate(size); + } void deallocate(void *ptr) override { // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. @@ -31,11 +34,9 @@ struct SpiRamAllocator : ArduinoJson::Allocator { } void *reallocate(void *ptr, size_t new_size) override { - return allocator_.reallocate(static_cast(ptr), new_size); + RAMAllocator allocator; + return allocator.reallocate(static_cast(ptr), new_size); } - - protected: - RAMAllocator allocator_{RAMAllocator::NONE}; }; #endif @@ -50,6 +51,8 @@ std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); +/// Parse JSON from raw bytes and run the provided json parse function if it's valid. +bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f); /// Parse a JSON string and return the root JsonDocument (or an unbound object on error) JsonDocument parse_json(const uint8_t *data, size_t len); diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 553179beec7..01445da7ee7 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -193,14 +193,14 @@ def _notify_old_style(config): # The dev and latest branches will be at *least* this version, which is what matters. # Use GitHub releases directly to avoid PlatformIO moderation delays. ARDUINO_VERSIONS = { - "dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"), + "dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"), "latest": ( - cv.Version(1, 11, 0), - "https://github.com/libretiny-eu/libretiny.git#v1.11.0", + cv.Version(1, 12, 1), + "https://github.com/libretiny-eu/libretiny.git#v1.12.1", ), "recommended": ( - cv.Version(1, 11, 0), - "https://github.com/libretiny-eu/libretiny.git#v1.11.0", + cv.Version(1, 12, 1), + "https://github.com/libretiny-eu/libretiny.git#v1.12.1", ), } diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 5a60b535da3..8549631e460 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -114,14 +114,11 @@ class LibreTinyPreferences : public ESPPreferences { return true; ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); - // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; fdb_err_t last_err = FDB_NO_ERR; uint32_t last_key = 0; - // go through vector from back to front (makes erase easier/more efficient) - for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { - const auto &save = s_pending_save[i]; + for (const auto &save : s_pending_save) { char key_str[KEY_BUFFER_SIZE]; snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); @@ -141,8 +138,9 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } - s_pending_save.erase(s_pending_save.begin() + i); } + s_pending_save.clear(); + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 3b4e136ba51..0291b2c3c6a 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -270,22 +270,23 @@ LightColorValues LightCall::validate_() { if (this->has_state()) v.set_state(this->state_); -#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \ + // clamp_and_log_if_invalid already clamps in-place, so assign directly + // to avoid redundant clamp code from the setter being inlined. +#define VALIDATE_AND_APPLY(field, name_str, ...) \ if (this->has_##field()) { \ clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ - v.setter(this->field##_); \ + v.field##_ = this->field##_; \ } - VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness") - VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness") - VALIDATE_AND_APPLY(red, set_red, "Red") - VALIDATE_AND_APPLY(green, set_green, "Green") - VALIDATE_AND_APPLY(blue, set_blue, "Blue") - VALIDATE_AND_APPLY(white, set_white, "White") - VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white") - VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white") - VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(), - traits.get_max_mireds()) + VALIDATE_AND_APPLY(brightness, "Brightness") + VALIDATE_AND_APPLY(color_brightness, "Color brightness") + VALIDATE_AND_APPLY(red, "Red") + VALIDATE_AND_APPLY(green, "Green") + VALIDATE_AND_APPLY(blue, "Blue") + VALIDATE_AND_APPLY(white, "White") + VALIDATE_AND_APPLY(cold_white, "Cold white") + VALIDATE_AND_APPLY(warm_white, "Warm white") + VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) #undef VALIDATE_AND_APPLY diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 97756b9f26a..dc232633124 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -95,15 +95,18 @@ class LightColorValues { */ void normalize_color() { if (this->color_mode_ & ColorCapability::RGB) { - float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); + float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_)); + // Assign directly to avoid redundant clamp in set_red/green/blue. + // Values are guaranteed in [0,1]: inputs are already clamped to [0,1], + // and dividing by max_value (the largest) keeps results in [0,1]. if (max_value == 0.0f) { - this->set_red(1.0f); - this->set_green(1.0f); - this->set_blue(1.0f); + this->red_ = 1.0f; + this->green_ = 1.0f; + this->blue_ = 1.0f; } else { - this->set_red(this->get_red() / max_value); - this->set_green(this->get_green() / max_value); - this->set_blue(this->get_blue() / max_value); + this->red_ /= max_value; + this->green_ /= max_value; + this->blue_ /= max_value; } } } @@ -276,6 +279,8 @@ class LightColorValues { /// Set the warm white property of these light color values. In range 0.0 to 1.0. void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } + friend class LightCall; + protected: float state_; ///< ON / OFF, float for transition float brightness_; diff --git a/esphome/components/ln882x/boards.py b/esphome/components/ln882x/boards.py index 600371951d8..df44419ed21 100644 --- a/esphome/components/ln882x/boards.py +++ b/esphome/components/ln882x/boards.py @@ -154,28 +154,26 @@ LN882X_BOARD_PINS = { "A7": 21, }, "wb02a": { - "WIRE0_SCL_0": 7, - "WIRE0_SCL_1": 5, + "WIRE0_SCL_0": 1, + "WIRE0_SCL_1": 2, "WIRE0_SCL_2": 3, - "WIRE0_SCL_3": 10, - "WIRE0_SCL_4": 2, - "WIRE0_SCL_5": 1, - "WIRE0_SCL_6": 4, - "WIRE0_SCL_7": 5, - "WIRE0_SCL_8": 9, - "WIRE0_SCL_9": 24, - "WIRE0_SCL_10": 25, - "WIRE0_SDA_0": 7, - "WIRE0_SDA_1": 5, + "WIRE0_SCL_3": 4, + "WIRE0_SCL_4": 5, + "WIRE0_SCL_5": 7, + "WIRE0_SCL_6": 9, + "WIRE0_SCL_7": 10, + "WIRE0_SCL_8": 24, + "WIRE0_SCL_9": 25, + "WIRE0_SDA_0": 1, + "WIRE0_SDA_1": 2, "WIRE0_SDA_2": 3, - "WIRE0_SDA_3": 10, - "WIRE0_SDA_4": 2, - "WIRE0_SDA_5": 1, - "WIRE0_SDA_6": 4, - "WIRE0_SDA_7": 5, - "WIRE0_SDA_8": 9, - "WIRE0_SDA_9": 24, - "WIRE0_SDA_10": 25, + "WIRE0_SDA_3": 4, + "WIRE0_SDA_4": 5, + "WIRE0_SDA_5": 7, + "WIRE0_SDA_6": 9, + "WIRE0_SDA_7": 10, + "WIRE0_SDA_8": 24, + "WIRE0_SDA_9": 25, "SERIAL0_RX": 3, "SERIAL0_TX": 2, "SERIAL1_RX": 24, @@ -221,32 +219,32 @@ LN882X_BOARD_PINS = { "A1": 4, }, "wl2s": { - "WIRE0_SCL_0": 7, - "WIRE0_SCL_1": 12, - "WIRE0_SCL_2": 3, - "WIRE0_SCL_3": 10, - "WIRE0_SCL_4": 2, - "WIRE0_SCL_5": 0, - "WIRE0_SCL_6": 19, - "WIRE0_SCL_7": 11, - "WIRE0_SCL_8": 9, - "WIRE0_SCL_9": 24, - "WIRE0_SCL_10": 25, - "WIRE0_SCL_11": 5, - "WIRE0_SCL_12": 1, - "WIRE0_SDA_0": 7, - "WIRE0_SDA_1": 12, - "WIRE0_SDA_2": 3, - "WIRE0_SDA_3": 10, - "WIRE0_SDA_4": 2, - "WIRE0_SDA_5": 0, - "WIRE0_SDA_6": 19, - "WIRE0_SDA_7": 11, - "WIRE0_SDA_8": 9, - "WIRE0_SDA_9": 24, - "WIRE0_SDA_10": 25, - "WIRE0_SDA_11": 5, - "WIRE0_SDA_12": 1, + "WIRE0_SCL_0": 0, + "WIRE0_SCL_1": 1, + "WIRE0_SCL_2": 2, + "WIRE0_SCL_3": 3, + "WIRE0_SCL_4": 5, + "WIRE0_SCL_5": 7, + "WIRE0_SCL_6": 9, + "WIRE0_SCL_7": 10, + "WIRE0_SCL_8": 11, + "WIRE0_SCL_9": 12, + "WIRE0_SCL_10": 19, + "WIRE0_SCL_11": 24, + "WIRE0_SCL_12": 25, + "WIRE0_SDA_0": 0, + "WIRE0_SDA_1": 1, + "WIRE0_SDA_2": 2, + "WIRE0_SDA_3": 3, + "WIRE0_SDA_4": 5, + "WIRE0_SDA_5": 7, + "WIRE0_SDA_6": 9, + "WIRE0_SDA_7": 10, + "WIRE0_SDA_8": 11, + "WIRE0_SDA_9": 12, + "WIRE0_SDA_10": 19, + "WIRE0_SDA_11": 24, + "WIRE0_SDA_12": 25, "SERIAL0_RX": 3, "SERIAL0_TX": 2, "SERIAL1_RX": 24, @@ -301,24 +299,24 @@ LN882X_BOARD_PINS = { "A2": 1, }, "ln-02": { - "WIRE0_SCL_0": 11, - "WIRE0_SCL_1": 19, - "WIRE0_SCL_2": 3, - "WIRE0_SCL_3": 24, - "WIRE0_SCL_4": 2, - "WIRE0_SCL_5": 25, - "WIRE0_SCL_6": 1, - "WIRE0_SCL_7": 0, - "WIRE0_SCL_8": 9, - "WIRE0_SDA_0": 11, - "WIRE0_SDA_1": 19, - "WIRE0_SDA_2": 3, - "WIRE0_SDA_3": 24, - "WIRE0_SDA_4": 2, - "WIRE0_SDA_5": 25, - "WIRE0_SDA_6": 1, - "WIRE0_SDA_7": 0, - "WIRE0_SDA_8": 9, + "WIRE0_SCL_0": 0, + "WIRE0_SCL_1": 1, + "WIRE0_SCL_2": 2, + "WIRE0_SCL_3": 3, + "WIRE0_SCL_4": 9, + "WIRE0_SCL_5": 11, + "WIRE0_SCL_6": 19, + "WIRE0_SCL_7": 24, + "WIRE0_SCL_8": 25, + "WIRE0_SDA_0": 0, + "WIRE0_SDA_1": 1, + "WIRE0_SDA_2": 2, + "WIRE0_SDA_3": 3, + "WIRE0_SDA_4": 9, + "WIRE0_SDA_5": 11, + "WIRE0_SDA_6": 19, + "WIRE0_SDA_7": 24, + "WIRE0_SDA_8": 25, "SERIAL0_RX": 3, "SERIAL0_TX": 2, "SERIAL1_RX": 24, diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 40ceaec7dc6..b2952d79956 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All( bk72xx=768, ln882x=768, rtl87xx=768, + nrf52=768, ): cv.All( cv.only_on( - [PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX] + [ + PLATFORM_ESP32, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + PLATFORM_NRF52, + ] ), cv.validate_bytes, cv.Any( @@ -313,11 +320,13 @@ async def to_code(config): ) if CORE.is_esp32: cg.add(log.create_pthread_key()) - if CORE.is_esp32 or CORE.is_libretiny: + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") cg.add(log.init_log_buffer(task_log_buffer_size)) + if CORE.using_zephyr: + zephyr_add_prj_conf("MPSC_PBUF", True) elif CORE.is_host: cg.add(log.create_pthread_key()) cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") @@ -417,6 +426,7 @@ async def to_code(config): pass if CORE.is_nrf52: + zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True) if config[CONF_HARDWARE_UART] == UART0: zephyr_add_overlay("""&uart0 { status = "okay";};""") if config[CONF_HARDWARE_UART] == UART1: diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h new file mode 100644 index 00000000000..3d872782480 --- /dev/null +++ b/esphome/components/logger/log_buffer.h @@ -0,0 +1,190 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::logger { + +// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) +static constexpr uint16_t MAX_HEADER_SIZE = 128; + +// ANSI color code last digit (30-38 range, store only last digit to save RAM) +static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { + '\0', // NONE + '1', // ERROR (31 = red) + '3', // WARNING (33 = yellow) + '2', // INFO (32 = green) + '5', // CONFIG (35 = magenta) + '6', // DEBUG (36 = cyan) + '7', // VERBOSE (37 = gray) + '8', // VERY_VERBOSE (38 = white) +}; + +static constexpr char LOG_LEVEL_LETTER_CHARS[] = { + '\0', // NONE + 'E', // ERROR + 'W', // WARNING + 'I', // INFO + 'C', // CONFIG + 'D', // DEBUG + 'V', // VERBOSE (VERY_VERBOSE uses two 'V's) +}; + +// Buffer wrapper for log formatting functions +struct LogBuffer { + char *data; + uint16_t size; + uint16_t pos{0}; + // Replaces the null terminator with a newline for console output. + // Must be called after notify_listeners_() since listeners need null-terminated strings. + // Console output uses length-based writes (buf.pos), so null terminator is not needed. + void terminate_with_newline() { + if (this->pos < this->size) { + this->data[this->pos++] = '\n'; + } else if (this->size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + this->data[this->size - 1] = '\n'; + this->pos = this->size; + } + } + void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) { + // Early return if insufficient space - intentionally don't update pos to prevent partial writes + if (this->pos + MAX_HEADER_SIZE > this->size) + return; + + char *p = this->current_(); + + // Write ANSI color + this->write_ansi_color_(p, level); + + // Construct: [LEVEL][tag:line] + *p++ = '['; + if (level != 0) { + if (level >= 7) { + *p++ = 'V'; // VERY_VERBOSE = "VV" + *p++ = 'V'; + } else { + *p++ = LOG_LEVEL_LETTER_CHARS[level]; + } + } + *p++ = ']'; + *p++ = '['; + + // Copy tag + this->copy_string_(p, tag); + + *p++ = ':'; + + // Format line number without modulo operations + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + *p++ = '0' + thousands; + line -= thousands * 1000; + } + int hundreds = line / 100; + int remainder = line - hundreds * 100; + int tens = remainder / 10; + *p++ = '0' + hundreds; + *p++ = '0' + tens; + *p++ = '0' + (remainder - tens * 10); + *p++ = ']'; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) + // Write thread name with bold red color + if (thread_name != nullptr) { + this->write_ansi_color_(p, 1); // Bold red for thread name + *p++ = '['; + this->copy_string_(p, thread_name); + *p++ = ']'; + this->write_ansi_color_(p, level); // Restore original color + } +#endif + + *p++ = ':'; + *p++ = ' '; + + this->pos = p - this->data; + } + void HOT format_body(const char *format, va_list args) { + this->format_vsnprintf_(format, args); + this->finalize_(); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void HOT format_body_P(PGM_P format, va_list args) { + this->format_vsnprintf_P_(format, args); + this->finalize_(); + } +#endif + void write_body(const char *text, uint16_t text_length) { + this->write_(text, text_length); + this->finalize_(); + } + + private: + bool full_() const { return this->pos >= this->size; } + uint16_t remaining_() const { return this->size - this->pos; } + char *current_() { return this->data + this->pos; } + void write_(const char *value, uint16_t length) { + const uint16_t available = this->remaining_(); + const uint16_t copy_len = (length < available) ? length : available; + if (copy_len > 0) { + memcpy(this->current_(), value, copy_len); + this->pos += copy_len; + } + } + void finalize_() { + // Write color reset sequence + static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; + this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); + // Null terminate + this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; + } + void strip_trailing_newlines_() { + while (this->pos > 0 && this->data[this->pos - 1] == '\n') + this->pos--; + } + void process_vsnprintf_result_(int ret) { + if (ret < 0) + return; + const uint16_t rem = this->remaining_(); + this->pos += (ret >= rem) ? (rem - 1) : static_cast(ret); + this->strip_trailing_newlines_(); + } + void format_vsnprintf_(const char *format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args)); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void format_vsnprintf_P_(PGM_P format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); + } +#endif + // Write ANSI color escape sequence to buffer, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void write_ansi_color_(char *&p, uint8_t level) { + if (level == 0) + return; + // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes) + *p++ = '\033'; + *p++ = '['; + *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold + *p++ = ';'; + *p++ = '3'; + *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = 'm'; + } + // Copy string without null terminator, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void copy_string_(char *&p, const char *str) { + const size_t len = strlen(str); + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by + // piece + memcpy(p, str, len); + p += len; + } +}; + +} // namespace esphome::logger diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index bb20c403e53..e1b49bcb614 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -10,9 +10,9 @@ namespace esphome::logger { static const char *const TAG = "logger"; -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) -// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS) -// Main thread/task always uses direct buffer access for console output and callbacks +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS, +// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks // // For non-main threads/tasks: // - WITH task log buffer: Prefer sending to ring buffer for async processing @@ -31,6 +31,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Get task handle once - used for both main task check and passing to non-main thread handler TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); const bool is_main_task = (current_task == this->main_task_); +#elif (USE_ZEPHYR) + k_tid_t current_task = k_current_get(); + const bool is_main_task = (current_task == this->main_task_); #else // USE_HOST const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_); #endif @@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Host: pass a stack buffer for pthread_getname_np to write into. #if defined(USE_ESP32) || defined(USE_LIBRETINY) const char *thread_name = get_thread_name_(current_task); +#elif defined(USE_ZEPHYR) + char thread_name_buf[MAX_POINTER_REPRESENTATION]; + const char *thread_name = get_thread_name_(thread_name_buf, current_task); #else // USE_HOST char thread_name_buf[THREAD_NAME_BUF_SIZE]; const char *thread_name = this->get_thread_name_(thread_name_buf); @@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li // This is safe to call from any context including ISRs this->enable_loop_soon_any_context(); } -#endif // USE_ESPHOME_TASK_LOG_BUFFER - +#endif // Emergency console logging for non-main threads when ring buffer is full or disabled // This is a fallback mechanism to ensure critical log messages are visible // Note: This may cause interleaved/corrupted console output if multiple threads // log simultaneously, but it's better than losing important messages entirely #ifdef USE_HOST - if (!message_sent) { + if (!message_sent) +#else + if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console +#endif + { +#ifdef USE_HOST // Host always has console output - no baud_rate check needed static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512; #else - if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; #endif @@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li // RAII guard automatically resets on return } #else -// Implementation for single-task platforms (ESP8266, RP2040, Zephyr) -// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path. +// Implementation for single-task platforms (ESP8266, RP2040) // Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking. // Not a problem in practice yet since Zephyr has no API support (logs are console-only). void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; -#ifdef USE_ZEPHYR - char tmp[MAX_POINTER_REPRESENTATION]; - this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, - this->get_thread_name_(tmp)); -#else // Other single-task platforms don't have thread names, so pass nullptr + // Other single-task platforms don't have thread names, so pass nullptr this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr); -#endif } -#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY +#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. @@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate } #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer(size_t total_buffer_size) { -#ifdef USE_HOST // Host uses slot count instead of byte size - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size); -#elif defined(USE_ESP32) // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); -#elif defined(USE_LIBRETINY) - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size); -#endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// Zephyr needs loop working to check when CDC port is open +#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) // Start with loop disabled when using task buffer (unless using USB CDC on ESP32) // The loop will be enabled automatically when messages arrive this->disable_loop_when_buffer_empty_(); @@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) { } #endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::loop() { this->process_messages_(); } +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +void Logger::loop() { + this->process_messages_(); +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) + this->cdc_loop_(); +#endif +} #endif void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available if (this->log_buffer_->has_messages()) { -#ifdef USE_HOST - logger::TaskLogBufferHost::LogMessage *message; - while (this->log_buffer_->get_message_main_loop(&message)) { - const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; - this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text, - message->text_length, buf); - this->log_buffer_->release_message_main_loop(); - this->write_log_buffer_to_console_(buf); - } -#elif defined(USE_ESP32) logger::TaskLogBuffer::LogMessage *message; - const char *text; - void *received_token; - while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { + uint16_t text_length; + while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; - this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length, buf); - // Release the message to allow other tasks to use it as soon as possible - this->log_buffer_->release_message_main_loop(received_token); - this->write_log_buffer_to_console_(buf); - } -#elif defined(USE_LIBRETINY) - logger::TaskLogBufferLibreTiny::LogMessage *message; - const char *text; - while (this->log_buffer_->borrow_message_main_loop(&message, &text)) { - const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; - this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length, buf); + this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, + message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(); this->write_log_buffer_to_console_(buf); } -#endif } -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// Zephyr needs loop working to check when CDC port is open +#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index ecf032ee0ec..835542dd8f1 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -13,15 +13,11 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -#ifdef USE_HOST +#include "log_buffer.h" #include "task_log_buffer_host.h" -#elif defined(USE_ESP32) #include "task_log_buffer_esp32.h" -#elif defined(USE_LIBRETINY) #include "task_log_buffer_libretiny.h" -#endif -#endif +#include "task_log_buffer_zephyr.h" #ifdef USE_ARDUINO #if defined(USE_ESP8266) @@ -97,195 +93,10 @@ struct CStrCompare { }; #endif -// ANSI color code last digit (30-38 range, store only last digit to save RAM) -static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { - '\0', // NONE - '1', // ERROR (31 = red) - '3', // WARNING (33 = yellow) - '2', // INFO (32 = green) - '5', // CONFIG (35 = magenta) - '6', // DEBUG (36 = cyan) - '7', // VERBOSE (37 = gray) - '8', // VERY_VERBOSE (38 = white) -}; - -static constexpr char LOG_LEVEL_LETTER_CHARS[] = { - '\0', // NONE - 'E', // ERROR - 'W', // WARNING - 'I', // INFO - 'C', // CONFIG - 'D', // DEBUG - 'V', // VERBOSE (VERY_VERBOSE uses two 'V's) -}; - -// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) -static constexpr uint16_t MAX_HEADER_SIZE = 128; - -// "0x" + 2 hex digits per byte + '\0' -static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; - // Stack buffer size for retrieving thread/task names from the OS // macOS allows up to 64 bytes, Linux up to 16 static constexpr size_t THREAD_NAME_BUF_SIZE = 64; -// Buffer wrapper for log formatting functions -struct LogBuffer { - char *data; - uint16_t size; - uint16_t pos{0}; - // Replaces the null terminator with a newline for console output. - // Must be called after notify_listeners_() since listeners need null-terminated strings. - // Console output uses length-based writes (buf.pos), so null terminator is not needed. - void terminate_with_newline() { - if (this->pos < this->size) { - this->data[this->pos++] = '\n'; - } else if (this->size > 0) { - // Buffer was full - replace last char with newline to ensure it's visible - this->data[this->size - 1] = '\n'; - this->pos = this->size; - } - } - void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) { - // Early return if insufficient space - intentionally don't update pos to prevent partial writes - if (this->pos + MAX_HEADER_SIZE > this->size) - return; - - char *p = this->current_(); - - // Write ANSI color - this->write_ansi_color_(p, level); - - // Construct: [LEVEL][tag:line] - *p++ = '['; - if (level != 0) { - if (level >= 7) { - *p++ = 'V'; // VERY_VERBOSE = "VV" - *p++ = 'V'; - } else { - *p++ = LOG_LEVEL_LETTER_CHARS[level]; - } - } - *p++ = ']'; - *p++ = '['; - - // Copy tag - this->copy_string_(p, tag); - - *p++ = ':'; - - // Format line number without modulo operations - if (line > 999) [[unlikely]] { - int thousands = line / 1000; - *p++ = '0' + thousands; - line -= thousands * 1000; - } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - *p++ = '0' + hundreds; - *p++ = '0' + tens; - *p++ = '0' + (remainder - tens * 10); - *p++ = ']'; - -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) - // Write thread name with bold red color - if (thread_name != nullptr) { - this->write_ansi_color_(p, 1); // Bold red for thread name - *p++ = '['; - this->copy_string_(p, thread_name); - *p++ = ']'; - this->write_ansi_color_(p, level); // Restore original color - } -#endif - - *p++ = ':'; - *p++ = ' '; - - this->pos = p - this->data; - } - void HOT format_body(const char *format, va_list args) { - this->format_vsnprintf_(format, args); - this->finalize_(); - } -#ifdef USE_STORE_LOG_STR_IN_FLASH - void HOT format_body_P(PGM_P format, va_list args) { - this->format_vsnprintf_P_(format, args); - this->finalize_(); - } -#endif - void write_body(const char *text, uint16_t text_length) { - this->write_(text, text_length); - this->finalize_(); - } - - private: - bool full_() const { return this->pos >= this->size; } - uint16_t remaining_() const { return this->size - this->pos; } - char *current_() { return this->data + this->pos; } - void write_(const char *value, uint16_t length) { - const uint16_t available = this->remaining_(); - const uint16_t copy_len = (length < available) ? length : available; - if (copy_len > 0) { - memcpy(this->current_(), value, copy_len); - this->pos += copy_len; - } - } - void finalize_() { - // Write color reset sequence - static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; - this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); - // Null terminate - this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; - } - void strip_trailing_newlines_() { - while (this->pos > 0 && this->data[this->pos - 1] == '\n') - this->pos--; - } - void process_vsnprintf_result_(int ret) { - if (ret < 0) - return; - const uint16_t rem = this->remaining_(); - this->pos += (ret >= rem) ? (rem - 1) : static_cast(ret); - this->strip_trailing_newlines_(); - } - void format_vsnprintf_(const char *format, va_list args) { - if (this->full_()) - return; - this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args)); - } -#ifdef USE_STORE_LOG_STR_IN_FLASH - void format_vsnprintf_P_(PGM_P format, va_list args) { - if (this->full_()) - return; - this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); - } -#endif - // Write ANSI color escape sequence to buffer, updates pointer in place - // Caller is responsible for ensuring buffer has sufficient space - void write_ansi_color_(char *&p, uint8_t level) { - if (level == 0) - return; - // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes) - *p++ = '\033'; - *p++ = '['; - *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold - *p++ = ';'; - *p++ = '3'; - *p++ = LOG_LEVEL_COLOR_DIGIT[level]; - *p++ = 'm'; - } - // Copy string without null terminator, updates pointer in place - // Caller is responsible for ensuring buffer has sufficient space - void copy_string_(char *&p, const char *str) { - const size_t len = strlen(str); - // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by - // piece - memcpy(p, str, len); - p += len; - } -}; - #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -411,11 +222,14 @@ class Logger : public Component { bool &flag_; }; -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // Handles non-main thread logging only (~0.1% of calls) // thread_name is resolved by the caller from the task handle, avoiding redundant lookups void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, const char *thread_name); +#endif +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) + void cdc_loop_(); #endif void process_messages_(); void write_msg_(const char *msg, uint16_t len); @@ -534,13 +348,7 @@ class Logger : public Component { std::vector level_listeners_; // Log level change listeners #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER -#ifdef USE_HOST - logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed -#elif defined(USE_ESP32) logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed -#elif defined(USE_LIBRETINY) - logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed -#endif #endif // Group smaller types together at the end @@ -552,7 +360,7 @@ class Logger : public Component { #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) bool main_task_recursion_guard_{false}; #ifdef USE_LIBRETINY bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny @@ -595,8 +403,10 @@ class Logger : public Component { } #elif defined(USE_ZEPHYR) - const char *HOT get_thread_name_(std::span buff) { - k_tid_t current_task = k_current_get(); + const char *HOT get_thread_name_(std::span buff, k_tid_t current_task = nullptr) { + if (current_task == nullptr) { + current_task = k_current_get(); + } if (current_task == main_task_) { return nullptr; // Main task } @@ -635,7 +445,7 @@ class Logger : public Component { // Create RAII guard for non-main task recursion inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); } -#elif defined(USE_LIBRETINY) +#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // LibreTiny doesn't have FreeRTOS TLS, so use a simple approach: // - Main task uses dedicated boolean (same as ESP32) // - Non-main tasks share a single recursion guard @@ -643,6 +453,8 @@ class Logger : public Component { // - Recursion from logging within logging is the main concern // - Cross-task "recursion" is prevented by the buffer mutex anyway // - Missing a recursive call from another task is acceptable (falls back to direct output) + // + // Zephyr use __thread as TLS // Check if non-main task is already in recursion inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; } @@ -651,7 +463,8 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// Zephyr needs loop working to check when CDC port is open +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 1fc0acd5733..f565c5760cd 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -14,7 +14,7 @@ namespace esphome::logger { static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC -void Logger::loop() { +void Logger::cdc_loop_() { if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; } diff --git a/esphome/components/logger/task_log_buffer_esp32.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp index 56c0a4ae2d4..e747ddc4d8b 100644 --- a/esphome/components/logger/task_log_buffer_esp32.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() { } } -bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) { - if (message == nullptr || text == nullptr || received_token == nullptr) { +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { + if (this->current_token_) { return false; } @@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char ** } LogMessage *msg = static_cast(received_item); - *message = msg; - *text = msg->text_data(); - *received_token = received_item; + message = msg; + text_length = msg->text_length; + this->current_token_ = received_item; return true; } -void TaskLogBuffer::release_message_main_loop(void *token) { - if (token == nullptr) { +void TaskLogBuffer::release_message_main_loop() { + if (this->current_token_ == nullptr) { return; } - vRingbufferReturnItem(ring_buffer_, token); + vRingbufferReturnItem(ring_buffer_, this->current_token_); + this->current_token_ = nullptr; // Update counter to mark all messages as processed last_processed_counter_ = message_counter_.load(std::memory_order_relaxed); } diff --git a/esphome/components/logger/task_log_buffer_esp32.h b/esphome/components/logger/task_log_buffer_esp32.h index 6c1bafaeba3..88d72eacfc4 100644 --- a/esphome/components/logger/task_log_buffer_esp32.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -52,10 +52,10 @@ class TaskLogBuffer { ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the ring buffer, only call from main loop - bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token); + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); // NOT thread-safe - release a message buffer and update the counter, only call from main loop - void release_message_main_loop(void *token); + void release_message_main_loop(); // Thread-safe - send a message to the ring buffer from any thread bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, @@ -78,6 +78,7 @@ class TaskLogBuffer { // Atomic counter for message tracking (only differences matter) std::atomic message_counter_{0}; // Incremented when messages are committed mutable uint16_t last_processed_counter_{0}; // Tracks last processed message + void *current_token_{nullptr}; }; } // namespace esphome::logger diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp index 676686304a3..c2ab009db47 100644 --- a/esphome/components/logger/task_log_buffer_host.cpp +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -10,16 +10,16 @@ namespace esphome::logger { -TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) { +TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) { // Allocate message slots this->slots_ = std::make_unique(slot_count); } -TaskLogBufferHost::~TaskLogBufferHost() { +TaskLogBuffer::~TaskLogBuffer() { // unique_ptr handles cleanup automatically } -int TaskLogBufferHost::acquire_write_slot_() { +int TaskLogBuffer::acquire_write_slot_() { // Try to reserve a slot using compare-and-swap size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed); @@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() { } } -void TaskLogBufferHost::commit_write_slot_(int slot_index) { +void TaskLogBuffer::commit_write_slot_(int slot_index) { // Mark the slot as ready for reading this->slots_[slot_index].ready.store(true, std::memory_order_release); @@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) { } } -bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, - const char *format, va_list args) { +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { // Acquire a slot int slot_index = this->acquire_write_slot_(); if (slot_index < 0) { @@ -115,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, return true; } -bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) { - if (message == nullptr) { - return false; - } - +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { size_t current_read = this->read_index_.load(std::memory_order_relaxed); size_t current_write = this->write_index_.load(std::memory_order_acquire); @@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) { return false; } - *message = &msg; + message = &msg; + text_length = msg.text_length; return true; } -void TaskLogBufferHost::release_message_main_loop() { +void TaskLogBuffer::release_message_main_loop() { size_t current_read = this->read_index_.load(std::memory_order_relaxed); // Clear the ready flag diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h index f8e4ee7bee5..1d4d2b0ec1c 100644 --- a/esphome/components/logger/task_log_buffer_host.h +++ b/esphome/components/logger/task_log_buffer_host.h @@ -21,12 +21,12 @@ namespace esphome::logger { * * Threading Model: Multi-Producer Single-Consumer (MPSC) * - Multiple threads can safely call send_message_thread_safe() concurrently - * - Only the main loop thread calls get_message_main_loop() and release_message_main_loop() + * - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop() * * Producers (multiple threads) Consumer (main loop only) * │ │ * ▼ ▼ - * acquire_write_slot_() get_message_main_loop() + * acquire_write_slot_() bool borrow_message_main_loop() * CAS on reserve_index_ read write_index_ * │ check ready flag * ▼ │ @@ -48,7 +48,7 @@ namespace esphome::logger { * - Atomic CAS for slot reservation allows multiple producers without locks * - Single consumer (main loop) processes messages in order */ -class TaskLogBufferHost { +class TaskLogBuffer { public: // Default number of message slots - host has plenty of memory static constexpr size_t DEFAULT_SLOT_COUNT = 64; @@ -71,15 +71,16 @@ class TaskLogBufferHost { thread_name[0] = '\0'; text[0] = '\0'; } + inline char *text_data() { return this->text; } }; /// Constructor that takes the number of message slots - explicit TaskLogBufferHost(size_t slot_count); - ~TaskLogBufferHost(); + explicit TaskLogBuffer(size_t slot_count); + ~TaskLogBuffer(); // NOT thread-safe - get next message from buffer, only call from main loop // Returns true if a message was retrieved, false if buffer is empty - bool get_message_main_loop(LogMessage **message); + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); // NOT thread-safe - release the message after processing, only call from main loop void release_message_main_loop(); diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index 5a22857dcb1..5969f6fb401 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -8,7 +8,7 @@ namespace esphome::logger { -TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) { +TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { this->size_ = total_buffer_size; // Allocate memory for the circular buffer using ESPHome's RAM allocator RAMAllocator allocator; @@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) { this->mutex_ = xSemaphoreCreateMutex(); } -TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() { +TaskLogBuffer::~TaskLogBuffer() { if (this->mutex_ != nullptr) { vSemaphoreDelete(this->mutex_); this->mutex_ = nullptr; @@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() { } } -size_t TaskLogBufferLibreTiny::available_contiguous_space() const { +size_t TaskLogBuffer::available_contiguous_space() const { if (this->head_ >= this->tail_) { // head is ahead of or equal to tail // Available space is from head to end, plus from start to tail @@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const { } } -bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) { - if (message == nullptr || text == nullptr) { - return false; - } - +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { // Check if buffer was initialized successfully if (this->mutex_ == nullptr || this->storage_ == nullptr) { return false; @@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons this->tail_ = 0; msg = reinterpret_cast(this->storage_); } - *message = msg; - *text = msg->text_data(); + message = msg; + text_length = msg->text_length; this->current_message_size_ = message_total_size(msg->text_length); // Keep mutex held until release_message_main_loop() return true; } -void TaskLogBufferLibreTiny::release_message_main_loop() { +void TaskLogBuffer::release_message_main_loop() { // Advance tail past the current message this->tail_ += this->current_message_size_; @@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() { xSemaphoreGive(this->mutex_); } -bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, - const char *thread_name, const char *format, va_list args) { +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) va_list args_copy; va_copy(args_copy, args); diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index bf315e828a5..c065065fe78 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -40,7 +40,7 @@ namespace esphome::logger { * - Volatile counter enables fast has_messages() without lock overhead * - If message doesn't fit at end, padding is added and message wraps to start */ -class TaskLogBufferLibreTiny { +class TaskLogBuffer { public: // Structure for a log message header (text data follows immediately after) struct LogMessage { @@ -60,11 +60,11 @@ class TaskLogBufferLibreTiny { static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF; // Constructor that takes a total buffer size - explicit TaskLogBufferLibreTiny(size_t total_buffer_size); - ~TaskLogBufferLibreTiny(); + explicit TaskLogBuffer(size_t total_buffer_size); + ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the buffer, only call from main loop - bool borrow_message_main_loop(LogMessage **message, const char **text); + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); // NOT thread-safe - release a message buffer, only call from main loop void release_message_main_loop(); diff --git a/esphome/components/logger/task_log_buffer_zephyr.cpp b/esphome/components/logger/task_log_buffer_zephyr.cpp new file mode 100644 index 00000000000..44d12d08a3b --- /dev/null +++ b/esphome/components/logger/task_log_buffer_zephyr.cpp @@ -0,0 +1,116 @@ +#ifdef USE_ZEPHYR + +#include "task_log_buffer_zephyr.h" + +namespace esphome::logger { + +__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +static inline uint32_t total_size_in_32bit_words(uint16_t text_length) { + // Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment) + return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t); +} + +static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) { + return total_size_in_32bit_words(reinterpret_cast(item)->text_length); +} + +TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { + // alignment to 4 bytes + total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t); + this->mpsc_config_.buf = new uint32_t[total_buffer_size]; + this->mpsc_config_.size = total_buffer_size; + this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE; + this->mpsc_config_.get_wlen = get_wlen, + + mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_); +} + +TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; } + +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { + // First, calculate the exact length needed using a null buffer (no actual writing) + va_list args_copy; + va_copy(args_copy, args); + int ret = vsnprintf(nullptr, 0, format, args_copy); + va_end(args_copy); + + if (ret <= 0) { + return false; // Formatting error or empty message + } + + // Calculate actual text length (capped to maximum size) + static constexpr size_t MAX_TEXT_SIZE = 255; + size_t text_length = (static_cast(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret; + size_t total_size = total_size_in_32bit_words(text_length); + auto *msg = reinterpret_cast(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT)); + if (msg == nullptr) { + return false; + } + msg->level = level; + msg->tag = tag; + msg->line = line; + strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); + msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination + + // Format the message text directly into the acquired memory + // We add 1 to text_length to ensure space for null terminator during formatting + char *text_area = msg->text_data(); + ret = vsnprintf(text_area, text_length + 1, format, args); + + // Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output) + if (ret < 0) { + // this should not happen, vsnprintf was called already once + // fill with '\n' to not call mpsc_pbuf_free from producer + // it will be trimmed anyway + for (size_t i = 0; i < text_length; ++i) { + text_area[i] = '\n'; + } + text_area[text_length] = 0; + // do not return false to free the buffer from main thread + } + + msg->text_length = text_length; + + mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast(msg)); + return true; +} + +bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { + if (this->current_token_) { + return false; + } + + this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_); + + if (this->current_token_ == nullptr) { + return false; + } + + // we claimed buffer already, const_cast is safe here + message = const_cast(reinterpret_cast(this->current_token_)); + + text_length = message->text_length; + // Remove trailing newlines + while (text_length > 0 && message->text_data()[text_length - 1] == '\n') { + text_length--; + } + + return true; +} + +void TaskLogBuffer::release_message_main_loop() { + if (this->current_token_ == nullptr) { + return; + } + mpsc_pbuf_free(&this->log_buffer_, this->current_token_); + this->current_token_ = nullptr; +} +#endif // USE_ESPHOME_TASK_LOG_BUFFER + +} // namespace esphome::logger + +#endif // USE_ZEPHYR diff --git a/esphome/components/logger/task_log_buffer_zephyr.h b/esphome/components/logger/task_log_buffer_zephyr.h new file mode 100644 index 00000000000..cc2ed1f6872 --- /dev/null +++ b/esphome/components/logger/task_log_buffer_zephyr.h @@ -0,0 +1,66 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome::logger { + +// "0x" + 2 hex digits per byte + '\0' +static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; + +extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +class TaskLogBuffer { + public: + // Structure for a log message header (text data follows immediately after) + struct LogMessage { + MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after + uint16_t line; // Source code line number + uint8_t level; // Log level (0-7) +#if defined(CONFIG_THREAD_NAME) + char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads) +#else + char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads) +#endif + const char *tag; // We store the pointer, assuming tags are static + uint16_t text_length; // Length of the message text (up to ~64KB) + + // Methods for accessing message contents + inline char *text_data() { return reinterpret_cast(this) + sizeof(LogMessage); } + }; + // Constructor that takes a total buffer size + explicit TaskLogBuffer(size_t total_buffer_size); + ~TaskLogBuffer(); + + // Check if there are messages ready to be processed using an atomic counter for performance + inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); } + + // Get the total buffer size in bytes + inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); } + + // NOT thread-safe - borrow a message from the ring buffer, only call from main loop + bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); + + // NOT thread-safe - release a message buffer and update the counter, only call from main loop + void release_message_main_loop(); + + // Thread-safe - send a message to the ring buffer from any thread + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args); + + protected: + mpsc_pbuf_buffer_config mpsc_config_{}; + mpsc_pbuf_buffer log_buffer_{}; + const mpsc_pbuf_generic *current_token_{}; +}; + +#endif // USE_ESPHOME_TASK_LOG_BUFFER + +} // namespace esphome::logger + +#endif // USE_ZEPHYR diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index f696cfff1ce..32f8f16ec1e 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -45,9 +45,28 @@ class MDNSComponent : public Component { void setup() override; void dump_config() override; -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO) - void loop() override; -#endif + // Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040). + // + // On these platforms, MDNS.update() calls _process(true) which only manages timer-driven + // state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS + // packets are handled independently via the lwIP onRx UDP callback and are NOT affected + // by how often update() is called. + // + // The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1). + // Announcement intervals are 1000ms and cache TTL checks are on the order of seconds + // to minutes. A 50ms polling interval provides sufficient resolution for all timers + // while completely removing mDNS from the per-iteration loop list. + // + // In steady state (after the ~8 second boot probe/announce phase completes), update() + // checks timers that are set to never expire, making every call pure overhead. + // + // Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this + // interval is safe in production. + // + // By using set_interval() instead of overriding loop(), the component is excluded from + // the main loop list via has_overridden_loop(), eliminating all per-iteration overhead + // including virtual dispatch. + static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50; float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } #ifdef USE_MDNS_EXTRA_SERVICES diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index dcbe5ebd526..295a408cbd4 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -36,9 +36,14 @@ static void register_esp8266(MDNSComponent *, StaticVectorsetup_buffers_and_register_(register_esp8266); } - -void MDNSComponent::loop() { MDNS.update(); } +void MDNSComponent::setup() { + this->setup_buffers_and_register_(register_esp8266); + // Schedule MDNS.update() via set_interval() instead of overriding loop(). + // This removes the component from the per-iteration loop list entirely, + // eliminating virtual dispatch overhead on every main loop cycle. + // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); +} void MDNSComponent::on_shutdown() { MDNS.close(); diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index e4a9b60cdbf..05d991c1fad 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -35,9 +35,14 @@ static void register_rp2040(MDNSComponent *, StaticVectorsetup_buffers_and_register_(register_rp2040); } - -void MDNSComponent::loop() { MDNS.update(); } +void MDNSComponent::setup() { + this->setup_buffers_and_register_(register_rp2040); + // Schedule MDNS.update() via set_interval() instead of overriding loop(). + // This removes the component from the per-iteration loop list entirely, + // eliminating virtual dispatch overhead on every main loop cycle. + // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); +} void MDNSComponent::on_shutdown() { MDNS.close(); diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index e4e090da2e3..cc86101f5ee 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,4 +1,17 @@ -from esphome.components.mipi import DriverChip +from esphome.components.mipi import ( + ETMOD, + FRMCTR2, + GMCTRN1, + GMCTRP1, + IFCTR, + MODE_RGB, + PWCTR1, + PWCTR3, + PWCTR4, + PWCTR5, + PWSET, + DriverChip, +) import esphome.config_validation as cv from .amoled import CO5300 @@ -129,6 +142,16 @@ DriverChip( ), ), ) +ST7789P = DriverChip( + "ST7789P", + # Max supported dimensions + width=240, + height=320, + # SPI: RGB layout + color_order=MODE_RGB, + invert_colors=True, + draw_rounding=1, +) ILI9488_A.extend( "PICO-RESTOUCH-LCD-3.5", @@ -162,3 +185,61 @@ AXS15231.extend( cs_pin=9, reset_pin=21, ) + +# Waveshare 1.83-v2 +# +# Do not use on 1.83-v1: Vendor warning on different chip! +ST7789P.extend( + "WAVESHARE-1.83-V2", + # Panel size smaller than ST7789 max allowed + width=240, + height=284, + # Vendor specific init derived from vendor sample code + # "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp" + # Compatible MIT license, see esphome/LICENSE file. + initsequence=( + (FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33), + (ETMOD, 0x35), + (0xBB, 0x19), + (PWCTR1, 0x2C), + (PWCTR3, 0x01), + (PWCTR4, 0x12), + (PWCTR5, 0x20), + (IFCTR, 0x0F), + (PWSET, 0xA4, 0xA1), + ( + GMCTRP1, + 0xD0, + 0x04, + 0x0D, + 0x11, + 0x13, + 0x2B, + 0x3F, + 0x54, + 0x4C, + 0x18, + 0x0D, + 0x0B, + 0x1F, + 0x23, + ), + ( + GMCTRN1, + 0xD0, + 0x04, + 0x0C, + 0x11, + 0x13, + 0x2C, + 0x3F, + 0x44, + 0x51, + 0x2F, + 0x1F, + 0x1F, + 0x20, + 0x23, + ), + ), +) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index d5034612573..90b423c3867 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -170,10 +170,8 @@ void MQTTClientComponent::send_device_info_() { void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { (void) tag; if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = std::string(message, message_len), - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); + this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos, + this->log_message_.retain); } } #endif diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 158cfb55529..81b2e0e8dbc 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -300,9 +300,11 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); + // Reusable stack buffer for topic construction (avoids heap allocation per publish) + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; // mode bool success = true; - if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode))) + if (!this->publish(this->get_mode_state_topic_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode))) success = false; int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -311,68 +313,70 @@ bool MQTTClimateComponent::publish_state_() { if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) && !std::isnan(this->device_->current_temperature)) { len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy); - if (!this->publish(this->get_current_temperature_state_topic(), payload, len)) + if (!this->publish(this->get_current_temperature_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy); - if (!this->publish(this->get_target_temperature_low_state_topic(), payload, len)) + if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len)) success = false; len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy); - if (!this->publish(this->get_target_temperature_high_state_topic(), payload, len)) + if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len)) success = false; } else { len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy); - if (!this->publish(this->get_target_temperature_state_topic(), payload, len)) + if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) && !std::isnan(this->device_->current_humidity)) { len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0); - if (!this->publish(this->get_current_humidity_state_topic(), payload, len)) + if (!this->publish(this->get_current_humidity_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) && !std::isnan(this->device_->target_humidity)) { len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0); - if (!this->publish(this->get_target_humidity_state_topic(), payload, len)) + if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len)) success = false; } if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { if (this->device_->has_custom_preset()) { - if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset())) + if (!this->publish(this->get_preset_state_topic_to(topic_buf), this->device_->get_custom_preset().c_str())) success = false; } else if (this->device_->preset.has_value()) { - if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value()))) + if (!this->publish(this->get_preset_state_topic_to(topic_buf), + climate_preset_to_mqtt_str(this->device_->preset.value()))) success = false; - } else if (!this->publish(this->get_preset_state_topic(), "")) { + } else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) { success = false; } } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action))) + if (!this->publish(this->get_action_state_topic_to(topic_buf), climate_action_to_mqtt_str(this->device_->action))) success = false; } if (traits.get_supports_fan_modes()) { if (this->device_->has_custom_fan_mode()) { - if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode())) + if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), this->device_->get_custom_fan_mode().c_str())) success = false; } else if (this->device_->fan_mode.has_value()) { - if (!this->publish(this->get_fan_mode_state_topic(), + if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value()))) success = false; - } else if (!this->publish(this->get_fan_mode_state_topic(), "")) { + } else if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), "")) { success = false; } } if (traits.get_supports_swing_modes()) { - if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) + if (!this->publish(this->get_swing_mode_state_topic_to(topic_buf), + climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) success = false; } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index eb98158647e..853712940ad 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -59,6 +59,11 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b \ public: \ void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \ + StringRef get_##name##_##type##_topic_to(std::span buf) const { \ + if (!this->custom_##name##_##type##_topic_.empty()) \ + return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \ + return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \ + } \ std::string get_##name##_##type##_topic() const { \ if (this->custom_##name##_##type##_topic_.empty()) \ return this->get_default_topic_for_(#name "/" #type); \ diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 50e68ecbcc9..97520040942 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,17 +67,26 @@ void MQTTCoverComponent::dump_config() { auto traits = this->cover_->get_traits(); bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt(); LOG_MQTT_COMPONENT(true, has_command_topic); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; +#ifdef USE_MQTT_COVER_JSON + if (this->use_json_format_) { + ESP_LOGCONFIG(TAG, " JSON State Payload: YES"); + } else { +#endif + if (traits.get_supports_position()) { + ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic_to(topic_buf).c_str()); + } + if (traits.get_supports_tilt()) { + ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic_to(topic_buf).c_str()); + } +#ifdef USE_MQTT_COVER_JSON + } +#endif if (traits.get_supports_position()) { - ESP_LOGCONFIG(TAG, - " Position State Topic: '%s'\n" - " Position Command Topic: '%s'", - this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str()); + ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str()); } if (traits.get_supports_tilt()) { - ESP_LOGCONFIG(TAG, - " Tilt State Topic: '%s'\n" - " Tilt Command Topic: '%s'", - this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str()); + ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str()); } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { @@ -92,13 +101,33 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; } - if (traits.get_supports_position()) { - root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); - root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); - } - if (traits.get_supports_tilt()) { - root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic(); - root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic(); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; +#ifdef USE_MQTT_COVER_JSON + if (this->use_json_format_) { + // JSON mode: all state published to state_topic as JSON, use templates to extract + root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}"); + if (traits.get_supports_position()) { + root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf); + root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}"); + root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf); + } + if (traits.get_supports_tilt()) { + root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf); + root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}"); + root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf); + } + } else +#endif + { + // Standard mode: separate topics for position and tilt + if (traits.get_supports_position()) { + root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf); + root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf); + } + if (traits.get_supports_tilt()) { + root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf); + root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf); + } } if (traits.get_supports_tilt() && !traits.get_supports_position()) { config.command_topic = false; @@ -111,20 +140,36 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_; bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } bool MQTTCoverComponent::publish_state() { auto traits = this->cover_->get_traits(); + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; +#ifdef USE_MQTT_COVER_JSON + if (this->use_json_format_) { + return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, + traits.get_supports_position()); + if (traits.get_supports_position()) { + root[ESPHOME_F("position")] = static_cast(roundf(this->cover_->position * 100)); + } + if (traits.get_supports_tilt()) { + root[ESPHOME_F("tilt")] = static_cast(roundf(this->cover_->tilt * 100)); + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + }); + } +#endif bool success = true; if (traits.get_supports_position()) { char pos[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0); - if (!this->publish(this->get_position_state_topic(), pos, len)) + if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len)) success = false; } if (traits.get_supports_tilt()) { char pos[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0); - if (!this->publish(this->get_tilt_state_topic(), pos, len)) + if (!this->publish(this->get_tilt_state_topic_to(topic_buf), pos, len)) success = false; } - char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (!this->publish(this->get_state_topic_to_(topic_buf), cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, traits.get_supports_position()))) diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index 13582d14d16..f801af5d128 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -27,12 +27,18 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { bool publish_state(); void dump_config() override; +#ifdef USE_MQTT_COVER_JSON + void set_use_json_format(bool use_json_format) { this->use_json_format_ = use_json_format; } +#endif protected: const char *component_type() const override; const EntityBase *get_entity() const override; cover::Cover *cover_; +#ifdef USE_MQTT_COVER_JSON + bool use_json_format_{false}; +#endif }; } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 84d51895c50..ae2b8c46002 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -173,19 +173,20 @@ bool MQTTFanComponent::publish_state() { this->publish(this->get_state_topic_to_(topic_buf), state_s); bool failed = false; if (this->state_->get_traits().supports_direction()) { - bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction)); + bool success = this->publish(this->get_direction_state_topic_to(topic_buf), + fan_direction_to_mqtt_str(this->state_->direction)); failed = failed || !success; } if (this->state_->get_traits().supports_oscillation()) { - bool success = - this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating)); + bool success = this->publish(this->get_oscillation_state_topic_to(topic_buf), + fan_oscillation_to_mqtt_str(this->state_->oscillating)); failed = failed || !success; } auto traits = this->state_->get_traits(); if (traits.supports_speed()) { char buf[12]; size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed); - bool success = this->publish(this->get_speed_level_state_topic(), buf, len); + bool success = this->publish(this->get_speed_level_state_topic_to(topic_buf), buf, len); failed = failed || !success; } return !failed; diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 16e25f6a8a1..2b9f02858b5 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -87,13 +87,13 @@ bool MQTTValveComponent::send_initial_state() { return this->publish_state(); } bool MQTTValveComponent::publish_state() { auto traits = this->valve_->get_traits(); bool success = true; + char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (traits.get_supports_position()) { char pos[VALUE_ACCURACY_MAX_LEN]; size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0); - if (!this->publish(this->get_position_state_topic(), pos, len)) + if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len)) success = false; } - char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (!this->publish(this->get_state_topic_to_(topic_buf), valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position, traits.get_supports_position()))) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 7a6d25bc7d4..057244e03d4 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -2,97 +2,34 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS +from esphome.components import runtime_image +from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent -from esphome.components.image import ( - CONF_INVERT_ALPHA, - CONF_TRANSPARENCY, - IMAGE_SCHEMA, - Image_, - get_image_type_enum, - get_transparency_enum, - validate_settings, -) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, - CONF_DITHER, - CONF_FILE, - CONF_FORMAT, CONF_ID, CONF_ON_ERROR, - CONF_RESIZE, CONF_TRIGGER_ID, - CONF_TYPE, CONF_URL, ) from esphome.core import Lambda -AUTO_LOAD = ["image"] +AUTO_LOAD = ["image", "runtime_image"] DEPENDENCIES = ["display", "http_request"] CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" -CONF_PLACEHOLDER = "placeholder" CONF_UPDATE = "update" _LOGGER = logging.getLogger(__name__) online_image_ns = cg.esphome_ns.namespace("online_image") -ImageFormat = online_image_ns.enum("ImageFormat") - - -class Format: - def __init__(self, image_type): - self.image_type = image_type - - @property - def enum(self): - return getattr(ImageFormat, self.image_type) - - def actions(self): - pass - - -class BMPFormat(Format): - def __init__(self): - super().__init__("BMP") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") - - -class JPEGFormat(Format): - def __init__(self): - super().__init__("JPEG") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT") - cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") - - -class PNGFormat(Format): - def __init__(self): - super().__init__("PNG") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.1.0") - - -IMAGE_FORMATS = { - x.image_type: x - for x in ( - BMPFormat(), - JPEGFormat(), - PNGFormat(), - ) -} -IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]}) - -OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) +OnlineImage = online_image_ns.class_( + "OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage +) # Actions SetUrlAction = online_image_ns.class_( @@ -111,29 +48,17 @@ DownloadErrorTrigger = online_image_ns.class_( ) -def remove_options(*options): - return { - cv.Optional(option): cv.invalid( - f"{option} is an invalid option for online_image" - ) - for option in options - } - - ONLINE_IMAGE_SCHEMA = ( - IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) + runtime_image.runtime_image_schema(OnlineImage) .extend( { - cv.Required(CONF_ID): cv.declare_id(OnlineImage), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), # Online Image specific options + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), - cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema( rp2040_arduino=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0), ), - validate_settings, + runtime_image.validate_runtime_image_settings, ) ) @@ -199,23 +124,21 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): - image_format = IMAGE_FORMATS[config[CONF_FORMAT]] - image_format.actions() + # Use the enhanced helper function to get all runtime image parameters + settings = await runtime_image.process_runtime_image_config(config) url = config[CONF_URL] - width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = get_transparency_enum(config[CONF_TRANSPARENCY]) - var = cg.new_Pvariable( config[CONF_ID], url, - width, - height, - image_format.enum, - get_image_type_enum(config[CONF_TYPE]), - transparent, + settings.width, + settings.height, + settings.format_enum, + settings.image_type_enum, + settings.transparent, + settings.placeholder or cg.nullptr, config[CONF_BUFFER_SIZE], - config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", + settings.byte_order_big_endian, ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) @@ -227,10 +150,6 @@ async def to_code(config): else: cg.add(var.add_request_header(key, value)) - if placeholder_id := config.get(CONF_PLACEHOLDER): - placeholder = await cg.get_variable(placeholder_id) - cg.add(var.set_placeholder(placeholder)) - for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "cached")], conf) diff --git a/esphome/components/online_image/download_buffer.cpp b/esphome/components/online_image/download_buffer.cpp new file mode 100644 index 00000000000..2ec6365aa17 --- /dev/null +++ b/esphome/components/online_image/download_buffer.cpp @@ -0,0 +1,57 @@ +#include "download_buffer.h" +#include "esphome/core/log.h" +#include + +namespace esphome::online_image { + +static const char *const TAG = "online_image.download_buffer"; + +DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { + RAMAllocator allocator; + this->buffer_ = allocator.allocate(size); + this->reset(); + if (!this->buffer_) { + ESP_LOGE(TAG, "Initial allocation of download buffer failed!"); + this->size_ = 0; + } +} + +uint8_t *DownloadBuffer::data(size_t offset) { + if (offset > this->size_) { + ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); + return this->buffer_; + } + return this->buffer_ + offset; +} + +size_t DownloadBuffer::read(size_t len) { + if (len >= this->unread_) { + this->unread_ = 0; + return 0; + } + this->unread_ -= len; + memmove(this->data(), this->data(len), this->unread_); + return this->unread_; +} + +size_t DownloadBuffer::resize(size_t size) { + if (this->size_ >= size) { + // Avoid useless reallocations; if the buffer is big enough, don't reallocate. + return this->size_; + } + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + this->buffer_ = allocator.allocate(size); + this->reset(); + if (this->buffer_) { + this->size_ = size; + return size; + } else { + ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size, + allocator.get_max_free_block_size()); + this->size_ = 0; + return 0; + } +} + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/download_buffer.h b/esphome/components/online_image/download_buffer.h new file mode 100644 index 00000000000..73061b23b5e --- /dev/null +++ b/esphome/components/online_image/download_buffer.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome::online_image { + +/** + * @brief Buffer for managing downloaded data. + * + * This class provides a buffer for downloading data with tracking of + * unread bytes and dynamic resizing capabilities. + */ +class DownloadBuffer { + public: + DownloadBuffer(size_t size); + ~DownloadBuffer() { + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + } + + uint8_t *data(size_t offset = 0); + uint8_t *append() { return this->data(this->unread_); } + + size_t unread() const { return this->unread_; } + size_t size() const { return this->size_; } + size_t free_capacity() const { return this->size_ - this->unread_; } + + size_t read(size_t len); + size_t write(size_t len) { + this->unread_ += len; + return this->unread_; + } + + void reset() { this->unread_ = 0; } + size_t resize(size_t size); + + protected: + uint8_t *buffer_; + size_t size_; + /** Total number of downloaded bytes not yet read. */ + size_t unread_; +}; + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp deleted file mode 100644 index 0ab7dadde3f..00000000000 --- a/esphome/components/online_image/image_decoder.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "image_decoder.h" -#include "online_image.h" - -#include "esphome/core/log.h" - -namespace esphome { -namespace online_image { - -static const char *const TAG = "online_image.decoder"; - -bool ImageDecoder::set_size(int width, int height) { - bool success = this->image_->resize_(width, height) > 0; - this->x_scale_ = static_cast(this->image_->buffer_width_) / width; - this->y_scale_ = static_cast(this->image_->buffer_height_) / height; - return success; -} - -void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { - auto width = std::min(this->image_->buffer_width_, static_cast(std::ceil((x + w) * this->x_scale_))); - auto height = std::min(this->image_->buffer_height_, static_cast(std::ceil((y + h) * this->y_scale_))); - for (int i = x * this->x_scale_; i < width; i++) { - for (int j = y * this->y_scale_; j < height; j++) { - this->image_->draw_pixel_(i, j, color); - } - } -} - -DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { - this->buffer_ = this->allocator_.allocate(size); - this->reset(); - if (!this->buffer_) { - ESP_LOGE(TAG, "Initial allocation of download buffer failed!"); - this->size_ = 0; - } -} - -uint8_t *DownloadBuffer::data(size_t offset) { - if (offset > this->size_) { - ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); - return this->buffer_; - } - return this->buffer_ + offset; -} - -size_t DownloadBuffer::read(size_t len) { - this->unread_ -= len; - if (this->unread_ > 0) { - memmove(this->data(), this->data(len), this->unread_); - } - return this->unread_; -} - -size_t DownloadBuffer::resize(size_t size) { - if (this->size_ >= size) { - // Avoid useless reallocations; if the buffer is big enough, don't reallocate. - return this->size_; - } - this->allocator_.deallocate(this->buffer_, this->size_); - this->buffer_ = this->allocator_.allocate(size); - this->reset(); - if (this->buffer_) { - this->size_ = size; - return size; - } else { - ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size, - this->allocator_.get_max_free_block_size()); - this->size_ = 0; - return 0; - } -} - -} // namespace online_image -} // namespace esphome diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 4e2ecc2c773..6f5b82116db 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -1,6 +1,6 @@ #include "online_image.h" - #include "esphome/core/log.h" +#include static const char *const TAG = "online_image"; static const char *const ETAG_HEADER_NAME = "etag"; @@ -8,142 +8,82 @@ static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match"; static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified"; static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since"; -#include "image_decoder.h" +namespace esphome::online_image { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT -#include "bmp_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT -#include "jpeg_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT -#include "png_image.h" -#endif - -namespace esphome { -namespace online_image { - -using image::ImageType; - -inline bool is_color_on(const Color &color) { - // This produces the most accurate monochrome conversion, but is slightly slower. - // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; - - // Approximation using fast integer computations; produces acceptable results - // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B - return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; -} - -OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) - : Image(nullptr, 0, 0, type, transparency), - buffer_(nullptr), - download_buffer_(download_buffer_size), - download_buffer_initial_size_(download_buffer_size), - format_(format), - fixed_width_(width), - fixed_height_(height), - is_big_endian_(is_big_endian) { +OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, + image::ImageType type, image::Transparency transparency, image::Image *placeholder, + uint32_t buffer_size, bool is_big_endian) + : RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height), + download_buffer_(buffer_size), + download_buffer_initial_size_(buffer_size) { this->set_url(url); } -void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { - if (this->data_start_) { - Image::draw(x, y, display, color_on, color_off); - } else if (this->placeholder_) { - this->placeholder_->draw(x, y, display, color_on, color_off); +bool OnlineImage::validate_url_(const std::string &url) { + if (url.empty()) { + ESP_LOGE(TAG, "URL is empty"); + return false; } -} - -void OnlineImage::release() { - if (this->buffer_) { - ESP_LOGV(TAG, "Deallocating old buffer"); - this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); - this->data_start_ = nullptr; - this->buffer_ = nullptr; - this->width_ = 0; - this->height_ = 0; - this->buffer_width_ = 0; - this->buffer_height_ = 0; - this->last_modified_ = ""; - this->etag_ = ""; - this->end_connection_(); + if (url.length() > 2048) { + ESP_LOGE(TAG, "URL is too long"); + return false; } -} - -size_t OnlineImage::resize_(int width_in, int height_in) { - int width = this->fixed_width_; - int height = this->fixed_height_; - if (this->is_auto_resize_()) { - width = width_in; - height = height_in; - if (this->width_ != width && this->height_ != height) { - this->release(); - } + if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) { + ESP_LOGE(TAG, "URL must start with http:// or https://"); + return false; } - size_t new_size = this->get_buffer_size_(width, height); - if (this->buffer_) { - // Buffer already allocated => no need to resize - return new_size; - } - ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); - this->buffer_ = this->allocator_.allocate(new_size); - if (this->buffer_ == nullptr) { - ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, - this->allocator_.get_max_free_block_size()); - this->end_connection_(); - return 0; - } - this->buffer_width_ = width; - this->buffer_height_ = height; - this->width_ = width; - ESP_LOGV(TAG, "New size: (%d, %d)", width, height); - return new_size; + return true; } void OnlineImage::update() { - if (this->decoder_) { + if (this->is_decoding()) { ESP_LOGW(TAG, "Image already being updated."); return; } - ESP_LOGI(TAG, "Updating image %s", this->url_.c_str()); - std::list headers = {}; - - http_request::Header accept_header; - accept_header.name = "Accept"; - std::string accept_mime_type; - switch (this->format_) { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - case ImageFormat::BMP: - accept_mime_type = "image/bmp"; - break; -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT - case ImageFormat::JPEG: - accept_mime_type = "image/jpeg"; - break; -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - case ImageFormat::PNG: - accept_mime_type = "image/png"; - break; -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - default: - accept_mime_type = "image/*"; + if (!this->validate_url_(this->url_)) { + ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str()); + this->download_error_callback_.call(); + return; } - accept_header.value = accept_mime_type + ",*/*;q=0.8"; + ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); + + std::list headers; + + // Add caching headers if we have them if (!this->etag_.empty()) { - headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_}); + headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_}); } - if (!this->last_modified_.empty()) { - headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); + headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); } - headers.push_back(accept_header); + // Add Accept header based on image format + const char *accept_mime_type; + switch (this->get_format()) { +#ifdef USE_RUNTIME_IMAGE_BMP + case runtime_image::BMP: + accept_mime_type = "image/bmp,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case runtime_image::JPEG: + accept_mime_type = "image/jpeg,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case runtime_image::PNG: + accept_mime_type = "image/png,*/*;q=0.8"; + break; +#endif + default: + accept_mime_type = "image/*,*/*;q=0.8"; + break; + } + headers.push_back({"Accept", accept_mime_type}); + // User headers last so they can override any of the above for (auto &header : this->request_headers_) { headers.push_back(http_request::Header{header.first, header.second.value()}); } @@ -175,186 +115,117 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Starting download"); size_t total_size = this->downloader_->content_length; -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - if (this->format_ == ImageFormat::BMP) { - ESP_LOGD(TAG, "Allocating BMP decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); + // Initialize decoder with the known format + if (!this->begin_decode(total_size)) { + ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format()); + this->end_connection_(); + this->download_error_callback_.call(); + return; } -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT - if (this->format_ == ImageFormat::JPEG) { - ESP_LOGD(TAG, "Allocating JPEG decoder"); - this->decoder_ = esphome::make_unique(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - if (this->format_ == ImageFormat::PNG) { - ESP_LOGD(TAG, "Allocating PNG decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - if (!this->decoder_) { - ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_); - this->end_connection_(); - this->download_error_callback_.call(); - return; - } - auto prepare_result = this->decoder_->prepare(total_size); - if (prepare_result < 0) { - this->end_connection_(); - this->download_error_callback_.call(); - return; + // JPEG requires the complete image in the download buffer before decoding + if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) { + this->download_buffer_.resize(total_size); } + ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); this->start_time_ = ::time(nullptr); + this->enable_loop(); } void OnlineImage::loop() { - if (!this->decoder_) { + if (!this->is_decoding()) { // Not decoding at the moment => nothing to do. this->disable_loop(); return; } - if (!this->downloader_ || this->decoder_->is_finished()) { - this->data_start_ = buffer_; - this->width_ = buffer_width_; - this->height_ = buffer_height_; - ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), - this->width_, this->height_); - ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_)); + + if (!this->downloader_) { + ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + // Check if download is complete — use decoder's format-specific completion check + // to handle both known content-length and chunked transfer encoding + if (this->is_decode_finished() || (this->downloader_->content_length > 0 && + this->downloader_->get_bytes_read() >= this->downloader_->content_length && + this->download_buffer_.unread() == 0)) { + // Finalize decoding + this->end_decode(); + + ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(), + (uint32_t) (::time(nullptr) - this->start_time_)); + + // Save caching headers this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME); + this->download_finished_callback_.call(false); this->end_connection_(); return; } - if (this->downloader_ == nullptr) { - ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); - return; - } + + // Download and decode more data size_t available = this->download_buffer_.free_capacity(); - if (available) { - // Some decoders need to fully download the image before downloading. - // In case of huge images, don't wait blocking until the whole image has been downloaded, - // use smaller chunks + if (available > 0) { + // Download in chunks to avoid blocking available = std::min(available, this->download_buffer_initial_size_); auto len = this->downloader_->read(this->download_buffer_.append(), available); + if (len > 0) { this->download_buffer_.write(len); - auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); - if (fed < 0) { - ESP_LOGE(TAG, "Error when decoding image."); + + // Feed data to decoder + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + + if (consumed < 0) { + ESP_LOGE(TAG, "Error decoding image: %d", consumed); this->end_connection_(); this->download_error_callback_.call(); return; } - this->download_buffer_.read(fed); - } - } -} -void OnlineImage::map_chroma_key(Color &color) { - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (color.g == 1 && color.r == 0 && color.b == 0) { - color.g = 0; - } - if (color.w < 0x80) { - color.r = 0; - color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1; - color.b = 0; - } - } -} - -void OnlineImage::draw_pixel_(int x, int y, Color color) { - if (!this->buffer_) { - ESP_LOGE(TAG, "Buffer not allocated!"); - return; - } - if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { - ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); - return; - } - uint32_t pos = this->get_position_(x, y); - switch (this->type_) { - case ImageType::IMAGE_TYPE_BINARY: { - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - pos = x + y * width_8; - auto bitno = 0x80 >> (pos % 8u); - pos /= 8u; - auto on = is_color_on(color); - if (this->has_transparency() && color.w < 0x80) - on = false; - if (on) { - this->buffer_[pos] |= bitno; - } else { - this->buffer_[pos] &= ~bitno; + if (consumed > 0) { + this->download_buffer_.read(consumed); } - break; + } else if (len < 0) { + ESP_LOGE(TAG, "Error downloading image: %d", len); + this->end_connection_(); + this->download_error_callback_.call(); + return; } - case ImageType::IMAGE_TYPE_GRAYSCALE: { - auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (gray == 1) { - gray = 0; - } - if (color.w < 0x80) { - gray = 1; - } - } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - if (color.w != 0xFF) - gray = color.w; - } - this->buffer_[pos] = gray; - break; - } - case ImageType::IMAGE_TYPE_RGB565: { - this->map_chroma_key(color); - uint16_t col565 = display::ColorUtil::color_to_565(color); - if (this->is_big_endian_) { - this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); - this->buffer_[pos + 1] = static_cast(col565 & 0xFF); - } else { - this->buffer_[pos + 0] = static_cast(col565 & 0xFF); - this->buffer_[pos + 1] = static_cast((col565 >> 8) & 0xFF); - } - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 2] = color.w; - } - break; - } - case ImageType::IMAGE_TYPE_RGB: { - this->map_chroma_key(color); - this->buffer_[pos + 0] = color.r; - this->buffer_[pos + 1] = color.g; - this->buffer_[pos + 2] = color.b; - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 3] = color.w; - } - break; + } else { + // Buffer is full, need to decode some data first + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + if (consumed > 0) { + this->download_buffer_.read(consumed); + } else if (consumed < 0) { + ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } else { + // Decoder can't process more data, might need complete image + // This is normal for JPEG which needs complete data + ESP_LOGV(TAG, "Decoder waiting for more data"); } } } void OnlineImage::end_connection_() { + // Abort any in-progress decode to free decoder resources. + // Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release(). + if (this->is_decoding()) { + RuntimeImage::release(); + } if (this->downloader_) { this->downloader_->end(); this->downloader_ = nullptr; } - this->decoder_.reset(); this->download_buffer_.reset(); -} - -bool OnlineImage::validate_url_(const std::string &url) { - if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { - ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); - return false; - } - return true; + this->disable_loop(); } void OnlineImage::add_on_finished_callback(std::function &&callback) { @@ -365,5 +236,16 @@ void OnlineImage::add_on_error_callback(std::function &&callback) { this->download_error_callback_.add(std::move(callback)); } -} // namespace online_image -} // namespace esphome +void OnlineImage::release() { + // Clear cache headers + this->etag_ = ""; + this->last_modified_ = ""; + + // End any active connection + this->end_connection_(); + + // Call parent's release to free the image buffer + RuntimeImage::release(); +} + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 12d409ca29b..c7c80c7c667 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -1,15 +1,14 @@ #pragma once +#include "download_buffer.h" #include "esphome/components/http_request/http_request.h" -#include "esphome/components/image/image.h" +#include "esphome/components/runtime_image/runtime_image.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "image_decoder.h" - -namespace esphome { -namespace online_image { +namespace esphome::online_image { using t_http_codes = enum { HTTP_CODE_OK = 200, @@ -17,27 +16,13 @@ using t_http_codes = enum { HTTP_CODE_NOT_FOUND = 404, }; -/** - * @brief Format that the image is encoded with. - */ -enum ImageFormat { - /** Automatically detect from MIME type. Not supported yet. */ - AUTO, - /** JPEG format. */ - JPEG, - /** PNG format. */ - PNG, - /** BMP format. */ - BMP, -}; - /** * @brief Download an image from a given URL, and decode it using the specified decoder. * The image will then be stored in a buffer, so that it can be re-displayed without the * need to re-download or re-decode. */ class OnlineImage : public PollingComponent, - public image::Image, + public runtime_image::RuntimeImage, public Parented { public: /** @@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent, * @param url URL to download the image from. * @param width Desired width of the target image area. * @param height Desired height of the target image area. - * @param format Format that the image is encoded in (@see ImageFormat). + * @param format Format that the image is encoded in (@see runtime_image::ImageFormat). + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. * @param buffer_size Size of the buffer used to download the image. + * @param is_big_endian Whether the image is stored in big-endian format. */ - OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); - - void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type, + image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size, + bool is_big_endian = false); void update() override; void loop() override; - void map_chroma_key(Color &color); /** Set the URL to download the image from. */ void set_url(const std::string &url) { @@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent, /** Add the request header */ template void add_request_header(const std::string &header, V value) { - this->request_headers_.push_back(std::pair >(header, value)); + this->request_headers_.push_back(std::pair>(header, value)); } - /** - * @brief Set the image that needs to be shown as long as the downloaded image - * is not available. - * - * @param placeholder Pointer to the (@link Image) to show as placeholder. - */ - void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } - /** * Release the buffer storing the image. The image will need to be downloaded again * to be able to be displayed. */ void release(); - /** - * Resize the download buffer - * - * @param size The new size for the download buffer. - */ - size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); } - void add_on_finished_callback(std::function &&callback); void add_on_error_callback(std::function &&callback); protected: bool validate_url_(const std::string &url); - - RAMAllocator allocator_{}; - - uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } - int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; } - - int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } - - ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } - - /** - * @brief Resize the image buffer to the requested dimensions. - * - * The buffer will be allocated if not existing. - * If the dimensions have been fixed in the yaml config, the buffer will be created - * with those dimensions and not resized, even on request. - * Otherwise, the old buffer will be deallocated and a new buffer with the requested - * allocated - * - * @param width - * @param height - * @return 0 if no memory could be allocated, the size of the new buffer otherwise. - */ - size_t resize_(int width, int height); - - /** - * @brief Draw a pixel into the buffer. - * - * This is used by the decoder to fill the buffer that will later be displayed - * by the `draw` method. This will internally convert the supplied 32 bit RGBA - * color into the requested image storage format. - * - * @param x Horizontal pixel position. - * @param y Vertical pixel position. - * @param color 32 bit color to put into the pixel. - */ - void draw_pixel_(int x, int y, Color color); - void end_connection_(); CallbackManager download_finished_callback_{}; CallbackManager download_error_callback_{}; std::shared_ptr downloader_{nullptr}; - std::unique_ptr decoder_{nullptr}; - - uint8_t *buffer_; DownloadBuffer download_buffer_; /** * This is the *initial* size of the download buffer, not the current size. @@ -153,40 +84,10 @@ class OnlineImage : public PollingComponent, */ size_t download_buffer_initial_size_; - const ImageFormat format_; - image::Image *placeholder_{nullptr}; - std::string url_{""}; - std::vector > > request_headers_; + std::vector>> request_headers_; - /** width requested on configuration, or 0 if non specified. */ - const int fixed_width_; - /** height requested on configuration, or 0 if non specified. */ - const int fixed_height_; - /** - * Whether the image is stored in big-endian format. - * This is used to determine how to store 16 bit colors in the buffer. - */ - bool is_big_endian_; - /** - * Actual width of the current image. If fixed_width_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_width()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_width_; - /** - * Actual height of the current image. If fixed_height_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_height()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_height_; /** * The value of the ETag HTTP header provided in the last response. */ @@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent, std::string last_modified_ = ""; time_t start_time_; - - friend bool ImageDecoder::set_size(int width, int height); - friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); }; template class OnlineImageSetUrlAction : public Action { @@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> { } }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::online_image diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index a9aff3cce49..ec212d1f681 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -104,7 +104,7 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION - otLinkModeConfig link_mode_config = {0}; + otLinkModeConfig link_mode_config{}; #if CONFIG_OPENTHREAD_FTD link_mode_config.mRxOnWhenIdle = true; link_mode_config.mDeviceType = true; @@ -115,7 +115,7 @@ void OpenThreadComponent::ot_main() { ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); } uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); } link_mode_config.mRxOnWhenIdle = this->poll_period == 0; link_mode_config.mDeviceType = false; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 73a6c151640..f98c0f93223 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -76,7 +76,7 @@ class PN532 : public PollingComponent { std::unique_ptr read_mifare_classic_tag_(nfc::NfcTagUid &uid); bool read_mifare_classic_block_(uint8_t block_num, std::vector &data); - bool write_mifare_classic_block_(uint8_t block_num, std::vector &data); + bool write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid); bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid); @@ -88,7 +88,7 @@ class PN532 : public PollingComponent { uint16_t read_mifare_ultralight_capacity_(); bool find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - bool write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + bool write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index b762d5d9361..cca6acd96d1 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn532.h" @@ -106,10 +107,10 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u } bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; bool error = false; @@ -118,20 +119,20 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { continue; } if (block != 0) { - if (!this->write_mifare_classic_block_(block, blank_buffer)) { + if (!this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); error = true; } } - if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) { + if (!this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 1); error = true; } - if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) { + if (!this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 2); error = true; } - if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) { + if (!this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 3); error = true; } @@ -141,28 +142,28 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { } bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!"); return false; } - if (!this->write_mifare_classic_block_(1, block_1_data)) + if (!this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size())) return false; - if (!this->write_mifare_classic_block_(2, block_2_data)) + if (!this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size())) return false; - if (!this->write_mifare_classic_block_(3, block_3_trailer)) + if (!this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size())) return false; ESP_LOGD(TAG, "Sector 0 formatted to NDEF"); @@ -172,36 +173,36 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { return false; } if (block == 4) { - if (!this->write_mifare_classic_block_(block, empty_ndef_message)) { + if (!this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); } } else { - if (!this->write_mifare_classic_block_(block, blank_block)) { + if (!this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); } } - if (!this->write_mifare_classic_block_(block + 1, blank_block)) { + if (!this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 1); } - if (!this->write_mifare_classic_block_(block + 2, blank_block)) { + if (!this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 2); } - if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) { + if (!this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size())) { ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3); } } return true; } -bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { - std::vector data({ +bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { + std::vector cmd({ PN532_COMMAND_INDATAEXCHANGE, 0x01, // One card nfc::MIFARE_CMD_WRITE, block_num, }); - data.insert(data.end(), write_data.begin(), write_data.end()); - if (!this->write_command_(data)) { + cmd.insert(cmd.end(), data, data + len); + if (!this->write_command_(cmd)) { ESP_LOGE(TAG, "Error writing block %d", block_num); return false; } @@ -243,8 +244,7 @@ bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *mes } } - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (!this->write_mifare_classic_block_(current_block, data)) { + if (!this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE)) { return false; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index 01e41df5c09..a8a8e2d5735 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn532.h" @@ -143,8 +144,7 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage * uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (!this->write_mifare_ultralight_page_(current_page, data)) { + if (!this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE)) { return false; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -157,25 +157,25 @@ bool PN532::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (!this->write_mifare_ultralight_page_(i, blank_data)) { + if (!this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size())) { return false; } } return true; } -bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { - std::vector data({ +bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { + std::vector cmd({ PN532_COMMAND_INDATAEXCHANGE, 0x01, // One card nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num, }); - data.insert(data.end(), write_data.begin(), write_data.end()); - if (!this->write_command_(data)) { + cmd.insert(cmd.end(), write_data, write_data + len); + if (!this->write_command_(cmd)) { ESP_LOGE(TAG, "Error writing page %u", page_num); return false; } diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index a5dcef9f991..5feba17d211 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -236,7 +236,7 @@ class PN7150 : public nfc::Nfcc, public Component { uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); - uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t sect_to_auth_(uint8_t block_num); uint8_t format_mifare_classic_mifare_(); @@ -250,7 +250,7 @@ class PN7150 : public nfc::Nfcc, public Component { uint16_t read_mifare_ultralight_capacity_(); uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp index dee81b610a3..61434cdb285 100644 --- a/esphome/components/pn7150/pn7150_mifare_classic.cpp +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn7150.h" @@ -139,10 +140,10 @@ uint8_t PN7150::sect_to_auth_(const uint8_t block_num) { } uint8_t PN7150::format_mifare_classic_mifare_() { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; auto status = nfc::STATUS_OK; @@ -151,20 +152,20 @@ uint8_t PN7150::format_mifare_classic_mifare_() { continue; } if (block != 0) { - if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -174,30 +175,30 @@ uint8_t PN7150::format_mifare_classic_mifare_() { } uint8_t PN7150::format_mifare_classic_ndef_() { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } @@ -210,25 +211,26 @@ uint8_t PN7150::format_mifare_classic_ndef_() { return nfc::STATUS_FAILED; } if (block == 4) { - if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) != + nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } else { - if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -236,7 +238,7 @@ uint8_t PN7150::format_mifare_classic_ndef_() { return status; } -uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { +uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); @@ -248,7 +250,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { @@ -294,8 +296,8 @@ uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index 166065f6c14..46f5dba2b76 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -144,8 +145,8 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -158,19 +159,19 @@ uint8_t PN7150::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } } return nfc::STATUS_OK; } -uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { +uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; - payload.insert(payload.end(), write_data.begin(), write_data.end()); + payload.insert(payload.end(), write_data, write_data + len); nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 572fab3351a..9f2d10c2d53 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -253,7 +253,7 @@ class PN7160 : public nfc::Nfcc, public Component { uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); - uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t sect_to_auth_(uint8_t block_num); uint8_t format_mifare_classic_mifare_(); @@ -267,7 +267,7 @@ class PN7160 : public nfc::Nfcc, public Component { uint16_t read_mifare_ultralight_capacity_(); uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp index 57d2042eaa4..710a7198c6b 100644 --- a/esphome/components/pn7160/pn7160_mifare_classic.cpp +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn7160.h" @@ -139,10 +140,10 @@ uint8_t PN7160::sect_to_auth_(const uint8_t block_num) { } uint8_t PN7160::format_mifare_classic_mifare_() { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; auto status = nfc::STATUS_OK; @@ -151,20 +152,20 @@ uint8_t PN7160::format_mifare_classic_mifare_() { continue; } if (block != 0) { - if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -174,30 +175,30 @@ uint8_t PN7160::format_mifare_classic_mifare_() { } uint8_t PN7160::format_mifare_classic_ndef_() { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } @@ -210,25 +211,26 @@ uint8_t PN7160::format_mifare_classic_ndef_() { return nfc::STATUS_FAILED; } if (block == 4) { - if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) != + nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } else { - if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -236,7 +238,7 @@ uint8_t PN7160::format_mifare_classic_ndef_() { return status; } -uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { +uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; @@ -248,7 +250,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { @@ -294,8 +296,8 @@ uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index c473ff48d92..9dc8d3dd2d5 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -144,8 +145,8 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -158,19 +159,19 @@ uint8_t PN7160::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } } return nfc::STATUS_OK; } -uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { +uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; - payload.insert(payload.end(), write_data.begin(), write_data.end()); + payload.insert(payload.end(), write_data, write_data + len); nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 007deb66e5b..433e1f0b7e3 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -38,8 +38,7 @@ void PulseMeterSensor::setup() { } void PulseMeterSensor::loop() { - // Reset the count in get before we pass it back to the ISR as set - this->get_->count_ = 0; + State state; { // Lock the interrupt so the interrupt code doesn't interfere with itself @@ -58,31 +57,35 @@ void PulseMeterSensor::loop() { } this->last_pin_val_ = current; - // Swap out set and get to get the latest state from the ISR - std::swap(this->set_, this->get_); + // Get the latest state from the ISR and reset the count in the ISR + state.last_detected_edge_us_ = this->state_.last_detected_edge_us_; + state.last_rising_edge_us_ = this->state_.last_rising_edge_us_; + state.count_ = this->state_.count_; + this->state_.count_ = 0; } const uint32_t now = micros(); // If an edge was peeked, repay the debt - if (this->peeked_edge_ && this->get_->count_ > 0) { + if (this->peeked_edge_ && state.count_ > 0) { this->peeked_edge_ = false; - this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_--; } - // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early - if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ && - now - this->get_->last_rising_edge_us_ >= this->filter_us_) { + // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early. + // Wait for the debt to be repaid before counting another unprocessed edge early. + if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ && + now - state.last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; - this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; - this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.last_detected_edge_us_ = state.last_rising_edge_us_; + state.count_++; } // Check if we detected a pulse this loop - if (this->get_->count_ > 0) { + if (state.count_ > 0) { // Keep a running total of pulses if a total sensor is configured if (this->total_sensor_ != nullptr) { - this->total_pulses_ += this->get_->count_; + this->total_pulses_ += state.count_; const uint32_t total = this->total_pulses_; this->total_sensor_->publish_state(total); } @@ -94,15 +97,15 @@ void PulseMeterSensor::loop() { this->meter_state_ = MeterState::RUNNING; } break; case MeterState::RUNNING: { - uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; - float pulse_width_us = delta_us / float(this->get_->count_); - ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, - this->get_->count_, pulse_width_us); + uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_; + float pulse_width_us = delta_us / float(state.count_); + ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_, + pulse_width_us); this->publish_state((60.0f * 1000000.0f) / pulse_width_us); } break; } - this->last_processed_edge_us_ = this->get_->last_detected_edge_us_; + this->last_processed_edge_us_ = state.last_detected_edge_us_; } // No detected edges this loop else { @@ -141,14 +144,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { // This is an interrupt handler - we can't call any virtual method from this method // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); - auto &state = sensor->edge_state_; - auto &set = *sensor->set_; + auto &edge_state = sensor->edge_state_; + auto &state = sensor->state_; - if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) { - state.last_sent_edge_us_ = now; - set.last_detected_edge_us_ = now; - set.last_rising_edge_us_ = now; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) { + edge_state.last_sent_edge_us_ = now; + state.last_detected_edge_us_ = now; + state.last_rising_edge_us_ = now; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high @@ -160,26 +163,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); const bool pin_val = sensor->isr_pin_.digital_read(); - auto &state = sensor->pulse_state_; - auto &set = *sensor->set_; + auto &pulse_state = sensor->pulse_state_; + auto &state = sensor->state_; // Filter length has passed since the last interrupt - const bool length = now - state.last_intr_ >= sensor->filter_us_; + const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_; - if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge - state.latched_ = false; - } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge - state.latched_ = true; - set.last_detected_edge_us_ = state.last_intr_; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge + pulse_state.latched_ = false; + } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge + pulse_state.latched_ = true; + state.last_detected_edge_us_ = pulse_state.last_intr_; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes // length && latched && rising (just reset from a long low edge) // !latched && (rising || high) (noise on the line resetting the potential rising edge) - set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_; + state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_; - state.last_intr_ = now; + pulse_state.last_intr_ = now; sensor->last_pin_val_ = pin_val; } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 5800c4ec42a..e46f1e615ff 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -46,17 +46,16 @@ class PulseMeterSensor : public sensor::Sensor, public Component { uint32_t total_pulses_ = 0; uint32_t last_processed_edge_us_ = 0; - // This struct (and the two pointers) are used to pass data between the ISR and loop. - // These two pointers are exchanged each loop. - // Use these to send data from the ISR to the loop not the other way around (except for resetting the values). + // This struct and variable are used to pass data between the ISR and loop. + // The data from state_ is read and then count_ in state_ is reset in each loop. + // This must be done while guarded by an InterruptLock. Use this variable to send data + // from the ISR to the loop not the other way around (except for resetting count_). struct State { uint32_t last_detected_edge_us_ = 0; uint32_t last_rising_edge_us_ = 0; uint32_t count_ = 0; }; - State state_[2]; - volatile State *set_ = state_; - volatile State *get_ = state_ + 1; + volatile State state_{}; // Only use the following variables in the ISR or while guarded by an InterruptLock ISRInternalGPIOPin isr_pin_; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 0cac28506fd..d73fff2b0a5 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -119,6 +119,8 @@ class RemoteComponentBase { }; #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED class RemoteRMTChannel { public: void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } @@ -137,7 +139,8 @@ class RemoteRMTChannel { uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; }; -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 class RemoteTransmitterBase : public RemoteComponentBase { public: diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b3dc213c5f6..362f6e99db4 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_( def validate_config(config): if CORE.is_esp32: variant = esp32.get_esp32_variant() + if variant in esp32_rmt.VARIANTS_NO_RMT: + return config if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2): max_idle = 65535 else: @@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_BUFFER_SIZE, esp32="10000b", + esp32_c2="1000b", + esp32_c61="1000b", esp8266="1000b", bk72xx="1000b", ln882x="1000b", @@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, esp32_c3=96, esp32_c5=96, esp32_c6=96, + esp32_c61=cv.UNDEFINED, esp32_h2=96, esp32_p4=192, esp32_s2=192, @@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RECEIVE_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( @@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), - cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.SplitDefault( + CONF_CARRIER_DUTY_PERCENT, + esp32=100, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All( cv.only_on_esp32, cv.percentage_int, cv.Range(min=1, max=100), ), - cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( - cv.only_on_esp32, cv.frequency, cv.int_ - ), + cv.SplitDefault( + CONF_CARRIER_FREQUENCY, + esp32="0Hz", + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All(cv.only_on_esp32, cv.frequency, cv.int_), } ) .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_FILTER_SYMBOLS, + CONF_RECEIVE_SYMBOLS, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, + ] + ) + ) .add_extra(validate_config) ) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_receiver.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index de47457dac3..d59ee636955 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3d9199a904d..5da9283a6e7 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -6,12 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore { volatile bool prev_level{false}; volatile bool overflow{false}; }; -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) && SOC_RMT_SUPPORTED struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore { class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void dump_config() override; void loop() override; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; @@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, RemoteReceiverComponentStore store_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index f95244ea458..357a36d052f 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include #include @@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c } // namespace esphome::remote_receiver -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 8383b9dd75d..fc772f88b22 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -40,45 +40,66 @@ DigitalWriteAction = remote_transmitter_ns.class_( cg.Parented.template(RemoteTransmitterComponent), ) + MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( - cv.percentage_int, cv.Range(min=1, max=100) - ), - cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( - cv.only_on_esp32, - esp32_rmt.validate_clock_resolution(), - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_USE_DMA): cv.All( - esp32.only_on_variant( - supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( + cv.percentage_int, cv.Range(min=1, max=100) ), - cv.boolean, - ), - cv.SplitDefault( - CONF_RMT_SYMBOLS, - esp32=64, - esp32_c3=48, - esp32_c5=48, - esp32_c6=48, - esp32_h2=48, - esp32_p4=48, - esp32_s2=64, - esp32_s3=48, - ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), - cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), - } -).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + esp32_rmt.validate_clock_resolution(), + ), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_USE_DMA): cv.All( + esp32.only_on_variant( + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] + ), + cv.boolean, + ), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32=64, + esp32_c2=cv.UNDEFINED, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_c61=cv.UNDEFINED, + esp32_h2=48, + esp32_p4=48, + esp32_s2=64, + esp32_s3=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_EOT_LEVEL, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_NON_BLOCKING, + ] + ) + ) +) def _validate_non_blocking(config): - if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + if ( + CORE.is_esp32 + and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT + and CONF_NON_BLOCKING not in config + ): _LOGGER.warning( "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" @@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_transmitter.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index bee1d0be8ac..8da4cfd95d0 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: @@ -14,5 +13,4 @@ template class DigitalWriteAction : public Action, public void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index d35541e2e1f..51a3c0b1d47 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,9 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->complete_trigger_.trigger(); } -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 65bd2ac8b28..aee52ea170b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -6,13 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) // IDF version 5.5.1 and above is required because of a bug in // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 @@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore { class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void digital_write(bool value); -#if defined(USE_ESP32) +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } @@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); @@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t target_time_; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void configure_rmt_(); void wait_for_rmt_(); @@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, Trigger<> complete_trigger_; }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 89d97895b26..71773e3ddf8 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -3,10 +3,11 @@ #include "esphome/core/application.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } #endif -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index 172da32adc3..fa72fd9a246 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -25,8 +25,8 @@ static uint8_t s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation -static constexpr size_t PREF_BUFFER_SIZE = 64; +// No preference can exceed the total flash storage, so stack buffer covers all cases. +static constexpr size_t PREF_MAX_BUFFER_SIZE = RP2040_FLASH_STORAGE_SIZE; extern "C" uint8_t _EEPROM_start; @@ -46,14 +46,14 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend { bool save(const uint8_t *data, size_t len) override { const size_t buffer_size = len + 1; - SmallBufferWithHeapFallback buffer_alloc(buffer_size); - uint8_t *buffer = buffer_alloc.get(); - + if (buffer_size > PREF_MAX_BUFFER_SIZE) + return false; + uint8_t buffer[PREF_MAX_BUFFER_SIZE]; memcpy(buffer, data, len); - buffer[len] = calculate_crc(buffer, buffer + len, type); + buffer[len] = calculate_crc(buffer, buffer + len, this->type); for (size_t i = 0; i < buffer_size; i++) { - uint32_t j = offset + i; + uint32_t j = this->offset + i; if (j >= RP2040_FLASH_STORAGE_SIZE) return false; uint8_t v = buffer[i]; @@ -66,17 +66,18 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend { } bool load(uint8_t *data, size_t len) override { const size_t buffer_size = len + 1; - SmallBufferWithHeapFallback buffer_alloc(buffer_size); - uint8_t *buffer = buffer_alloc.get(); + if (buffer_size > PREF_MAX_BUFFER_SIZE) + return false; + uint8_t buffer[PREF_MAX_BUFFER_SIZE]; for (size_t i = 0; i < buffer_size; i++) { - uint32_t j = offset + i; + uint32_t j = this->offset + i; if (j >= RP2040_FLASH_STORAGE_SIZE) return false; buffer[i] = s_flash_storage[j]; } - uint8_t crc = calculate_crc(buffer, buffer + len, type); + uint8_t crc = calculate_crc(buffer, buffer + len, this->type); if (buffer[len] != crc) { return false; } diff --git a/esphome/components/rtl87xx/boards.py b/esphome/components/rtl87xx/boards.py index 45ef02b7e74..3a5ee853f28 100644 --- a/esphome/components/rtl87xx/boards.py +++ b/esphome/components/rtl87xx/boards.py @@ -71,6 +71,10 @@ RTL87XX_BOARDS = { "name": "WR3L Wi-Fi Module", "family": FAMILY_RTL8710B, }, + "wbru": { + "name": "WBRU Wi-Fi Module", + "family": FAMILY_RTL8720C, + }, "wr2le": { "name": "WR2LE Wi-Fi Module", "family": FAMILY_RTL8710B, @@ -83,6 +87,14 @@ RTL87XX_BOARDS = { "name": "T103_V1.0", "family": FAMILY_RTL8710B, }, + "cr3l": { + "name": "CR3L Wi-Fi Module", + "family": FAMILY_RTL8720C, + }, + "generic-rtl8720cm-4mb-1712k": { + "name": "Generic - RTL8720CM (4M/1712k)", + "family": FAMILY_RTL8720C, + }, "generic-rtl8720cf-2mb-896k": { "name": "Generic - RTL8720CF (2M/896k)", "family": FAMILY_RTL8720C, @@ -103,6 +115,10 @@ RTL87XX_BOARDS = { "name": "WR2L Wi-Fi Module", "family": FAMILY_RTL8710B, }, + "wbr1": { + "name": "WBR1 Wi-Fi Module", + "family": FAMILY_RTL8720C, + }, "wr1": { "name": "WR1 Wi-Fi Module", "family": FAMILY_RTL8710B, @@ -119,10 +135,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, @@ -230,10 +246,10 @@ RTL87XX_BOARD_PINS = { "A1": 41, }, "wbr3": { - "WIRE0_SCL_0": 11, - "WIRE0_SCL_1": 2, - "WIRE0_SCL_2": 19, - "WIRE0_SCL_3": 15, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SCL_3": 19, "WIRE0_SDA_0": 3, "WIRE0_SDA_1": 12, "WIRE0_SDA_2": 16, @@ -242,10 +258,10 @@ RTL87XX_BOARD_PINS = { "SERIAL0_TX_0": 11, "SERIAL0_TX_1": 14, "SERIAL1_CTS": 4, - "SERIAL1_RX_0": 2, - "SERIAL1_RX_1": 0, - "SERIAL1_TX_0": 3, - "SERIAL1_TX_1": 1, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, "SERIAL2_CTS": 19, "SERIAL2_RX": 15, "SERIAL2_TX": 16, @@ -296,6 +312,12 @@ RTL87XX_BOARD_PINS = { }, "generic-rtl8710bn-2mb-468k": { "SPI0_CS": 19, + "SPI0_FCS": 6, + "SPI0_FD0": 9, + "SPI0_FD1": 7, + "SPI0_FD2": 8, + "SPI0_FD3": 11, + "SPI0_FSCK": 10, "SPI0_MISO": 22, "SPI0_MOSI": 23, "SPI0_SCK": 18, @@ -396,10 +418,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, @@ -463,10 +485,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, @@ -714,6 +736,12 @@ RTL87XX_BOARD_PINS = { }, "generic-rtl8710bn-2mb-788k": { "SPI0_CS": 19, + "SPI0_FCS": 6, + "SPI0_FD0": 9, + "SPI0_FD1": 7, + "SPI0_FD2": 8, + "SPI0_FD3": 11, + "SPI0_FSCK": 10, "SPI0_MISO": 22, "SPI0_MOSI": 23, "SPI0_SCK": 18, @@ -807,6 +835,12 @@ RTL87XX_BOARD_PINS = { }, "generic-rtl8710bx-4mb-980k": { "SPI0_CS": 19, + "SPI0_FCS": 6, + "SPI0_FD0": 9, + "SPI0_FD1": 7, + "SPI0_FD2": 8, + "SPI0_FD3": 11, + "SPI0_FSCK": 10, "SPI0_MISO": 22, "SPI0_MOSI": 23, "SPI0_SCK": 18, @@ -957,8 +991,8 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, "WIRE0_SDA_0": 19, "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, @@ -1088,6 +1122,99 @@ RTL87XX_BOARD_PINS = { "A0": 19, "A1": 41, }, + "wbru": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 7, + "SPI0_CS_2": 15, + "SPI0_MISO_0": 10, + "SPI0_MISO_1": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 9, + "SPI0_MOSI_2": 19, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 8, + "SPI0_SCK_2": 16, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SCL_3": 19, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 16, + "WIRE0_SDA_3": 20, + "SERIAL0_CTS": 10, + "SERIAL0_RTS": 9, + "SERIAL0_RX_0": 12, + "SERIAL0_RX_1": 13, + "SERIAL0_TX_0": 11, + "SERIAL0_TX_1": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX": 3, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CS0": 7, + "CTS0": 10, + "CTS1": 4, + "CTS2": 19, + "MOSI0": 19, + "PA00": 0, + "PA0": 0, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PWM0": 0, + "PWM1": 12, + "PWM5": 17, + "PWM6": 18, + "RTS0": 9, + "RTS2": 20, + "RX2": 15, + "SCK0": 16, + "TX1": 3, + "TX2": 16, + "D0": 8, + "D1": 9, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 15, + "D6": 16, + "D7": 11, + "D8": 12, + "D9": 17, + "D10": 18, + "D11": 19, + "D12": 14, + "D13": 13, + "D14": 20, + "D15": 0, + "D16": 10, + "D17": 7, + }, "wr2le": { "MISO0": 22, "MISO1": 22, @@ -1116,21 +1243,21 @@ RTL87XX_BOARD_PINS = { "SPI0_MISO": 20, "SPI0_MOSI_0": 4, "SPI0_MOSI_1": 19, - "SPI0_SCK_0": 16, - "SPI0_SCK_1": 3, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 16, "WIRE0_SCL_0": 2, "WIRE0_SCL_1": 15, "WIRE0_SCL_2": 19, - "WIRE0_SDA_0": 20, + "WIRE0_SDA_0": 3, "WIRE0_SDA_1": 16, - "WIRE0_SDA_2": 3, + "WIRE0_SDA_2": 20, "SERIAL0_RX": 13, "SERIAL0_TX": 14, "SERIAL1_CTS": 4, - "SERIAL1_RX_0": 2, - "SERIAL1_RX_1": 0, - "SERIAL1_TX_0": 3, - "SERIAL1_TX_1": 1, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, "SERIAL2_CTS": 19, "SERIAL2_RTS": 20, "SERIAL2_RX": 15, @@ -1251,6 +1378,168 @@ RTL87XX_BOARD_PINS = { "A0": 19, "A1": 41, }, + "cr3l": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 15, + "SPI0_MISO": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 19, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 16, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 15, + "WIRE0_SCL_2": 19, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 16, + "WIRE0_SDA_2": 20, + "SERIAL0_RX": 13, + "SERIAL0_TX": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX": 2, + "SERIAL1_TX": 3, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CTS1": 4, + "CTS2": 19, + "MISO0": 20, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PWM0": 20, + "PWM5": 17, + "PWM6": 18, + "RTS2": 20, + "RX0": 13, + "RX1": 2, + "RX2": 15, + "SCL0": 19, + "SDA0": 16, + "TX0": 14, + "TX1": 3, + "TX2": 16, + "D0": 20, + "D1": 2, + "D2": 3, + "D3": 4, + "D4": 15, + "D5": 16, + "D6": 17, + "D7": 18, + "D8": 19, + "D9": 13, + "D10": 14, + }, + "generic-rtl8720cm-4mb-1712k": { + "SPI0_CS_0": 2, + "SPI0_CS_1": 7, + "SPI0_CS_2": 15, + "SPI0_MISO_0": 10, + "SPI0_MISO_1": 20, + "SPI0_MOSI_0": 4, + "SPI0_MOSI_1": 9, + "SPI0_MOSI_2": 19, + "SPI0_SCK_0": 3, + "SPI0_SCK_1": 8, + "SPI0_SCK_2": 16, + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SCL_3": 19, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 16, + "WIRE0_SDA_3": 20, + "SERIAL0_CTS": 10, + "SERIAL0_RTS": 9, + "SERIAL0_RX_0": 12, + "SERIAL0_RX_1": 13, + "SERIAL0_TX_0": 11, + "SERIAL0_TX_1": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, + "SERIAL2_CTS": 19, + "SERIAL2_RTS": 20, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CS0": 15, + "CTS0": 10, + "CTS1": 4, + "CTS2": 19, + "MOSI0": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PA19": 19, + "PA20": 20, + "PA23": 23, + "PWM0": 20, + "PWM5": 17, + "PWM6": 18, + "PWM7": 23, + "RTS0": 9, + "RTS2": 20, + "RX2": 15, + "SCK0": 16, + "TX2": 16, + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 7, + "D6": 8, + "D7": 9, + "D8": 10, + "D9": 11, + "D10": 12, + "D11": 13, + "D12": 14, + "D13": 15, + "D14": 16, + "D15": 17, + "D16": 18, + "D17": 19, + "D18": 20, + "D19": 23, + }, "generic-rtl8720cf-2mb-896k": { "SPI0_CS_0": 2, "SPI0_CS_1": 7, @@ -1456,8 +1745,8 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, "WIRE0_SDA_0": 19, "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, @@ -1585,6 +1874,65 @@ RTL87XX_BOARD_PINS = { "D4": 12, "A0": 19, }, + "wbr1": { + "WIRE0_SCL_0": 2, + "WIRE0_SCL_1": 11, + "WIRE0_SCL_2": 15, + "WIRE0_SDA_0": 3, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 16, + "SERIAL0_RX_0": 12, + "SERIAL0_RX_1": 13, + "SERIAL0_TX_0": 11, + "SERIAL0_TX_1": 14, + "SERIAL1_CTS": 4, + "SERIAL1_RX_0": 0, + "SERIAL1_RX_1": 2, + "SERIAL1_TX_0": 1, + "SERIAL1_TX_1": 3, + "SERIAL2_RX": 15, + "SERIAL2_TX": 16, + "CTS1": 4, + "MOSI0": 4, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA11": 11, + "PA12": 12, + "PA13": 13, + "PA14": 14, + "PA15": 15, + "PA16": 16, + "PA17": 17, + "PA18": 18, + "PWM5": 17, + "PWM6": 18, + "PWM7": 13, + "RX2": 15, + "SCL0": 15, + "SDA0": 12, + "TX2": 16, + "D0": 14, + "D1": 13, + "D2": 2, + "D3": 3, + "D4": 16, + "D5": 4, + "D6": 11, + "D7": 15, + "D8": 12, + "D9": 17, + "D10": 18, + "D11": 0, + "D12": 1, + }, "wr1": { "SPI0_CS": 19, "SPI0_MISO": 22, @@ -1594,10 +1942,10 @@ RTL87XX_BOARD_PINS = { "SPI1_MISO": 22, "SPI1_MOSI": 23, "SPI1_SCK": 18, - "WIRE0_SCL_0": 29, - "WIRE0_SCL_1": 22, - "WIRE0_SDA_0": 30, - "WIRE0_SDA_1": 19, + "WIRE0_SCL_0": 22, + "WIRE0_SCL_1": 29, + "WIRE0_SDA_0": 19, + "WIRE0_SDA_1": 30, "WIRE1_SCL": 18, "WIRE1_SDA": 23, "SERIAL0_CTS": 19, diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index c179282c50d..6e86405b740 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -2,29 +2,74 @@ #include #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" -namespace esphome { -namespace rtttl { +namespace esphome::rtttl { static const char *const TAG = "rtttl"; -static const uint32_t DOUBLE_NOTE_GAP_MS = 10; - // These values can also be found as constants in the Tone library (Tone.h) static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; -static const uint16_t I2S_SPEED = 1000; +#if defined(USE_OUTPUT) || defined(USE_SPEAKER) +static const uint32_t DOUBLE_NOTE_GAP_MS = 10; +#endif // USE_OUTPUT || USE_SPEAKER -#undef HALF_PI -static const double HALF_PI = 1.5707963267948966192313216916398; +#ifdef USE_SPEAKER +static const size_t SAMPLE_BUFFER_SIZE = 2048; + +struct SpeakerSample { + int8_t left{0}; + int8_t right{0}; +}; inline double deg2rad(double degrees) { static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0; return degrees * PI_ON_180; } +#endif // USE_SPEAKER + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback +PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING", + "State::STOPPING", "UNKNOWN"); + +static const LogString *state_to_string(State state) { + return RtttlStateStrings::get_log_str(static_cast(state), RtttlStateStrings::LAST_INDEX); +} +#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + +static uint8_t note_index_from_char(char note) { + switch (note) { + case 'c': + return 1; + // 'c#': 2 + case 'd': + return 3; + // 'd#': 4 + case 'e': + return 5; + case 'f': + return 6; + // 'f#': 7 + case 'g': + return 8; + // 'g#': 9 + case 'a': + return 10; + // 'a#': 11 + // Support both 'b' (English notation for B natural) and 'h' (German notation for B natural) + case 'b': + case 'h': + return 12; + case 'p': + default: + return 0; + } +} void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, @@ -33,161 +78,34 @@ void Rtttl::dump_config() { this->gain_); } -void Rtttl::play(std::string rtttl) { - if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { - size_t pos = this->rtttl_.find(':'); - size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); - ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); - return; - } - - this->rtttl_ = std::move(rtttl); - - this->default_duration_ = 4; - this->default_octave_ = 6; - this->note_duration_ = 0; - - int bpm = 63; - uint8_t num; - - // Get name - this->position_ = this->rtttl_.find(':'); - - // it's somewhat documented to be up to 10 characters but let's be a bit flexible here - if (this->position_ == std::string::npos || this->position_ > 15) { - ESP_LOGE(TAG, "Unable to determine name; missing ':'"); - return; - } - - ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); - - // get default duration - this->position_ = this->rtttl_.find("d=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing 'd='"); - return; - } - this->position_ += 2; - num = this->get_integer_(); - if (num > 0) - this->default_duration_ = num; - - // get default octave - this->position_ = this->rtttl_.find("o=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing 'o="); - return; - } - this->position_ += 2; - num = get_integer_(); - if (num >= 3 && num <= 7) - this->default_octave_ = num; - - // get BPM - this->position_ = this->rtttl_.find("b=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing b="); - return; - } - this->position_ += 2; - num = get_integer_(); - if (num != 0) - bpm = num; - - this->position_ = this->rtttl_.find(':', this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing second ':'"); - return; - } - this->position_++; - - // BPM usually expresses the number of quarter notes per minute - this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) - - this->output_freq_ = 0; - this->last_note_ = millis(); - this->note_duration_ = 1; - -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - this->set_state_(State::STATE_INIT); - this->samples_sent_ = 0; - this->samples_count_ = 0; - } -#endif -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->set_state_(State::STATE_RUNNING); - } -#endif -} - -void Rtttl::stop() { -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - if (this->speaker_->is_running()) { - this->speaker_->stop(); - } - this->set_state_(STATE_STOPPING); - } -#endif - this->position_ = this->rtttl_.length(); - this->note_duration_ = 0; -} - -void Rtttl::finish_() { - ESP_LOGV(TAG, "Rtttl::finish_()"); -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(State::STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); - this->speaker_->finish(); - this->set_state_(State::STATE_STOPPING); - } -#endif - // Ensure no more notes are played in case finish_() is called for an error. - this->position_ = this->rtttl_.length(); - this->note_duration_ = 0; -} - void Rtttl::loop() { - if (this->state_ == State::STATE_STOPPED) { + if (this->state_ == State::STOPPED) { this->disable_loop(); return; } +#ifdef USE_OUTPUT + if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) { + return; + } +#endif // USE_OUTPUT + #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { - if (this->state_ == State::STATE_STOPPING) { + if (this->state_ == State::STOPPING) { if (this->speaker_->is_stopped()) { - this->set_state_(State::STATE_STOPPED); + this->set_state_(State::STOPPED); } else { return; } - } else if (this->state_ == State::STATE_INIT) { + } else if (this->state_ == State::INIT) { if (this->speaker_->is_stopped()) { this->speaker_->start(); - this->set_state_(State::STATE_STARTING); + this->set_state_(State::STARTING); } - } else if (this->state_ == State::STATE_STARTING) { + } else if (this->state_ == State::STARTING) { if (this->speaker_->is_running()) { - this->set_state_(State::STATE_RUNNING); + this->set_state_(State::RUNNING); } } if (!this->speaker_->is_running()) { @@ -229,19 +147,17 @@ void Rtttl::loop() { } } } -#endif -#ifdef USE_OUTPUT - if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) - return; -#endif +#endif // USE_SPEAKER + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... - while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') + while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') { this->position_++; + } // first, get note duration, if available uint8_t num = this->get_integer_(); @@ -253,35 +169,8 @@ void Rtttl::loop() { this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after } - uint8_t note; + uint8_t note = note_index_from_char(this->rtttl_[this->position_]); - switch (this->rtttl_[this->position_]) { - case 'c': - note = 1; - break; - case 'd': - note = 3; - break; - case 'e': - note = 5; - break; - case 'f': - note = 6; - break; - case 'g': - note = 8; - break; - case 'a': - note = 10; - break; - case 'h': - case 'b': - note = 12; - break; - case 'p': - default: - note = 0; - } this->position_++; // now, get optional '#' sharp @@ -291,7 +180,7 @@ void Rtttl::loop() { } // now, get scale - uint8_t scale = get_integer_(); + uint8_t scale = this->get_integer_(); if (scale == 0) { scale = this->default_octave_; } @@ -344,7 +233,8 @@ void Rtttl::loop() { this->output_->set_level(0.0); } } -#endif +#endif // USE_OUTPUT + #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { this->samples_sent_ = 0; @@ -369,29 +259,152 @@ void Rtttl::loop() { } // Convert from frequency in Hz to high and low samples in fixed point } -#endif +#endif // USE_SPEAKER this->last_note_ = millis(); } -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE -static const LogString *state_to_string(State state) { - switch (state) { - case STATE_STOPPED: - return LOG_STR("STATE_STOPPED"); - case STATE_STARTING: - return LOG_STR("STATE_STARTING"); - case STATE_RUNNING: - return LOG_STR("STATE_RUNNING"); - case STATE_STOPPING: - return LOG_STR("STATE_STOPPING"); - case STATE_INIT: - return LOG_STR("STATE_INIT"); - default: - return LOG_STR("UNKNOWN"); +void Rtttl::play(std::string rtttl) { + if (this->state_ != State::STOPPED && this->state_ != State::STOPPING) { + size_t pos = this->rtttl_.find(':'); + size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); + ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); + return; } -}; -#endif + + this->rtttl_ = std::move(rtttl); + + this->default_duration_ = 4; + this->default_octave_ = 6; + this->note_duration_ = 0; + + int bpm = 63; + uint8_t num; + + // Get name + this->position_ = this->rtttl_.find(':'); + + // it's somewhat documented to be up to 10 characters but let's be a bit flexible here + if (this->position_ == std::string::npos || this->position_ > 15) { + ESP_LOGE(TAG, "Unable to determine name; missing ':'"); + return; + } + + ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); + + // get default duration + this->position_ = this->rtttl_.find("d=", this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'd='"); + return; + } + this->position_ += 2; + num = this->get_integer_(); + if (num > 0) { + this->default_duration_ = num; + } + + // get default octave + this->position_ = this->rtttl_.find("o=", this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'o="); + return; + } + this->position_ += 2; + num = this->get_integer_(); + if (num >= 3 && num <= 7) { + this->default_octave_ = num; + } + + // get BPM + this->position_ = this->rtttl_.find("b=", this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing b="); + return; + } + this->position_ += 2; + num = this->get_integer_(); + if (num != 0) { + bpm = num; + } + + this->position_ = this->rtttl_.find(':', this->position_); + if (this->position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing second ':'"); + return; + } + this->position_++; + + // BPM usually expresses the number of quarter notes per minute + this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + + this->output_freq_ = 0; + this->last_note_ = millis(); + this->note_duration_ = 1; + +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->set_state_(State::RUNNING); + } +#endif // USE_OUTPUT + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->set_state_(State::INIT); + this->samples_sent_ = 0; + this->samples_count_ = 0; + } +#endif // USE_SPEAKER +} + +void Rtttl::stop() { +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STOPPED); + } +#endif // USE_OUTPUT + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + if (this->speaker_->is_running()) { + this->speaker_->stop(); + } + this->set_state_(State::STOPPING); + } +#endif // USE_SPEAKER + + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; +} + +void Rtttl::finish_() { + ESP_LOGV(TAG, "Rtttl::finish_()"); + +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STOPPED); + } +#endif // USE_OUTPUT + +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->finish(); + this->set_state_(State::STOPPING); + } +#endif // USE_SPEAKER + + // Ensure no more notes are played in case finish_() is called for an error. + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; +} void Rtttl::set_state_(State state) { State old_state = this->state_; @@ -399,15 +412,14 @@ void Rtttl::set_state_(State state) { ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), LOG_STR_ARG(state_to_string(state))); - // Clear loop_done when transitioning from STOPPED to any other state - if (state == State::STATE_STOPPED) { + // Clear loop_done when transitioning from `State::STOPPED` to any other state + if (state == State::STOPPED) { this->disable_loop(); this->on_finished_playback_callback_.call(); ESP_LOGD(TAG, "Playback finished"); - } else if (old_state == State::STATE_STOPPED) { + } else if (old_state == State::STOPPED) { this->enable_loop(); } } -} // namespace rtttl -} // namespace esphome +} // namespace esphome::rtttl diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 1e924a897c2..6f5df07766b 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -5,48 +5,41 @@ #ifdef USE_OUTPUT #include "esphome/components/output/float_output.h" -#endif +#endif // USE_OUTPUT #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" -#endif +#endif // USE_SPEAKER -namespace esphome { -namespace rtttl { +namespace esphome::rtttl { -enum State : uint8_t { - STATE_STOPPED = 0, - STATE_INIT, - STATE_STARTING, - STATE_RUNNING, - STATE_STOPPING, +enum class State : uint8_t { + STOPPED = 0, + INIT, + STARTING, + RUNNING, + STOPPING, }; -#ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 2048; - -struct SpeakerSample { - int8_t left{0}; - int8_t right{0}; -}; -#endif - class Rtttl : public Component { public: #ifdef USE_OUTPUT void set_output(output::FloatOutput *output) { this->output_ = output; } -#endif +#endif // USE_OUTPUT + #ifdef USE_SPEAKER void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } -#endif - float get_gain() { return gain_; } - void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); } +#endif // USE_SPEAKER + + void dump_config() override; + void loop() override; void play(std::string rtttl); void stop(); - void dump_config() override; - bool is_playing() { return this->state_ != State::STATE_STOPPED; } - void loop() override; + float get_gain() { return this->gain_; } + void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); } + + bool is_playing() { return this->state_ != State::STOPPED; } void add_on_finished_playback_callback(std::function callback) { this->on_finished_playback_callback_.add(std::move(callback)); @@ -90,12 +83,12 @@ class Rtttl : public Component { /// The gain of the output. float gain_{0.6f}; /// The current state of the RTTTL player. - State state_{State::STATE_STOPPED}; + State state_{State::STOPPED}; #ifdef USE_OUTPUT /// The output to write the sound to. output::FloatOutput *output_; -#endif +#endif // USE_OUTPUT #ifdef USE_SPEAKER /// The speaker to write the sound to. @@ -110,8 +103,7 @@ class Rtttl : public Component { int samples_count_{0}; /// The number of samples for the gap between notes. int samples_gap_{0}; - -#endif +#endif // USE_SPEAKER /// The callback to call when playback is finished. CallbackManager on_finished_playback_callback_; @@ -145,5 +137,4 @@ class FinishedPlaybackTrigger : public Trigger<> { } }; -} // namespace rtttl -} // namespace esphome +} // namespace esphome::rtttl diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py new file mode 100644 index 00000000000..0773a53d911 --- /dev/null +++ b/esphome/components/runtime_image/__init__.py @@ -0,0 +1,191 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.image import ( + IMAGE_TYPE, + Image_, + validate_settings, + validate_transparency, + validate_type, +) +import esphome.config_validation as cv +from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE + +AUTO_LOAD = ["image"] +CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] + +CONF_PLACEHOLDER = "placeholder" +CONF_TRANSPARENCY = "transparency" + +runtime_image_ns = cg.esphome_ns.namespace("runtime_image") + +# Base decoder classes +ImageDecoder = runtime_image_ns.class_("ImageDecoder") +BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder) +JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder) +PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder) + +# Runtime image class +RuntimeImage = runtime_image_ns.class_( + "RuntimeImage", cg.esphome_ns.namespace("image").class_("Image") +) + +# Image format enum +ImageFormat = runtime_image_ns.enum("ImageFormat") +IMAGE_FORMAT_AUTO = ImageFormat.AUTO +IMAGE_FORMAT_JPEG = ImageFormat.JPEG +IMAGE_FORMAT_PNG = ImageFormat.PNG +IMAGE_FORMAT_BMP = ImageFormat.BMP + +# Export enum for decode errors +DecodeError = runtime_image_ns.enum("DecodeError") +DECODE_ERROR_INVALID_TYPE = DecodeError.DECODE_ERROR_INVALID_TYPE +DECODE_ERROR_UNSUPPORTED_FORMAT = DecodeError.DECODE_ERROR_UNSUPPORTED_FORMAT +DECODE_ERROR_OUT_OF_MEMORY = DecodeError.DECODE_ERROR_OUT_OF_MEMORY + + +class Format: + """Base class for image format definitions.""" + + def __init__(self, name: str, decoder_class: cg.MockObjClass) -> None: + self.name = name + self.decoder_class = decoder_class + + def actions(self) -> None: + """Add defines and libraries needed for this format.""" + + +class BMPFormat(Format): + """BMP format decoder configuration.""" + + def __init__(self): + super().__init__("BMP", BmpDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_BMP") + + +class JPEGFormat(Format): + """JPEG format decoder configuration.""" + + def __init__(self): + super().__init__("JPEG", JpegDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_JPEG") + cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") + + +class PNGFormat(Format): + """PNG format decoder configuration.""" + + def __init__(self): + super().__init__("PNG", PngDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_PNG") + cg.add_library("pngle", "1.1.0") + + +# Registry of available formats +IMAGE_FORMATS = { + "BMP": BMPFormat(), + "JPEG": JPEGFormat(), + "PNG": PNGFormat(), + "JPG": JPEGFormat(), # Alias for JPEG +} + + +def get_format(format_name: str) -> Format | None: + """Get a format instance by name.""" + return IMAGE_FORMATS.get(format_name.upper()) + + +def enable_format(format_name: str) -> Format | None: + """Enable a specific image format by adding its defines and libraries.""" + format_obj = get_format(format_name) + if format_obj: + format_obj.actions() + return format_obj + return None + + +# Runtime image configuration schema base - to be extended by components +def runtime_image_schema(image_class: cg.MockObjClass = RuntimeImage) -> cv.Schema: + """Create a runtime image schema with the specified image class.""" + return cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(image_class), + cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), + cv.Optional(CONF_BYTE_ORDER): cv.one_of( + "BIG_ENDIAN", "LITTLE_ENDIAN", upper=True + ), + cv.Optional(CONF_TRANSPARENCY, default="OPAQUE"): validate_transparency(), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + } + ) + + +def validate_runtime_image_settings(config: dict) -> dict: + """Apply validate_settings from image component to runtime image config.""" + return validate_settings(config) + + +@dataclass +class RuntimeImageSettings: + """Processed runtime image configuration parameters.""" + + width: int + height: int + format_enum: cg.MockObj + image_type_enum: cg.MockObj + transparent: cg.MockObj + byte_order_big_endian: bool + placeholder: cg.MockObj | None + + +async def process_runtime_image_config(config: dict) -> RuntimeImageSettings: + """ + Helper function to process common runtime image configuration parameters. + Handles format enabling and returns all necessary enums and parameters. + """ + from esphome.components.image import get_image_type_enum, get_transparency_enum + + # Get resize dimensions with default (0, 0) + width, height = config.get(CONF_RESIZE, (0, 0)) + + # Handle format (required for runtime images) + format_name = config[CONF_FORMAT] + # Enable the format in the runtime_image component + enable_format(format_name) + # Map format names to enum values (handle JPG as alias for JPEG) + if format_name.upper() == "JPG": + format_name = "JPEG" + format_enum = getattr(ImageFormat, format_name.upper()) + + # Get image type enum + image_type_enum = get_image_type_enum(config[CONF_TYPE]) + + # Get transparency enum + transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE")) + + # Get byte order (True for big endian, False for little endian) + byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN" + + # Get placeholder if specified + placeholder = None + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + + return RuntimeImageSettings( + width=width, + height=height, + format_enum=format_enum, + image_type_enum=image_type_enum, + transparent=transparent, + byte_order_big_endian=byte_order_big_endian, + placeholder=placeholder, + ) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/runtime_image/bmp_decoder.cpp similarity index 82% rename from esphome/components/online_image/bmp_image.cpp rename to esphome/components/runtime_image/bmp_decoder.cpp index 676a2efca9a..1a56484c606 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -1,15 +1,14 @@ -#include "bmp_image.h" +#include "bmp_decoder.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "esphome/components/display/display.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { -static const char *const TAG = "online_image.bmp"; +static const char *const TAG = "image_decoder.bmp"; int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t index = 0; @@ -30,7 +29,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return DECODE_ERROR_INVALID_TYPE; } - this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + // BMP file contains its own size in the header + size_t file_size = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + if (this->expected_size_ == 0) { + this->expected_size_ = file_size; // Use file header size if not provided + } this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]); this->current_index_ = 14; @@ -90,8 +93,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { while (index < size) { uint8_t current_byte = buffer[index]; for (uint8_t i = 0; i < 8; i++) { - size_t x = (this->paint_index_ % this->width_) + i; - size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + size_t x = (this->paint_index_ % static_cast(this->width_)) + i; + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; this->draw(x, y, 1, 1, c); } @@ -110,8 +113,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { uint8_t b = buffer[index]; uint8_t g = buffer[index + 1]; uint8_t r = buffer[index + 2]; - size_t x = this->paint_index_ % this->width_; - size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + size_t x = this->paint_index_ % static_cast(this->width_); + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = Color(r, g, b); this->draw(x, y, 1, 1, c); this->paint_index_++; @@ -133,7 +136,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return size; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/runtime_image/bmp_decoder.h similarity index 52% rename from esphome/components/online_image/bmp_image.h rename to esphome/components/runtime_image/bmp_decoder.h index 916ffea1adb..37db6b49405 100644 --- a/esphome/components/online_image/bmp_image.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -1,27 +1,32 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "image_decoder.h" +#include "runtime_image.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** - * @brief Image decoder specialization for PNG images. + * @brief Image decoder specialization for BMP images. */ class BmpDecoder : public ImageDecoder { public: /** * @brief Construct a new BMP Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - BmpDecoder(OnlineImage *image) : ImageDecoder(image) {} + BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {} int HOT decode(uint8_t *buffer, size_t size) override; + bool is_finished() const override { + // BMP is finished when we've decoded all pixel data + return this->paint_index_ >= static_cast(this->width_ * this->height_); + } + protected: size_t current_index_{0}; size_t paint_index_{0}; @@ -36,7 +41,6 @@ class BmpDecoder : public ImageDecoder { uint8_t padding_bytes_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/runtime_image/image_decoder.cpp b/esphome/components/runtime_image/image_decoder.cpp new file mode 100644 index 00000000000..8d3320b5d17 --- /dev/null +++ b/esphome/components/runtime_image/image_decoder.cpp @@ -0,0 +1,28 @@ +#include "image_decoder.h" +#include "runtime_image.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::runtime_image { + +static const char *const TAG = "image_decoder"; + +bool ImageDecoder::set_size(int width, int height) { + bool success = this->image_->resize(width, height) > 0; + this->x_scale_ = static_cast(this->image_->get_buffer_width()) / width; + this->y_scale_ = static_cast(this->image_->get_buffer_height()) / height; + return success; +} + +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + auto width = std::min(this->image_->get_buffer_width(), static_cast(std::ceil((x + w) * this->x_scale_))); + auto height = std::min(this->image_->get_buffer_height(), static_cast(std::ceil((y + h) * this->y_scale_))); + for (int i = x * this->x_scale_; i < width; i++) { + for (int j = y * this->y_scale_; j < height; j++) { + this->image_->draw_pixel(i, j, color); + } + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/runtime_image/image_decoder.h similarity index 60% rename from esphome/components/online_image/image_decoder.h rename to esphome/components/runtime_image/image_decoder.h index d11b8b46d3b..926108a8a0e 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/runtime_image/image_decoder.h @@ -1,8 +1,7 @@ #pragma once #include "esphome/core/color.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { enum DecodeError : int { DECODE_ERROR_INVALID_TYPE = -1, @@ -10,7 +9,7 @@ enum DecodeError : int { DECODE_ERROR_OUT_OF_MEMORY = -3, }; -class OnlineImage; +class RuntimeImage; /** * @brief Class to abstract decoding different image formats. @@ -20,19 +19,19 @@ class ImageDecoder { /** * @brief Construct a new Image Decoder object * - * @param image The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - ImageDecoder(OnlineImage *image) : image_(image) {} + ImageDecoder(RuntimeImage *image) : image_(image) {} virtual ~ImageDecoder() = default; /** * @brief Initialize the decoder. * - * @param download_size The total number of bytes that need to be downloaded for the image. + * @param expected_size Hint about the expected data size (0 if unknown). * @return int Returns 0 on success, a {@see DecodeError} value in case of an error. */ - virtual int prepare(size_t download_size) { - this->download_size_ = download_size; + virtual int prepare(size_t expected_size) { + this->expected_size_ = expected_size; return 0; } @@ -73,49 +72,26 @@ class ImageDecoder { */ void draw(int x, int y, int w, int h, const Color &color); - bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } + /** + * @brief Check if the decoder has finished processing. + * + * This should be overridden by decoders that can detect completion + * based on format-specific markers rather than byte counts. + */ + virtual bool is_finished() const { + if (this->expected_size_ > 0) { + return this->decoded_bytes_ >= this->expected_size_; + } + // If size is unknown, derived classes should override this + return false; + } protected: - OnlineImage *image_; - // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". - // Will be overwritten anyway once the download size is known. - size_t download_size_ = 1; - size_t decoded_bytes_ = 0; + RuntimeImage *image_; + size_t expected_size_ = 0; // Expected data size (0 if unknown) + size_t decoded_bytes_ = 0; // Bytes processed so far double x_scale_ = 1.0; double y_scale_ = 1.0; }; -class DownloadBuffer { - public: - DownloadBuffer(size_t size); - - virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } - - uint8_t *data(size_t offset = 0); - - uint8_t *append() { return this->data(this->unread_); } - - size_t unread() const { return this->unread_; } - size_t size() const { return this->size_; } - size_t free_capacity() const { return this->size_ - this->unread_; } - - size_t read(size_t len); - size_t write(size_t len) { - this->unread_ += len; - return this->unread_; - } - - void reset() { this->unread_ = 0; } - - size_t resize(size_t size); - - protected: - RAMAllocator allocator_{}; - uint8_t *buffer_; - size_t size_; - /** Total number of downloaded bytes not yet read. */ - size_t unread_; -}; - -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/runtime_image/jpeg_decoder.cpp similarity index 69% rename from esphome/components/online_image/jpeg_image.cpp rename to esphome/components/runtime_image/jpeg_decoder.cpp index 10586091d5d..dcaa07cd58c 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/runtime_image/jpeg_decoder.cpp @@ -1,16 +1,19 @@ -#include "jpeg_image.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#include "jpeg_decoder.h" +#ifdef USE_RUNTIME_IMAGE_JPEG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "online_image.h" -static const char *const TAG = "online_image.jpeg"; +#ifdef USE_ESP_IDF +#include "esp_task_wdt.h" +#endif -namespace esphome { -namespace online_image { +static const char *const TAG = "image_decoder.jpeg"; + +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the JPEGDEC engine when a chunk @@ -22,8 +25,14 @@ static int draw_callback(JPEGDRAW *jpeg) { ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser; // Some very big images take too long to decode, so feed the watchdog on each callback - // to avoid crashing. - App.feed_wdt(); + // to avoid crashing if the executing task has a watchdog enabled. +#ifdef USE_ESP_IDF + if (esp_task_wdt_status(nullptr) == ESP_OK) { +#endif + App.feed_wdt(); +#ifdef USE_ESP_IDF + } +#endif size_t position = 0; size_t height = static_cast(jpeg->iHeight); size_t width = static_cast(jpeg->iWidth); @@ -43,22 +52,23 @@ static int draw_callback(JPEGDRAW *jpeg) { return 1; } -int JpegDecoder::prepare(size_t download_size) { - ImageDecoder::prepare(download_size); - auto size = this->image_->resize_download_buffer(download_size); - if (size < download_size) { - ESP_LOGE(TAG, "Download buffer resize failed!"); - return DECODE_ERROR_OUT_OF_MEMORY; - } +int JpegDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); + // JPEG decoder needs complete data before decoding return 0; } int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { - if (size < this->download_size_) { - ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_); + // JPEG decoder requires complete data + // If we know the expected size, wait for it + if (this->expected_size_ > 0 && size < this->expected_size_) { + ESP_LOGV(TAG, "Download not complete. Size: %zu/%zu", size, this->expected_size_); return 0; } + // If size unknown, try to decode and see if it's valid + // The JPEGDEC library will fail gracefully if data is incomplete + if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError()); return DECODE_ERROR_INVALID_TYPE; @@ -88,7 +98,6 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { return size; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/jpeg_image.h b/esphome/components/runtime_image/jpeg_decoder.h similarity index 54% rename from esphome/components/online_image/jpeg_image.h rename to esphome/components/runtime_image/jpeg_decoder.h index fd488d6138e..ed2401e2630 100644 --- a/esphome/components/online_image/jpeg_image.h +++ b/esphome/components/runtime_image/jpeg_decoder.h @@ -1,12 +1,12 @@ #pragma once #include "image_decoder.h" +#include "runtime_image.h" #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#ifdef USE_RUNTIME_IMAGE_JPEG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for JPEG images. @@ -16,19 +16,18 @@ class JpegDecoder : public ImageDecoder { /** * @brief Construct a new JPEG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} + JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {} ~JpegDecoder() override {} - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: JPEGDEC jpeg_{}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/runtime_image/png_decoder.cpp similarity index 77% rename from esphome/components/online_image/png_image.cpp rename to esphome/components/runtime_image/png_decoder.cpp index ce9d3bdc916..591504328d8 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -1,15 +1,14 @@ -#include "png_image.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_decoder.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "online_image.png"; +static const char *const TAG = "image_decoder.png"; -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the PNGLE engine when the basic @@ -49,9 +48,10 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui } } -PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { +PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { { - pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); + RAMAllocator allocator; + pngle_t *pngle = allocator.allocate(1, PNGLE_T_SIZE); if (!pngle) { ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!"); return; @@ -65,12 +65,13 @@ PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { PngDecoder::~PngDecoder() { if (this->pngle_) { pngle_reset(this->pngle_); - this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE); + RAMAllocator allocator; + allocator.deallocate(this->pngle_, PNGLE_T_SIZE); } } -int PngDecoder::prepare(size_t download_size) { - ImageDecoder::prepare(download_size); +int PngDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); if (!this->pngle_) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; @@ -86,8 +87,9 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; } - if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { - ESP_LOGD(TAG, "Waiting for data"); + // PNG can be decoded progressively, but wait for a reasonable chunk + if (size < 256 && this->expected_size_ > 0 && size < this->expected_size_ - this->decoded_bytes_) { + ESP_LOGD(TAG, "Waiting for more data"); return 0; } auto fed = pngle_feed(this->pngle_, buffer, size); @@ -99,7 +101,6 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { return fed; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/online_image/png_image.h b/esphome/components/runtime_image/png_decoder.h similarity index 62% rename from esphome/components/online_image/png_image.h rename to esphome/components/runtime_image/png_decoder.h index 40e85dde33f..24521d33a87 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -3,11 +3,11 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "image_decoder.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "runtime_image.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for PNG images. @@ -17,24 +17,22 @@ class PngDecoder : public ImageDecoder { /** * @brief Construct a new PNG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - PngDecoder(OnlineImage *image); + PngDecoder(RuntimeImage *image); ~PngDecoder() override; - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } protected: - RAMAllocator allocator_; - pngle_t *pngle_; + pngle_t *pngle_{nullptr}; uint32_t pixels_decoded_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp new file mode 100644 index 00000000000..603ea76f01a --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -0,0 +1,302 @@ +#include "runtime_image.h" +#include "image_decoder.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +#ifdef USE_RUNTIME_IMAGE_BMP +#include "bmp_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG +#include "jpeg_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_PNG +#include "png_decoder.h" +#endif + +namespace esphome::runtime_image { + +static const char *const TAG = "runtime_image"; + +inline bool is_color_on(const Color &color) { + // This produces the most accurate monochrome conversion, but is slightly slower. + // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; + + // Approximation using fast integer computations; produces acceptable results + // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B + return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; +} + +RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height) + : Image(nullptr, 0, 0, type, transparency), + format_(format), + fixed_width_(fixed_width), + fixed_height_(fixed_height), + placeholder_(placeholder), + is_big_endian_(is_big_endian) {} + +RuntimeImage::~RuntimeImage() { this->release(); } + +int RuntimeImage::resize(int width, int height) { + // Use fixed dimensions if specified (0 means auto-resize) + int target_width = this->fixed_width_ ? this->fixed_width_ : width; + int target_height = this->fixed_height_ ? this->fixed_height_ : height; + + size_t result = this->resize_buffer_(target_width, target_height); + if (result > 0 && this->progressive_display_) { + // Update display dimensions for progressive display + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + return result; +} + +void RuntimeImage::draw_pixel(int x, int y, const Color &color) { + if (!this->buffer_) { + ESP_LOGE(TAG, "Buffer not allocated!"); + return; + } + if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { + ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); + return; + } + + switch (this->type_) { + case image::IMAGE_TYPE_BINARY: { + const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u; + uint32_t pos = x + y * width_8; + auto bitno = 0x80 >> (pos % 8u); + pos /= 8u; + auto on = is_color_on(color); + if (this->has_transparency() && color.w < 0x80) + on = false; + if (on) { + this->buffer_[pos] |= bitno; + } else { + this->buffer_[pos] &= ~bitno; + } + break; + } + case image::IMAGE_TYPE_GRAYSCALE: { + uint32_t pos = this->get_position_(x, y); + auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (gray == 1) { + gray = 0; + } + if (color.w < 0x80) { + gray = 1; + } + } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + if (color.w != 0xFF) + gray = color.w; + } + this->buffer_[pos] = gray; + break; + } + case image::IMAGE_TYPE_RGB565: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color); + if (this->is_big_endian_) { + this->buffer_[pos + 0] = static_cast((rgb565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(rgb565 & 0xFF); + } else { + this->buffer_[pos + 0] = static_cast(rgb565 & 0xFF); + this->buffer_[pos + 1] = static_cast((rgb565 >> 8) & 0xFF); + } + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 2] = color.w; + } + break; + } + case image::IMAGE_TYPE_RGB: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + this->buffer_[pos + 0] = mapped_color.r; + this->buffer_[pos + 1] = mapped_color.g; + this->buffer_[pos + 2] = mapped_color.b; + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 3] = color.w; + } + break; + } + } +} + +void RuntimeImage::map_chroma_key(Color &color) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (color.g == 1 && color.r == 0 && color.b == 0) { + color.g = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1; + color.b = 0; + } + } +} + +void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + // If we have a complete image, use the base class draw method + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + // Show placeholder while the runtime image is not available + this->placeholder_->draw(x, y, display, color_on, color_off); + } + // If no image is loaded and no placeholder, nothing to draw +} + +bool RuntimeImage::begin_decode(size_t expected_size) { + if (this->decoder_) { + ESP_LOGW(TAG, "Decoding already in progress"); + return false; + } + + this->decoder_ = this->create_decoder_(); + if (!this->decoder_) { + ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_); + return false; + } + + this->total_size_ = expected_size; + this->decoded_bytes_ = 0; + + // Initialize decoder + int result = this->decoder_->prepare(expected_size); + if (result < 0) { + ESP_LOGE(TAG, "Failed to prepare decoder: %d", result); + this->decoder_ = nullptr; + return false; + } + + return true; +} + +int RuntimeImage::feed_data(uint8_t *data, size_t len) { + if (!this->decoder_) { + ESP_LOGE(TAG, "No decoder initialized"); + return -1; + } + + int consumed = this->decoder_->decode(data, len); + if (consumed > 0) { + this->decoded_bytes_ += consumed; + } + + return consumed; +} + +bool RuntimeImage::end_decode() { + if (!this->decoder_) { + return false; + } + + // Finalize the image for display + if (!this->progressive_display_) { + // Only now make the image visible + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + + // Clean up decoder + this->decoder_ = nullptr; + + ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_); + return true; +} + +bool RuntimeImage::is_decode_finished() const { + if (!this->decoder_) { + return false; + } + return this->decoder_->is_finished(); +} + +void RuntimeImage::release() { + this->release_buffer_(); + // Reset decoder separately — release() can be called from within the decoder + // (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here. + // The decoder lifecycle is managed by begin_decode()/end_decode(). + this->decoder_ = nullptr; +} + +void RuntimeImage::release_buffer_() { + if (this->buffer_) { + ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + this->buffer_ = nullptr; + this->data_start_ = nullptr; + this->width_ = 0; + this->height_ = 0; + this->buffer_width_ = 0; + this->buffer_height_ = 0; + } +} + +size_t RuntimeImage::resize_buffer_(int width, int height) { + size_t new_size = this->get_buffer_size_(width, height); + + if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) { + // Buffer already allocated with correct size + return new_size; + } + + // Release old buffer if dimensions changed + if (this->buffer_) { + this->release_buffer_(); + } + + ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size); + RAMAllocator allocator; + this->buffer_ = allocator.allocate(new_size); + + if (!this->buffer_) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size, + allocator.get_max_free_block_size()); + return 0; + } + + // Clear buffer + memset(this->buffer_, 0, new_size); + + this->buffer_width_ = width; + this->buffer_height_ = height; + + return new_size; +} + +size_t RuntimeImage::get_buffer_size_(int width, int height) const { + return (this->get_bpp() * width + 7u) / 8u * height; +} + +int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } + +std::unique_ptr RuntimeImage::create_decoder_() { + switch (this->format_) { +#ifdef USE_RUNTIME_IMAGE_BMP + case BMP: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case JPEG: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case PNG: + return make_unique(this); +#endif + default: + ESP_LOGE(TAG, "Unsupported image format: %d", this->format_); + return nullptr; + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h new file mode 100644 index 00000000000..4bdcdcac9e0 --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.h @@ -0,0 +1,213 @@ +#pragma once + +#include "esphome/components/image/image.h" +#include "esphome/core/helpers.h" + +namespace esphome::runtime_image { + +// Forward declaration +class ImageDecoder; + +/** + * @brief Image format types that can be decoded dynamically. + */ +enum ImageFormat { + /** Automatically detect from data. Not implemented yet. */ + AUTO, + /** JPEG format. */ + JPEG, + /** PNG format. */ + PNG, + /** BMP format. */ + BMP, +}; + +/** + * @brief A dynamic image that can be loaded and decoded at runtime. + * + * This class provides dynamic buffer allocation and management for images + * that are decoded at runtime, as opposed to static images compiled into + * the firmware. It serves as a base class for components that need to + * load images dynamically from various sources. + */ +class RuntimeImage : public image::Image { + public: + /** + * @brief Construct a new RuntimeImage object. + * + * @param format The image format to decode. + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. + * @param is_big_endian Whether the image is stored in big-endian format. + * @param fixed_width Fixed width for the image (0 for auto-resize). + * @param fixed_height Fixed height for the image (0 for auto-resize). + */ + RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder = nullptr, bool is_big_endian = false, int fixed_width = 0, + int fixed_height = 0); + + ~RuntimeImage(); + + // Decoder interface methods + /** + * @brief Resize the image buffer to the requested dimensions. + * + * The buffer will be allocated if not existing. + * If fixed dimensions have been specified in the constructor, the buffer will be created + * with those dimensions and not resized, even on request. + * Otherwise, the old buffer will be deallocated and a new buffer with the requested + * dimensions allocated. + * + * @param width Requested width (ignored if fixed_width_ is set) + * @param height Requested height (ignored if fixed_height_ is set) + * @return Size of the allocated buffer in bytes, or 0 if allocation failed. + */ + int resize(int width, int height); + void draw_pixel(int x, int y, const Color &color); + void map_chroma_key(Color &color); + int get_buffer_width() const { return this->buffer_width_; } + int get_buffer_height() const { return this->buffer_height_; } + + // Image drawing interface + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + + /** + * @brief Begin decoding an image. + * + * @param expected_size Optional hint about the expected data size. + * @return true if decoder was successfully initialized. + */ + bool begin_decode(size_t expected_size = 0); + + /** + * @brief Feed data to the decoder. + * + * @param data Pointer to the data buffer. + * @param len Length of data to process. + * @return Number of bytes consumed by the decoder. + */ + int feed_data(uint8_t *data, size_t len); + + /** + * @brief Complete the decoding process. + * + * @return true if decoding completed successfully. + */ + bool end_decode(); + + /** + * @brief Check if decoding is currently in progress. + */ + bool is_decoding() const { return this->decoder_ != nullptr; } + + /** + * @brief Check if the decoder has finished processing all data. + * + * This delegates to the decoder's format-specific completion check, + * which handles both known-size and chunked transfer cases. + */ + bool is_decode_finished() const; + + /** + * @brief Check if an image is currently loaded. + */ + bool is_loaded() const { return this->buffer_ != nullptr; } + + /** + * @brief Get the image format. + */ + ImageFormat get_format() const { return this->format_; } + + /** + * @brief Release the image buffer and free memory. + */ + void release(); + + /** + * @brief Set whether to allow progressive display during decode. + * + * When enabled, the image can be displayed even while still decoding. + * When disabled, the image is only displayed after decoding completes. + */ + void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; } + + protected: + /** + * @brief Resize the image buffer to the requested dimensions. + * + * @param width New width in pixels. + * @param height New height in pixels. + * @return Size of the allocated buffer, or 0 on failure. + */ + size_t resize_buffer_(int width, int height); + + /** + * @brief Release only the image buffer without resetting the decoder. + * + * This is safe to call from within the decoder (e.g., during resize). + */ + void release_buffer_(); + + /** + * @brief Get the buffer size in bytes for given dimensions. + */ + size_t get_buffer_size_(int width, int height) const; + + /** + * @brief Get the position in the buffer for a pixel. + */ + int get_position_(int x, int y) const; + + /** + * @brief Create decoder instance for the image's format. + */ + std::unique_ptr create_decoder_(); + + // Memory management + uint8_t *buffer_{nullptr}; + + // Decoder management + std::unique_ptr decoder_{nullptr}; + /** The image format this RuntimeImage is configured to decode. */ + const ImageFormat format_; + + /** + * Actual width of the current image. + * This needs to be separate from "Image::get_width()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_width_{0}; + /** + * Actual height of the current image. + * This needs to be separate from "Image::get_height()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_height_{0}; + + // Decoding state + size_t total_size_{0}; + size_t decoded_bytes_{0}; + + /** Fixed width requested on configuration, or 0 if not specified. */ + const int fixed_width_{0}; + /** Fixed height requested on configuration, or 0 if not specified. */ + const int fixed_height_{0}; + + /** Placeholder image to show when the runtime image is not available. */ + image::Image *placeholder_{nullptr}; + + // Configuration + bool progressive_display_{false}; + /** + * Whether the image is stored in big-endian format. + * This is used to determine how to store 16 bit colors in the buffer. + */ + bool is_big_endian_{false}; +}; + +} // namespace esphome::runtime_image diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index b670b9c068b..c96713f3766 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -16,19 +16,13 @@ namespace esphome::socket { class BSDSocketImpl final : public Socket { public: - BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { -#ifdef USE_SOCKET_SELECT_SUPPORT + BSDSocketImpl(int fd, bool monitor_loop = false) { + this->fd_ = fd; // Register new socket with the application for select() if monitoring requested if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds this->loop_monitored_ = App.register_socket_fd(this->fd_); - } else { - this->loop_monitored_ = false; } -#else - // Without select support, ignore monitor_loop parameter - (void) monitor_loop; -#endif } ~BSDSocketImpl() override { if (!this->closed_) { @@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket { int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int close() override { if (!this->closed_) { -#ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } -#endif int ret = ::close(this->fd_); this->closed_ = true; return ret; @@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket { ::fcntl(this->fd_, F_SETFL, fl); return 0; } - - int get_fd() const override { return this->fd_; } - -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const override { - if (!this->loop_monitored_) - return true; - return App.is_socket_ready(this->fd_); - } -#endif - - protected: - int fd_; - bool closed_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; -#endif }; // Helper to create a socket with optional monitoring diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index a9c2eda4e80..aa37386d700 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -452,6 +452,8 @@ class LWIPRawImpl : public Socket { errno = ENOSYS; return -1; } + bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } + int setblocking(bool blocking) final { if (pcb_ == nullptr) { errno = ECONNRESET; @@ -576,6 +578,8 @@ class LWIPRawListenImpl final : public LWIPRawImpl { tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler } + bool ready() const override { return this->accepted_socket_count_ > 0; } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { if (pcb_ == nullptr) { errno = EBADF; diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index a885f243f3a..79d68e085ae 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -11,19 +11,13 @@ namespace esphome::socket { class LwIPSocketImpl final : public Socket { public: - LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { -#ifdef USE_SOCKET_SELECT_SUPPORT + LwIPSocketImpl(int fd, bool monitor_loop = false) { + this->fd_ = fd; // Register new socket with the application for select() if monitoring requested if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds this->loop_monitored_ = App.register_socket_fd(this->fd_); - } else { - this->loop_monitored_ = false; } -#else - // Without select support, ignore monitor_loop parameter - (void) monitor_loop; -#endif } ~LwIPSocketImpl() override { if (!this->closed_) { @@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket { int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int close() override { if (!this->closed_) { -#ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } -#endif int ret = lwip_close(this->fd_); this->closed_ = true; return ret; @@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket { lwip_fcntl(this->fd_, F_SETFL, fl); return 0; } - - int get_fd() const override { return this->fd_; } - -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const override { - if (!this->loop_monitored_) - return true; - return App.is_socket_ready(this->fd_); - } -#endif - - protected: - int fd_; - bool closed_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; -#endif }; // Helper to create a socket with optional monitoring diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index fd8725b3632..2fcc162eadc 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -10,6 +10,10 @@ namespace esphome::socket { Socket::~Socket() {} +#ifdef USE_SOCKET_SELECT_SUPPORT +bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); } +#endif + // Platform-specific inet_ntop wrappers #if defined(USE_SOCKET_IMPL_LWIP_TCP) // LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index e8b0948acdb..c0098d689a1 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -63,13 +63,29 @@ class Socket { virtual int setblocking(bool blocking) = 0; virtual int loop() { return 0; }; - /// Get the underlying file descriptor (returns -1 if not supported) - virtual int get_fd() const { return -1; } + /// Get the underlying file descriptor (returns -1 if not supported) + /// Non-virtual: only one socket implementation is active per build. +#ifdef USE_SOCKET_SELECT_SUPPORT + int get_fd() const { return this->fd_; } +#else + int get_fd() const { return -1; } +#endif /// Check if socket has data ready to read - /// For loop-monitored sockets, checks with the Application's select() results - /// For non-monitored sockets, always returns true (assumes data may be available) + /// For select()-based sockets: non-virtual, checks Application's select() results + /// For LWIP raw TCP sockets: virtual, checks internal buffer state +#ifdef USE_SOCKET_SELECT_SUPPORT + bool ready() const; +#else virtual bool ready() const { return true; } +#endif + + protected: +#ifdef USE_SOCKET_SELECT_SUPPORT + int fd_{-1}; + bool closed_{false}; + bool loop_monitored_{false}; +#endif }; /// Create a socket of the given domain, type and protocol. diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index af98e3a51f9..55f7fd162c2 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -7,15 +7,15 @@ namespace speed { static const char *const TAG = "speed.fan"; void SpeedFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index eae6ecbf31f..9e423c17609 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -4,6 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include #include @@ -1544,42 +1545,19 @@ void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) { ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name)); } +// Request origin strings indexed by SprinklerValveRunRequestOrigin enum (0-2): USER, CYCLE, QUEUE +PROGMEM_STRING_TABLE(SprinklerRequestOriginStrings, "USER", "CYCLE", "QUEUE", "UNKNOWN"); + const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { - switch (origin) { - case USER: - return LOG_STR("USER"); - - case CYCLE: - return LOG_STR("CYCLE"); - - case QUEUE: - return LOG_STR("QUEUE"); - - default: - return LOG_STR("UNKNOWN"); - } + return SprinklerRequestOriginStrings::get_log_str(static_cast(origin), + SprinklerRequestOriginStrings::LAST_INDEX); } +// Sprinkler state strings indexed by SprinklerState enum (0-4): IDLE, STARTING, ACTIVE, STOPPING, BYPASS +PROGMEM_STRING_TABLE(SprinklerStateStrings, "IDLE", "STARTING", "ACTIVE", "STOPPING", "BYPASS", "UNKNOWN"); + const LogString *Sprinkler::state_as_str_(SprinklerState state) { - switch (state) { - case IDLE: - return LOG_STR("IDLE"); - - case STARTING: - return LOG_STR("STARTING"); - - case ACTIVE: - return LOG_STR("ACTIVE"); - - case STOPPING: - return LOG_STR("STOPPING"); - - case BYPASS: - return LOG_STR("BYPASS"); - - default: - return LOG_STR("UNKNOWN"); - } + return SprinklerStateStrings::get_log_str(static_cast(state), SprinklerStateStrings::LAST_INDEX); } void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index e0e7f94ce08..be99bd93da1 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -1,6 +1,7 @@ #include "ssd1306_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace ssd1306_base { @@ -40,6 +41,55 @@ static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC; static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD; +// Verify first enum value and table sizes match SSD1306_MODEL_COUNT +static_assert(SSD1306_MODEL_128_32 == 0, "SSD1306Model enum must start at 0"); + +// PROGMEM lookup table indexed by SSD1306Model enum (width, height per model) +struct ModelDimensions { + uint8_t width; + uint8_t height; +}; +static const ModelDimensions MODEL_DIMS[] PROGMEM = { + {128, 32}, // SSD1306_MODEL_128_32 + {128, 64}, // SSD1306_MODEL_128_64 + {96, 16}, // SSD1306_MODEL_96_16 + {64, 48}, // SSD1306_MODEL_64_48 + {64, 32}, // SSD1306_MODEL_64_32 + {72, 40}, // SSD1306_MODEL_72_40 + {128, 32}, // SH1106_MODEL_128_32 + {128, 64}, // SH1106_MODEL_128_64 + {96, 16}, // SH1106_MODEL_96_16 + {64, 48}, // SH1106_MODEL_64_48 + {64, 128}, // SH1107_MODEL_128_64 (note: width is 64, height is 128) + {128, 128}, // SH1107_MODEL_128_128 + {128, 32}, // SSD1305_MODEL_128_32 + {128, 64}, // SSD1305_MODEL_128_64 +}; + +// clang-format off +PROGMEM_STRING_TABLE(ModelStrings, + "SSD1306 128x32", // SSD1306_MODEL_128_32 + "SSD1306 128x64", // SSD1306_MODEL_128_64 + "SSD1306 96x16", // SSD1306_MODEL_96_16 + "SSD1306 64x48", // SSD1306_MODEL_64_48 + "SSD1306 64x32", // SSD1306_MODEL_64_32 + "SSD1306 72x40", // SSD1306_MODEL_72_40 + "SH1106 128x32", // SH1106_MODEL_128_32 + "SH1106 128x64", // SH1106_MODEL_128_64 + "SH1106 96x16", // SH1106_MODEL_96_16 + "SH1106 64x48", // SH1106_MODEL_64_48 + "SH1107 128x64", // SH1107_MODEL_128_64 + "SH1107 128x128", // SH1107_MODEL_128_128 + "SSD1305 128x32", // SSD1305_MODEL_128_32 + "SSD1305 128x64", // SSD1305_MODEL_128_64 + "Unknown" // fallback +); +// clang-format on +static_assert(sizeof(MODEL_DIMS) / sizeof(MODEL_DIMS[0]) == SSD1306_MODEL_COUNT, + "MODEL_DIMS must have one entry per SSD1306Model"); +static_assert(ModelStrings::COUNT == SSD1306_MODEL_COUNT + 1, + "ModelStrings must have one entry per SSD1306Model plus fallback"); + void SSD1306::setup() { this->init_internal_(this->get_buffer_length_()); @@ -146,6 +196,7 @@ void SSD1306::setup() { break; case SH1107_MODEL_128_64: case SH1107_MODEL_128_128: + case SSD1306_MODEL_COUNT: // Not used, but prevents build warning break; } @@ -274,54 +325,14 @@ void SSD1306::turn_off() { this->is_on_ = false; } int SSD1306::get_height_internal() { - switch (this->model_) { - case SH1107_MODEL_128_64: - case SH1107_MODEL_128_128: - return 128; - case SSD1306_MODEL_128_32: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_128_32: - case SSD1305_MODEL_128_32: - return 32; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1305_MODEL_128_64: - return 64; - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - return 16; - case SSD1306_MODEL_64_48: - case SH1106_MODEL_64_48: - return 48; - case SSD1306_MODEL_72_40: - return 40; - default: - return 0; - } + if (this->model_ >= SSD1306_MODEL_COUNT) + return 0; + return progmem_read_byte(&MODEL_DIMS[this->model_].height); } int SSD1306::get_width_internal() { - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1305_MODEL_128_32: - case SSD1305_MODEL_128_64: - case SH1107_MODEL_128_128: - return 128; - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - return 96; - case SSD1306_MODEL_64_48: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_64_48: - case SH1107_MODEL_128_64: - return 64; - case SSD1306_MODEL_72_40: - return 72; - default: - return 0; - } + if (this->model_ >= SSD1306_MODEL_COUNT) + return 0; + return progmem_read_byte(&MODEL_DIMS[this->model_].width); } size_t SSD1306::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; @@ -361,37 +372,8 @@ void SSD1306::init_reset_() { this->reset_pin_->digital_write(true); } } -const char *SSD1306::model_str_() { - switch (this->model_) { - case SSD1306_MODEL_128_32: - return "SSD1306 128x32"; - case SSD1306_MODEL_128_64: - return "SSD1306 128x64"; - case SSD1306_MODEL_64_32: - return "SSD1306 64x32"; - case SSD1306_MODEL_96_16: - return "SSD1306 96x16"; - case SSD1306_MODEL_64_48: - return "SSD1306 64x48"; - case SSD1306_MODEL_72_40: - return "SSD1306 72x40"; - case SH1106_MODEL_128_32: - return "SH1106 128x32"; - case SH1106_MODEL_128_64: - return "SH1106 128x64"; - case SH1106_MODEL_96_16: - return "SH1106 96x16"; - case SH1106_MODEL_64_48: - return "SH1106 64x48"; - case SH1107_MODEL_128_64: - return "SH1107 128x64"; - case SSD1305_MODEL_128_32: - return "SSD1305 128x32"; - case SSD1305_MODEL_128_64: - return "SSD1305 128x64"; - default: - return "Unknown"; - } +const LogString *SSD1306::model_str_() { + return ModelStrings::get_log_str(static_cast(this->model_), ModelStrings::LAST_INDEX); } } // namespace ssd1306_base diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index a5734373861..3cc795a3236 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -22,6 +22,9 @@ enum SSD1306Model { SH1107_MODEL_128_128, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, + // When adding a new model, add it before SSD1306_MODEL_COUNT and update + // MODEL_DIMS and ModelStrings tables in ssd1306_base.cpp + SSD1306_MODEL_COUNT, // must be last }; class SSD1306 : public display::DisplayBuffer { @@ -70,7 +73,7 @@ class SSD1306 : public display::DisplayBuffer { int get_height_internal() override; int get_width_internal() override; size_t get_buffer_length_(); - const char *model_str_(); + const LogString *model_str_(); SSD1306Model model_{SSD1306_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 47a21a8ff40..e1f6e912430 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -28,7 +28,7 @@ void I2CSSD1306::dump_config() { " Offset X: %d\n" " Offset Y: %d\n" " Inverted Color: %s", - this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), + LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), this->offset_x_, this->offset_y_, YESNO(this->invert_)); LOG_I2C_DEVICE(this); LOG_PIN(" Reset Pin: ", this->reset_pin_); diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index db28dfc564b..af9a17c8ab2 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -24,7 +24,7 @@ void SPISSD1306::dump_config() { " Offset X: %d\n" " Offset Y: %d\n" " Inverted Color: %s", - this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), + LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), this->offset_x_, this->offset_y_, YESNO(this->invert_)); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 0e1920a984d..cd267bd552c 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -6,15 +6,15 @@ namespace esphome::template_ { static const char *const TAG = "template.fan"; void TemplateFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); } - - // Construct traits - this->traits_ = - fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 19b9a4077fa..6c242220a69 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -90,7 +90,6 @@ void IDFUARTComponent::setup() { return; } this->uart_num_ = static_cast(next_uart_num++); - this->lock_ = xSemaphoreCreateMutex(); #if (SOC_UART_LP_NUM >= 1) size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); @@ -102,11 +101,7 @@ void IDFUARTComponent::setup() { this->rx_buffer_size_ = fifo_len * 2; } - xSemaphoreTake(this->lock_, portMAX_DELAY); - this->load_settings(false); - - xSemaphoreGive(this->lock_); } void IDFUARTComponent::load_settings(bool dump_config) { @@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } } +#ifdef USE_UART_WAKE_LOOP_ON_RX + constexpr int event_queue_size = 20; + QueueHandle_t *event_queue_ptr = &this->uart_event_queue_; +#else + constexpr int event_queue_size = 0; + QueueHandle_t *event_queue_ptr = nullptr; +#endif err = uart_driver_install(this->uart_num_, // UART number this->rx_buffer_size_, // RX ring buffer size - 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will - // block task until all data has been sent out - 20, // event queue size/depth - &this->uart_event_queue_, // event queue - 0 // Flags used to allocate the interrupt + 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will + // block task until all data has been sent out + event_queue_size, // event queue size/depth + event_queue_ptr, // event queue + 0 // Flags used to allocate the interrupt ); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); @@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { } void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { - xSemaphoreTake(this->lock_, portMAX_DELAY); int32_t write_len = uart_write_bytes(this->uart_num_, data, len); - xSemaphoreGive(this->lock_); if (write_len != (int32_t) len) { ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); this->mark_failed(); @@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { bool IDFUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { *data = this->peek_byte_; } else { @@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { this->peek_byte_ = *data; } } - xSemaphoreGive(this->lock_); return true; } @@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; @@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { } if (length_to_read > 0) read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); - xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); @@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() { size_t available = 0; esp_err_t err; - xSemaphoreTake(this->lock_, portMAX_DELAY); err = uart_get_buffered_data_len(this->uart_num_, &available); - xSemaphoreGive(this->lock_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); @@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() { void IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - xSemaphoreTake(this->lock_, portMAX_DELAY); uart_wait_tx_done(this->uart_num_, portMAX_DELAY); - xSemaphoreGive(this->lock_); } void IDFUARTComponent::check_logger_conflict() {} @@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() { ESP_LOGV(TAG, "RX event task started"); } +// FreeRTOS task that relays UART ISR events to the main loop. +// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a +// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline. +// IMPORTANT: This task must NOT call any UART wrapper methods (read_array, +// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading +// is done by the main loop. This task only reads from the event queue and +// calls App.wake_loop_threadsafe(). void IDFUARTComponent::rx_event_task_func(void *param) { auto *self = static_cast(param); uart_event_t event; @@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) { case UART_FIFO_OVF: case UART_BUFFER_FULL: - ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing"); - uart_flush_input(self->uart_num_); + // Don't call uart_flush_input() here — this task does not own the read side. + // ESP-IDF examples flush on overflow because the same task handles both events + // and reads, so flush and read are serialized. Here, reads happen on the main + // loop, so flushing from this task races with read_array() and can destroy data + // mid-read. The driver self-heals without an explicit flush: uart_read_bytes() + // calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes + // into the ring buffer and re-enables RX interrupts once space is freed. + ESP_LOGW(TAG, "FIFO overflow or ring buffer full"); #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 1ecb02d7ab6..1517eab5098 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -8,6 +8,13 @@ namespace esphome::uart { +/// ESP-IDF UART driver wrapper. +/// +/// Thread safety: All public methods must only be called from the main loop. +/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's +/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task +/// (when enabled) must not call any of these methods — it communicates with the +/// main loop exclusively via App.wake_loop_threadsafe(). class IDFUARTComponent : public UARTComponent, public Component { public: void setup() override; @@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component { void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } +#ifdef USE_UART_WAKE_LOOP_ON_RX QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } +#endif /** * Load the UART with the current settings. @@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component { protected: void check_logger_conflict() override; uart_port_t uart_num_; - QueueHandle_t uart_event_queue_; uart_config_t get_config_(); - SemaphoreHandle_t lock_; bool has_peek_{false}; uint8_t peek_byte_; #ifdef USE_UART_WAKE_LOOP_ON_RX - // RX notification support + // RX notification support — runs on a separate FreeRTOS task. + // IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array, + // write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the + // event queue and call App.wake_loop_threadsafe(). void start_rx_event_task_(); static void rx_event_task_func(void *param); + QueueHandle_t uart_event_queue_; TaskHandle_t rx_event_task_handle_{nullptr}; #endif // USE_UART_WAKE_LOOP_ON_RX }; diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d11a148a0fb..d1ec356613f 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -148,6 +148,7 @@ class USBClient : public Component { EventPool event_pool; protected: + void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 09da6e3b73a..0612d7a841b 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace esphome { namespace usb_host { @@ -142,18 +143,23 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc } while (next_desc != NULL); } #endif -static std::string get_descriptor_string(const usb_str_desc_t *desc) { - char buffer[256]; - if (desc == nullptr) +// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType). +// Character count = (bLength - 2) / 2, max 126 chars + null terminator. +static constexpr size_t DESC_STRING_BUF_SIZE = 128; + +static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span buffer) { + if (desc == nullptr || desc->bLength < 2) return "(unspecified)"; - char *p = buffer; - for (int i = 0; i != desc->bLength / 2; i++) { + int char_count = (desc->bLength - 2) / 2; + char *p = buffer.data(); + char *end = p + buffer.size() - 1; + for (int i = 0; i != char_count && p < end; i++) { auto c = desc->wData[i]; if (c < 0x100) *p++ = static_cast(c); } *p = '\0'; - return {buffer}; + return buffer.data(); } // CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) @@ -259,60 +265,63 @@ void USBClient::loop() { ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped); } - switch (this->state_) { - case USB_CLIENT_OPEN: { - int err; - ESP_LOGD(TAG, "Open device %d", this->device_addr_); - err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err)); - this->state_ = USB_CLIENT_INIT; - break; - } - ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_); - const usb_device_desc_t *desc; - err = usb_host_get_device_descriptor(this->device_handle_, &desc); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err)); - this->disconnect(); - } else { - ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); - if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) { - usb_device_info_t dev_info; - err = usb_host_device_info(this->device_handle_, &dev_info); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); - this->disconnect(); - break; - } - this->state_ = USB_CLIENT_CONNECTED; - ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s", - get_descriptor_string(dev_info.str_desc_manufacturer).c_str(), - get_descriptor_string(dev_info.str_desc_product).c_str(), - get_descriptor_string(dev_info.str_desc_serial_num).c_str()); + if (this->state_ == USB_CLIENT_OPEN) { + this->handle_open_state_(); + } +} + +void USBClient::handle_open_state_() { + int err; + ESP_LOGD(TAG, "Open device %d", this->device_addr_); + err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err)); + this->state_ = USB_CLIENT_INIT; + return; + } + ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_); + const usb_device_desc_t *desc; + err = usb_host_get_device_descriptor(this->device_handle_, &desc); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err)); + this->disconnect(); + return; + } + ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); + if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) { + if (this->vid_ != 0 || this->pid_ != 0) { + ESP_LOGD(TAG, "Not our device, closing"); + this->disconnect(); + return; + } + } + usb_device_info_t dev_info; + err = usb_host_device_info(this->device_handle_, &dev_info); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); + this->disconnect(); + return; + } + this->state_ = USB_CLIENT_CONNECTED; + char buf_manuf[DESC_STRING_BUF_SIZE]; + char buf_product[DESC_STRING_BUF_SIZE]; + char buf_serial[DESC_STRING_BUF_SIZE]; + ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s", + get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf), + get_descriptor_string(dev_info.str_desc_product, buf_product), + get_descriptor_string(dev_info.str_desc_serial_num, buf_serial)); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - const usb_device_desc_t *device_desc; - err = usb_host_get_device_descriptor(this->device_handle_, &device_desc); - if (err == ESP_OK) - usb_client_print_device_descriptor(device_desc); - const usb_config_desc_t *config_desc; - err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc); - if (err == ESP_OK) - usb_client_print_config_descriptor(config_desc, nullptr); + const usb_device_desc_t *device_desc; + err = usb_host_get_device_descriptor(this->device_handle_, &device_desc); + if (err == ESP_OK) + usb_client_print_device_descriptor(device_desc); + const usb_config_desc_t *config_desc; + err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc); + if (err == ESP_OK) + usb_client_print_config_descriptor(config_desc, nullptr); #endif - this->on_connected(); - } else { - ESP_LOGD(TAG, "Not our device, closing"); - this->disconnect(); - } - } - break; - } - - default: - break; - } + this->on_connected(); } void USBClient::on_opened(uint8_t addr) { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index dfd602be6be..3acd2d21190 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -198,7 +198,8 @@ EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const { #if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue -void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { +void __attribute__((flatten)) +DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size @@ -557,7 +558,9 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J root[ESPHOME_F("device")] = device_name; } #endif +#ifdef USE_ENTITY_ICON root[ESPHOME_F("icon")] = obj->get_icon_ref(); +#endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) @@ -583,8 +586,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { - auto *param = request->getParam(ESPHOME_F("detail")); - return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; + return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } #ifdef USE_SENSOR @@ -861,10 +863,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } auto call = is_on ? obj->turn_on() : obj->turn_off(); - parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); + parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); - if (request->hasParam(ESPHOME_F("oscillation"))) { - auto speed = request->getParam(ESPHOME_F("oscillation"))->value(); + if (request->hasArg(ESPHOME_F("oscillation"))) { + auto speed = request->arg(ESPHOME_F("oscillation")); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -1040,14 +1042,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) || - (request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { + if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) || + (request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); - parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1106,7 +1108,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); + parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1174,12 +1176,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); + call.set_date(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1234,12 +1237,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); + call.set_time(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1293,12 +1297,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); + call.set_datetime(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1477,10 +1482,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature_high"), call, - &decltype(call)::set_target_temperature_high); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); + // static_cast needed to disambiguate overloaded setters (float vs optional) + using ClimateCall = decltype(call); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), call, + static_cast(&ClimateCall::set_target_temperature_high)); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), call, + static_cast(&ClimateCall::set_target_temperature_low)); + parse_num_param_(request, ESPHOME_F("target_temperature"), call, + static_cast(&ClimateCall::set_target_temperature)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1721,12 +1730,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) { + if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1870,12 +1879,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_high); + parse_num_param_(request, ESPHOME_F("target_temperature"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_low); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_high); // Parse away mode parameter parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away); @@ -1979,16 +1988,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); // Parse carrier frequency (optional) - if (request->hasParam(ESPHOME_F("carrier_frequency"))) { - auto value = parse_number(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("carrier_frequency")).c_str()); if (value.has_value()) { call.set_carrier_frequency(*value); } } // Parse repeat count (optional, defaults to 1) - if (request->hasParam(ESPHOME_F("repeat_count"))) { - auto value = parse_number(request->getParam(ESPHOME_F("repeat_count"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("repeat_count")).c_str()); if (value.has_value()) { call.set_repeat_count(*value); } @@ -1996,18 +2005,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) - if (!request->hasParam(ESPHOME_F("data"))) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); - return; - } + const auto &data_arg = request->arg(ESPHOME_F("data")); - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string encoded = - request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr) - - // Validate base64url is not empty - if (encoded.empty()) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter")); + // Validate base64url is not empty (also catches missing parameter since arg() returns empty string) + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty) + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter")); return; } @@ -2015,7 +2018,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // it outlives the call - set_raw_timings_base64url stores a pointer, so the string // must remain valid until perform() completes. // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. - this->defer([call, encoded = std::move(encoded)]() mutable { + this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable { call.set_raw_timings_base64url(encoded); call.perform(); }); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 224c051ecee..026da763ea7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -112,10 +112,10 @@ class DeferredUpdateEventSource : public AsyncEventSource { /* This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for - the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a - std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per - entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing - because of dedup) would take up only 0.8 kB. + the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a + std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two + pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors + publishing because of dedup) would take up only 0.8 kB. */ struct DeferredEvent { friend class DeferredUpdateEventSource; @@ -130,7 +130,9 @@ class DeferredUpdateEventSource : public AsyncEventSource { bool operator==(const DeferredEvent &test) const { return (source_ == test.source_ && message_generator_ == test.message_generator_); } - } __attribute__((packed)); + }; + static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *), + "DeferredEvent should have no padding"); protected: // surface a couple methods from the base class @@ -511,11 +513,9 @@ class WebServer : public Controller, template void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float), float scale = 1.0f) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value / scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value / scale); } } @@ -523,34 +523,19 @@ class WebServer : public Controller, template void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(uint32_t), uint32_t scale = 1) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value * scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value * scale); } } #endif - // Generic helper to parse and apply a float parameter - template - void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } - } - } - - // Generic helper to parse and apply an int parameter - template - void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } + // Generic helper to parse and apply a numeric parameter + template + void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) { + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value); } } @@ -558,10 +543,9 @@ class WebServer : public Controller, template void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(const std::string &)) { - if (request->hasParam(param_name)) { - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) - (call.*setter)(value); + if (request->hasArg(param_name)) { + const auto &value = request->arg(param_name); + (call.*setter)(std::string(value.c_str(), value.length())); } } @@ -571,8 +555,9 @@ class WebServer : public Controller, // Invalid values are ignored (setter not called) template void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) { - if (request->hasParam(param_name)) { - auto param_value = request->getParam(param_name)->value(); + const auto ¶m_value = request->arg(param_name); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (param_value.length() > 0) { // NOLINT(readability-container-size-empty) // First check on/off (default), then true/false (custom) auto val = parse_on_off(param_value.c_str()); if (val == PARSE_NONE) { diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 52dafeb9972..7744272a5cd 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -54,14 +54,15 @@ size_t MultipartReader::parse(const char *data, size_t len) { void MultipartReader::process_header_(const char *value, size_t length) { // Process the completed header (field + value pair) - std::string value_str(value, length); + const char *field = current_header_field_.c_str(); + size_t field_len = current_header_field_.length(); - if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) { + if (str_startswith_case_insensitive(field, field_len, "content-disposition")) { // Parse name and filename from Content-Disposition - current_part_.name = extract_header_param(value_str, "name"); - current_part_.filename = extract_header_param(value_str, "filename"); - } else if (str_startswith_case_insensitive(current_header_field_, "content-type")) { - current_part_.content_type = str_trim(value_str); + extract_header_param(value, length, "name", current_part_.name); + extract_header_param(value, length, "filename", current_part_.filename); + } else if (str_startswith_case_insensitive(field, field_len, "content-type")) { + str_trim(value, length, current_part_.content_type); } // Clear field for next header @@ -107,25 +108,29 @@ int MultipartReader::on_part_data_end(multipart_parser *parser) { // ========== Utility Functions ========== // Case-insensitive string prefix check -bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) { - if (str.length() < prefix.length()) { +bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix) { + size_t prefix_len = strlen(prefix); + if (str_len < prefix_len) { return false; } - return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length()); + return str_ncmp_ci(str, prefix, prefix_len); } // Extract a parameter value from a header line // Handles both quoted and unquoted values -std::string extract_header_param(const std::string &header, const std::string ¶m) { +// Assigns to out if found, clears out otherwise +void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out) { + size_t param_len = strlen(param); size_t search_pos = 0; - while (search_pos < header.length()) { + while (search_pos < header_len) { // Look for param name - const char *found = stristr(header.c_str() + search_pos, param.c_str()); + const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param); if (!found) { - return ""; + out.clear(); + return; } - size_t pos = found - header.c_str(); + size_t pos = found - header; // Check if this is a word boundary (not part of another parameter) if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') { @@ -134,14 +139,14 @@ std::string extract_header_param(const std::string &header, const std::string &p } // Move past param name - pos += param.length(); + pos += param_len; // Skip whitespace and find '=' - while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { + while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } - if (pos >= header.length() || header[pos] != '=') { + if (pos >= header_len || header[pos] != '=') { search_pos = pos; continue; } @@ -149,36 +154,39 @@ std::string extract_header_param(const std::string &header, const std::string &p pos++; // Skip '=' // Skip whitespace after '=' - while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { + while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } - if (pos >= header.length()) { - return ""; + if (pos >= header_len) { + out.clear(); + return; } // Check if value is quoted if (header[pos] == '"') { pos++; - size_t end = header.find('"', pos); - if (end != std::string::npos) { - return header.substr(pos, end - pos); + const char *end = static_cast(memchr(header + pos, '"', header_len - pos)); + if (end) { + out.assign(header + pos, end - (header + pos)); + return; } // Malformed - no closing quote - return ""; + out.clear(); + return; } // Unquoted value - find the end (semicolon, comma, or end of string) size_t end = pos; - while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' && - header[end] != '\t') { + while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') { end++; } - return header.substr(pos, end - pos); + out.assign(header + pos, end - pos); + return; } - return ""; + out.clear(); } // Parse boundary from Content-Type header @@ -189,13 +197,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st return false; } + size_t content_type_len = strlen(content_type); + // Check for multipart/form-data (case-insensitive) - if (!stristr(content_type, "multipart/form-data")) { + if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) { return false; } // Look for boundary parameter - const char *b = stristr(content_type, "boundary="); + const char *b = strcasestr_n(content_type, content_type_len, "boundary="); if (!b) { return false; } @@ -238,14 +248,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st return true; } -// Trim whitespace from both ends of a string -std::string str_trim(const std::string &str) { - size_t start = str.find_first_not_of(" \t\r\n"); - if (start == std::string::npos) { - return ""; - } - size_t end = str.find_last_not_of(" \t\r\n"); - return str.substr(start, end - start + 1); +// Trim whitespace from both ends, assign result to out +void str_trim(const char *str, size_t len, std::string &out) { + const char *start = str; + const char *end = str + len; + while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n')) + start++; + while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n')) + end--; + out.assign(start, end - start); } } // namespace esphome::web_server_idf diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index 9008be64598..cb1e0ecd1db 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -66,19 +66,20 @@ class MultipartReader { // ========== Utility Functions ========== // Case-insensitive string prefix check -bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix); +bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix); // Extract a parameter value from a header line // Handles both quoted and unquoted values -std::string extract_header_param(const std::string &header, const std::string ¶m); +// Assigns to out if found, clears out otherwise +void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out); // Parse boundary from Content-Type header // Returns true if boundary found, false otherwise // boundary_start and boundary_len will point to the boundary value bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len); -// Trim whitespace from both ends of a string -std::string str_trim(const std::string &str); +// Trim whitespace from both ends, assign result to out +void str_trim(const char *str, size_t len, std::string &out); } // namespace esphome::web_server_idf #endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index a1a2793b4ae..c58ca2a24ff 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,17 +1,13 @@ #ifdef USE_ESP32 -#include #include #include #include "esphome/core/helpers.h" -#include "esphome/core/log.h" #include "http_parser.h" #include "utils.h" namespace esphome::web_server_idf { -static const char *const TAG = "web_server_idf_utils"; - size_t url_decode(char *str) { char *start = str; char *ptr = str, buf; @@ -54,32 +50,15 @@ optional request_get_header(httpd_req_t *req, const char *name) { return {str}; } -optional request_get_url_query(httpd_req_t *req) { - auto len = httpd_req_get_url_query_len(req); - if (len == 0) { - return {}; - } - - std::string str; - str.resize(len); - - auto res = httpd_req_get_url_query_str(req, &str[0], len + 1); - if (res != ESP_OK) { - ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); - return {}; - } - - return {str}; -} - optional query_key_value(const char *query_url, size_t query_len, const char *key) { if (query_url == nullptr || query_len == 0) { return {}; } - // Use stack buffer for typical query strings, heap fallback for large ones - SmallBufferWithHeapFallback<256, char> val(query_len); - + // Value can't exceed query_len. Use small stack buffer for typical values, + // heap fallback for long ones (e.g. base64 IR data) to limit stack usage + // since callers may also have stack buffers for the query string. + SmallBufferWithHeapFallback<128, char> val(query_len); if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } @@ -88,6 +67,18 @@ optional query_key_value(const char *query_url, size_t query_len, c return {val.get()}; } +bool query_has_key(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { + return false; + } + // Minimal buffer — we only care if the key exists, not the value + char buf[1]; + // httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found + // but value truncated (expected with 1-byte buffer), or other errors for invalid input + auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf)); + return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC; +} + // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { for (size_t i = 0; i < n; i++) { @@ -98,8 +89,8 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { return true; } -// Case-insensitive string search (like strstr but case-insensitive) -const char *stristr(const char *haystack, const char *needle) { +// Bounded case-insensitive string search (like strcasestr but length-bounded) +const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) { if (!haystack) { return nullptr; } @@ -109,7 +100,12 @@ const char *stristr(const char *haystack, const char *needle) { return haystack; } - for (const char *p = haystack; *p; p++) { + if (haystack_len < needle_len) { + return nullptr; + } + + const char *end = haystack + haystack_len - needle_len + 1; + for (const char *p = haystack; p < end; p++) { if (str_ncmp_ci(p, needle, needle_len)) { return p; } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index bb0610dac00..027a2f7b6cb 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -13,11 +13,8 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); -optional request_get_url_query(httpd_req_t *req); optional query_key_value(const char *query_url, size_t query_len, const char *key); -inline optional query_key_value(const std::string &query_url, const std::string &key) { - return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); -} +bool query_has_key(const char *query_url, size_t query_len, const char *key); // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } @@ -25,8 +22,8 @@ inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b) // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n); -// Case-insensitive string search (like strstr but case-insensitive) -const char *stristr(const char *haystack, const char *needle); +// Bounded case-insensitive string search (like strcasestr but length-bounded) +const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle); } // namespace esphome::web_server_idf #endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ac7f99bef7c..1798159e7f7 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -171,10 +171,11 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { const char *content_type_char = content_type.value().c_str(); // Check most common case first - if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) { + size_t content_type_len = strlen(content_type_char); + if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) { // Normal form data - proceed with regular handling #ifdef USE_WEBSERVER_OTA - } else if (stristr(content_type_char, "multipart/form-data") != nullptr) { + } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) { auto *server = static_cast(r->user_ctx); return server->handle_multipart_upload_(r, content_type_char); #endif @@ -344,14 +345,34 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw memcpy(user_info + user_len + 1, password, pass_len); user_info[user_info_len] = '\0'; - size_t n = 0, out; - esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast(user_info), user_info_len); - - auto digest = std::unique_ptr(new char[n + 1]); - esp_crypto_base64_encode(reinterpret_cast(digest.get()), n, &out, + // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes + // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety + constexpr size_t max_digest_len = 350; + char digest[max_digest_len]; + size_t out; + esp_crypto_base64_encode(reinterpret_cast(digest), max_digest_len, &out, reinterpret_cast(user_info), user_info_len); - return strcmp(digest.get(), auth_str + auth_prefix_len) == 0; + // Constant-time comparison to avoid timing side channels. + // No early return on length mismatch — the length difference is folded + // into the accumulator so any mismatch is rejected. + const char *provided = auth_str + auth_prefix_len; + size_t digest_len = out; // length from esp_crypto_base64_encode + // Derive provided_len from the already-sized std::string rather than + // rescanning with strlen (avoids attacker-controlled scan length). + size_t provided_len = auth.value().size() - auth_prefix_len; + // Use full-width XOR so any bit difference in the lengths is preserved + // (uint8_t truncation would miss differences in higher bytes, e.g. + // digest_len vs digest_len + 256). + volatile size_t result = digest_len ^ provided_len; + // Iterate over the expected digest length only — the full-width length + // XOR above already rejects any length mismatch, and bounding the loop + // prevents a long Authorization header from forcing extra work. + for (size_t i = 0; i < digest_len; i++) { + char provided_ch = (i < provided_len) ? provided[i] : 0; + result |= static_cast(digest[i] ^ provided_ch); + } + return result == 0; } void AsyncWebServerRequest::requestAuthentication(const char *realm) const { @@ -372,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { } // Look up value from query strings - optional val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); - if (!val.has_value()) { - auto url_query = request_get_url_query(*this); - if (url_query.has_value()) { - val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); - } - } + auto val = this->find_query_value_(name); // Don't cache misses to avoid wasting memory when handlers check for // optional parameters that don't exist in the request @@ -391,6 +406,50 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { return param; } +/// Search post_query then URL query with a callback. +/// Returns first truthy result, or value-initialized default. +/// URL query is accessed directly from req->uri (same pattern as url_to()). +template +static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) + -> decltype(func(nullptr, size_t{0}, name)) { + if (!post_query.empty()) { + auto result = func(post_query.c_str(), post_query.size(), name); + if (result) { + return result; + } + } + // Use httpd API for query length, then access string directly from URI. + // http_parser identifies components by offset/length without modifying the URI string. + // This is the same pattern used by url_to(). + auto len = httpd_req_get_url_query_len(req); + if (len == 0) { + return {}; + } + const char *query = strchr(req->uri, '?'); + if (query == nullptr) { + return {}; + } + query++; // skip '?' + return func(query, len, name); +} + +optional AsyncWebServerRequest::find_query_value_(const char *name) const { + return search_query_sources(this->req_, this->post_query_, name, + [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); }); +} + +bool AsyncWebServerRequest::hasArg(const char *name) { + return search_query_sources(this->req_, this->post_query_, name, query_has_key); +} + +std::string AsyncWebServerRequest::arg(const char *name) { + auto val = this->find_query_value_(name); + if (val.has_value()) { + return std::move(val.value()); + } + return {}; +} + void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } @@ -861,8 +920,8 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } }); - // Process data - std::unique_ptr buffer(new char[MULTIPART_CHUNK_SIZE]); + // Use heap buffer - 1460 bytes is too large for the httpd task stack + auto buffer = std::make_unique(MULTIPART_CHUNK_SIZE); size_t bytes_since_yield = 0; for (size_t remaining = r->content_len; remaining > 0;) { diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 17605449632..74601ffda88 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -116,7 +116,8 @@ class AsyncWebServerRequest { /// Write URL (without query string) to buffer, returns StringRef pointing to buffer. /// URL is decoded (e.g., %20 -> space). StringRef url_to(std::span buffer) const; - /// Get URL as std::string. Prefer url_to() to avoid heap allocation. + // Remove before 2026.9.0 + ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0") std::string url() const { char buffer[URL_BUF_SIZE]; return std::string(this->url_to(buffer)); @@ -170,14 +171,8 @@ class AsyncWebServerRequest { AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const char *name) { - auto *param = this->getParam(name); - if (param) { - return param->value(); - } - return {}; - } + bool hasArg(const char *name); + std::string arg(const char *name); std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } @@ -192,6 +187,7 @@ class AsyncWebServerRequest { // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid // duplicate storage. Only successful lookups are cached to prevent cache pollution when // handlers check for optional parameters that don't exist. + optional find_query_value_(const char *name) const; std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} @@ -259,9 +255,9 @@ using message_generator_t = std::string(esphome::web_server::WebServer *, void * /* This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for - the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a - std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per - entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing + the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a + std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers + per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing because of dedup) would take up only 0.8 kB. */ struct DeferredEvent { @@ -277,7 +273,9 @@ struct DeferredEvent { bool operator==(const DeferredEvent &test) const { return (source_ == test.source_ && message_generator_ == test.message_generator_); } -} __attribute__((packed)); +}; +static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *), + "DeferredEvent should have no padding"); class AsyncEventSourceResponse { friend class AsyncEventSource; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 6863e6fb62d..e865de86634 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -288,11 +288,6 @@ def _validate(config): config = config.copy() config[CONF_NETWORKS] = [] - if config.get(CONF_FAST_CONNECT, False): - networks = config.get(CONF_NETWORKS, []) - if not networks: - raise cv.Invalid("At least one network required for fast_connect!") - if CONF_USE_ADDRESS not in config: use_address = CORE.name + config[CONF_DOMAIN] if CONF_MANUAL_IP in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e350f990af9..a2efac8d262 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -20,6 +20,7 @@ #endif #include +#include #include #include "lwip/dns.h" #include "lwip/err.h" @@ -47,6 +48,69 @@ namespace esphome::wifi { static const char *const TAG = "wifi"; +// CompactString implementation +CompactString::CompactString(const char *str, size_t len) { + if (len > MAX_LENGTH) { + len = MAX_LENGTH; // Clamp to max valid length + } + + this->length_ = len; + if (len <= INLINE_CAPACITY) { + // Store inline with null terminator + this->is_heap_ = 0; + if (len > 0) { + std::memcpy(this->storage_, str, len); + } + this->storage_[len] = '\0'; + } else { + // Heap allocate with null terminator + this->is_heap_ = 1; + char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory) + std::memcpy(heap_data, str, len); + heap_data[len] = '\0'; + this->set_heap_ptr_(heap_data); + } +} + +CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {} + +CompactString &CompactString::operator=(const CompactString &other) { + if (this != &other) { + this->~CompactString(); + new (this) CompactString(other); + } + return *this; +} + +CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) { + // Copy full storage (includes null terminator for inline, or pointer for heap) + std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1); + other.length_ = 0; + other.is_heap_ = 0; + other.storage_[0] = '\0'; +} + +CompactString &CompactString::operator=(CompactString &&other) noexcept { + if (this != &other) { + this->~CompactString(); + new (this) CompactString(std::move(other)); + } + return *this; +} + +CompactString::~CompactString() { + if (this->is_heap_) { + delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory) + } +} + +bool CompactString::operator==(const CompactString &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0; +} +bool CompactString::operator==(const StringRef &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0; +} + /// WiFi Retry Logic - Priority-Based BSSID Selection /// /// The WiFi component uses a state machine with priority degradation to handle connection failures @@ -349,18 +413,18 @@ bool WiFiComponent::needs_scan_results_() const { return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); } -bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { +bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const { // Check if this SSID is configured as hidden // If explicitly marked hidden, we should always try hidden mode regardless of scan results for (const auto &conf : this->sta_) { - if (conf.get_ssid() == ssid && conf.get_hidden()) { + if (conf.ssid_ == ssid && conf.get_hidden()) { return false; // Treat as not seen - force hidden mode attempt } } // Otherwise, check if we saw it in scan results for (const auto &scan : this->scan_result_) { - if (scan.get_ssid() == ssid) { + if (scan.ssid_ == ssid) { return true; } } @@ -409,20 +473,33 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t continue; } // For BSSID-only configs (empty SSID), match by BSSID - if (sta.get_ssid().empty()) { + if (sta.ssid_.empty()) { if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) { return true; } continue; } // Match by SSID - if (sta.get_ssid() == ssid) { + if (sta.ssid_ == ssid) { return true; } } return false; } +void __attribute__((flatten)) WiFiComponent::set_sta_priority(bssid_t bssid, int8_t priority) { + for (auto &it : this->sta_priorities_) { + if (it.bssid == bssid) { + it.priority = priority; + return; + } + } + this->sta_priorities_.push_back(WiFiSTAPriority{ + .bssid = bssid, + .priority = priority, + }); +} + void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE // Skip logging during roaming scans to avoid log buffer overflow @@ -465,18 +542,18 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { if (!include_explicit_hidden && sta.get_hidden()) { int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); if (first_non_hidden_idx < 0 || static_cast(i) < first_non_hidden_idx) { - ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str()); continue; } } // In BLIND_RETRY mode, treat all networks as candidates // In SCAN_BASED mode, only retry networks that weren't seen in the scan - if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { - ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); + if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.ssid_)) { + ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast(i)); return static_cast(i); } - ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str()); } // No hidden SSIDs found return -1; @@ -593,11 +670,11 @@ void WiFiComponent::start() { // Fast connect optimization: only use when we have saved BSSID+channel data // Without saved data, try first configured network or use normal flow if (loaded_fast_connect) { - ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); + ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str()); this->start_connecting(params); } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { // No saved data, but have configured networks - try first non-hidden network - ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); + ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str()); this->selected_sta_index_ = 0; params = this->build_params_for_current_phase_(); this->start_connecting(params); @@ -827,7 +904,7 @@ void WiFiComponent::setup_ap_config_() { if (this->ap_setup_) return; - if (this->ap_.get_ssid().empty()) { + if (this->ap_.ssid_.empty()) { // Build AP SSID from app name without heap allocation // WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7 static constexpr size_t AP_SSID_MAX_LEN = 32; @@ -863,7 +940,7 @@ void WiFiComponent::setup_ap_config_() { " AP SSID: '%s'\n" " AP Password: '%s'\n" " IP Address: %s", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); + this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); #ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); @@ -960,9 +1037,12 @@ WiFiAP WiFiComponent::get_sta() const { return config ? *config : WiFiAP{}; } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { + this->save_wifi_sta(ssid.c_str(), password.c_str()); +} +void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination - strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 - strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 + strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 + strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); @@ -996,14 +1076,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGI(TAG, "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", - ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, + ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:\n" " SSID: '%s'", - ap.get_ssid().c_str()); + ap.ssid_.c_str()); if (ap.has_bssid()) { ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { @@ -1036,7 +1116,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { client_key_present ? "present" : "not present"); } else { #endif - ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str()); #ifdef USE_WIFI_WPA2_EAP } #endif @@ -1411,7 +1491,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN && config && !config->get_hidden() && this->scan_result_.empty()) { - ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str()); + ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str()); } // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; @@ -1825,11 +1905,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } // Get SSID for logging (use pointer to avoid copy) - const std::string *ssid = nullptr; + const char *ssid = nullptr; if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { - ssid = &this->scan_result_[0].get_ssid(); + ssid = this->scan_result_[0].ssid_.c_str(); } else if (const WiFiAP *config = this->get_selected_sta_()) { - ssid = &config->get_ssid(); + ssid = config->ssid_.c_str(); } // Only decrease priority on the last attempt for this phase @@ -1849,8 +1929,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } char bssid_s[18]; format_mac_addr_upper(failed_bssid.value().data(), bssid_s); - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", - ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "", + bssid_s, old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start @@ -2098,10 +2178,14 @@ void WiFiComponent::save_fast_connect_settings_() { } #endif -void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } +void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } +void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); } void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; } void WiFiAP::clear_bssid() { this->bssid_ = {}; } -void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +void WiFiAP::set_password(const std::string &password) { + this->password_ = CompactString(password.c_str(), password.size()); +} +void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); } #ifdef USE_WIFI_WPA2_EAP void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif @@ -2111,10 +2195,8 @@ void WiFiAP::clear_channel() { this->channel_ = 0; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } #endif void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } -const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; } bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } -const std::string &WiFiAP::get_password() const { return this->password_; } #ifdef USE_WIFI_WPA2_EAP const optional &WiFiAP::get_eap() const { return this->eap_; } #endif @@ -2125,12 +2207,12 @@ const optional &WiFiAP::get_manual_ip() const { return this->manual_ip #endif bool WiFiAP::get_hidden() const { return this->hidden_; } -WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, - bool is_hidden) +WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, + bool with_auth, bool is_hidden) : bssid_(bssid), channel_(channel), rssi_(rssi), - ssid_(std::move(ssid)), + ssid_(ssid, ssid_len), with_auth_(with_auth), is_hidden_(is_hidden) {} bool WiFiScanResult::matches(const WiFiAP &config) const { @@ -2139,9 +2221,9 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { // don't match SSID if (!this->is_hidden_) return false; - } else if (!config.get_ssid().empty()) { + } else if (!config.ssid_.empty()) { // check if SSID matches - if (config.get_ssid() != this->ssid_) + if (this->ssid_ != config.ssid_) return false; } else { // network is configured without SSID - match other settings @@ -2152,15 +2234,15 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { #ifdef USE_WIFI_WPA2_EAP // BSSID requires auth but no PSK or EAP credentials given - if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) + if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value())) return false; // BSSID does not require auth, but PSK or EAP credentials given - if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value())) + if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value())) return false; #else // If PSK given, only match for networks with auth (and vice versa) - if (config.get_password().empty() == this->with_auth_) + if (config.password_.empty() == this->with_auth_) return false; #endif @@ -2173,7 +2255,6 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { bool WiFiScanResult::get_matches() const { return this->matches_; } void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; } const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; } -const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; } uint8_t WiFiScanResult::get_channel() const { return this->channel_; } int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } @@ -2284,7 +2365,7 @@ void WiFiComponent::process_roaming_scan_() { for (const auto &result : this->scan_result_) { // Must be same SSID, different BSSID - if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid) + if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid) continue; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 58f19c184a5..4a038f602c6 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -172,12 +172,67 @@ template using wifi_scan_vector_t = std::vector; template using wifi_scan_vector_t = FixedVector; #endif +/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated. +/// Used internally for WiFi SSID/password storage to reduce heap fragmentation. +class CompactString { + public: + static constexpr uint8_t MAX_LENGTH = 127; + static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes + + CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; } + CompactString(const char *str, size_t len); + CompactString(const CompactString &other); + CompactString(CompactString &&other) noexcept; + CompactString &operator=(const CompactString &other); + CompactString &operator=(CompactString &&other) noexcept; + ~CompactString(); + + const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; } + const char *c_str() const { return this->data(); } // Always null-terminated + size_t size() const { return this->length_; } + bool empty() const { return this->length_ == 0; } + + /// Return a StringRef view of this string (zero-copy) + StringRef ref() const { return StringRef(this->data(), this->size()); } + + bool operator==(const CompactString &other) const; + bool operator!=(const CompactString &other) const { return !(*this == other); } + bool operator==(const StringRef &other) const; + bool operator!=(const StringRef &other) const { return !(*this == other); } + bool operator==(const char *other) const { return *this == StringRef(other); } + bool operator!=(const char *other) const { return !(*this == other); } + + protected: + char *get_heap_ptr_() const { + char *ptr; + std::memcpy(&ptr, this->storage_, sizeof(ptr)); + return ptr; + } + void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); } + + // Storage for string data. When is_heap_=0, contains the string directly (null-terminated). + // When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation. + char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator + uint8_t length_ : 7; // String length (0-127) + uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage + // Total size: 20 bytes (19 bytes storage + 1 byte bitfields) +}; + +static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); + class WiFiAP { + friend class WiFiComponent; + friend class WiFiScanResult; + public: void set_ssid(const std::string &ssid); + void set_ssid(const char *ssid); + void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } void set_bssid(const bssid_t &bssid); void clear_bssid(); void set_password(const std::string &password); + void set_password(const char *password); + void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); } #ifdef USE_WIFI_WPA2_EAP void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP @@ -188,10 +243,10 @@ class WiFiAP { void set_manual_ip(optional manual_ip); #endif void set_hidden(bool hidden); - const std::string &get_ssid() const; + StringRef get_ssid() const { return this->ssid_.ref(); } + StringRef get_password() const { return this->password_.ref(); } const bssid_t &get_bssid() const; bool has_bssid() const; - const std::string &get_password() const; #ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP @@ -204,8 +259,8 @@ class WiFiAP { bool get_hidden() const; protected: - std::string ssid_; - std::string password_; + CompactString ssid_; + CompactString password_; #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP @@ -220,15 +275,18 @@ class WiFiAP { }; class WiFiScanResult { + friend class WiFiComponent; + public: - WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden); + WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, + bool is_hidden); bool matches(const WiFiAP &config) const; bool get_matches() const; void set_matches(bool matches); const bssid_t &get_bssid() const; - const std::string &get_ssid() const; + StringRef get_ssid() const { return this->ssid_.ref(); } uint8_t get_channel() const; int8_t get_rssi() const; bool get_with_auth() const; @@ -242,7 +300,7 @@ class WiFiScanResult { bssid_t bssid_; uint8_t channel_; int8_t rssi_; - std::string ssid_; + CompactString ssid_; int8_t priority_{0}; bool matches_{false}; bool with_auth_; @@ -381,6 +439,8 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + void save_wifi_sta(const char *ssid, const char *password); + void save_wifi_sta(StringRef ssid, StringRef password) { this->save_wifi_sta(ssid.c_str(), password.c_str()); } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -428,20 +488,11 @@ class WiFiComponent : public Component { } return 0; } - void set_sta_priority(const bssid_t bssid, int8_t priority) { - for (auto &it : this->sta_priorities_) { - if (it.bssid == bssid) { - it.priority = priority; - return; - } - } - this->sta_priorities_.push_back(WiFiSTAPriority{ - .bssid = bssid, - .priority = priority, - }); - } + void set_sta_priority(bssid_t bssid, int8_t priority); network::IPAddresses wifi_sta_ip_addresses(); + // Remove before 2026.9.0 + ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") std::string wifi_ssid(); /// Write SSID to buffer without heap allocation. /// Returns pointer to buffer, or empty string if not connected. @@ -545,7 +596,7 @@ class WiFiComponent : public Component { int8_t find_first_non_hidden_index_() const; /// Check if an SSID was seen in the most recent scan results /// Used to skip hidden mode for SSIDs we know are visible - bool ssid_was_seen_in_scan_(const std::string &ssid) const; + bool ssid_was_seen_in_scan_(const CompactString &ssid) const; /// Check if full scan results are needed (captive portal active, improv, listeners) bool needs_full_scan_results_() const; /// Check if network matches any configured network (for scan result filtering) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 6488de8dae3..cbf7d7d80f8 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -216,23 +216,16 @@ bool WiFiComponent::wifi_apply_hostname_() { ESP_LOGV(TAG, "Set hostname failed"); } - // inform dhcp server of hostname change using dhcp_renew() + // Update hostname on all lwIP interfaces so DHCP packets include it. + // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically + // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is + // fixed at compile time and never changes at runtime. for (netif *intf = netif_list; intf; intf = intf->next) { - // unconditionally update all known interfaces #if LWIP_VERSION_MAJOR == 1 intf->hostname = (char *) wifi_station_get_hostname(); #else intf->hostname = wifi_station_get_hostname(); #endif - if (netif_dhcp_data(intf) != nullptr) { - // renew already started DHCP leases - err_t lwipret = dhcp_renew(intf); - if (lwipret != ERR_OK) { - ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname, - (int) lwipret, intf->name[0], intf->name[1], intf->num); - ret = false; - } - } } return ret; @@ -247,16 +240,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ssid)) { ESP_LOGE(TAG, "SSID too long"); return false; } - if (ap.get_password().size() > sizeof(conf.password)) { + if (ap.password_.size() > sizeof(conf.password)) { ESP_LOGE(TAG, "Password too long"); return false; } - memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + memcpy(reinterpret_cast(conf.password), ap.password_.c_str(), ap.password_.size()); if (ap.has_bssid()) { conf.bssid_set = 1; @@ -266,7 +259,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.threshold.authmode = AUTH_OPEN; } else { // Set threshold based on configured minimum auth mode @@ -738,8 +731,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { const char *ssid_cstr = reinterpret_cast(it->ssid); if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) { this->scan_result_.emplace_back( - bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, - std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); + bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr, + it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); } else { this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel); } @@ -832,27 +825,27 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; struct softap_config conf {}; - if (ap.get_ssid().size() > sizeof(conf.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ssid)) { ESP_LOGE(TAG, "AP SSID too long"); return false; } - memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - conf.ssid_len = static_cast(ap.get_ssid().size()); + memcpy(reinterpret_cast(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + conf.ssid_len = static_cast(ap.ssid_.size()); conf.channel = ap.has_channel() ? ap.get_channel() : 1; conf.ssid_hidden = ap.get_hidden(); conf.max_connection = 5; conf.beacon_interval = 100; - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.authmode = AUTH_OPEN; *conf.password = 0; } else { conf.authmode = AUTH_WPA2_PSK; - if (ap.get_password().size() > sizeof(conf.password)) { + if (ap.password_.size() > sizeof(conf.password)) { ESP_LOGE(TAG, "AP password too long"); return false; } - memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.password), ap.password_.c_str(), ap.password_.size()); } ETS_UART_INTR_DISABLE(); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index d74d083954c..52ee4821215 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -300,19 +300,19 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { + if (ap.ssid_.size() > sizeof(conf.sta.ssid)) { ESP_LOGE(TAG, "SSID too long"); return false; } - if (ap.get_password().size() > sizeof(conf.sta.password)) { + if (ap.password_.size() > sizeof(conf.sta.password)) { ESP_LOGE(TAG, "Password too long"); return false; } - memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + memcpy(reinterpret_cast(conf.sta.password), ap.password_.c_str(), ap.password_.size()); // The weakest authmode to accept in the fast scan mode - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.sta.threshold.authmode = WIFI_AUTH_OPEN; } else { // Set threshold based on configured minimum auth mode @@ -864,8 +864,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { bssid_t bssid; std::copy(record.bssid, record.bssid + 6, bssid.begin()); - std::string ssid(ssid_cstr); - this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi, + this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); } else { this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); @@ -1055,26 +1054,26 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ap.ssid)) { ESP_LOGE(TAG, "AP SSID too long"); return false; } - memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); + memcpy(reinterpret_cast(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size()); conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1; - conf.ap.ssid_hidden = ap.get_ssid().size(); + conf.ap.ssid_hidden = ap.get_hidden(); conf.ap.max_connection = 5; conf.ap.beacon_interval = 100; - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.ap.authmode = WIFI_AUTH_OPEN; *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - if (ap.get_password().size() > sizeof(conf.ap.password)) { + if (ap.password_.size() > sizeof(conf.ap.password)) { ESP_LOGE(TAG, "AP password too long"); return false; } - memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.ap.password), ap.password_.c_str(), ap.password_.size()); } // pairwise cipher of SoftAP, group cipher will be derived using this. diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 5fd9d7663b1..2cc05928afd 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -193,7 +193,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; String ssid = WiFi.SSID(); - if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) { + if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) { WiFi.disconnect(); } @@ -213,7 +213,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { s_sta_state = LTWiFiSTAState::CONNECTING; s_ignored_disconnect_count = 0; - WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), + WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), ap.get_channel(), // 0 = auto ap.has_bssid() ? ap.get_bssid().data() : NULL); if (status != WL_CONNECTED) { @@ -688,7 +688,7 @@ void WiFiComponent::wifi_scan_done_callback_() { auto &ap = scan->ap[i]; this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3], ap.bssid.addr[4], ap.bssid.addr[5]}, - std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, + ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); } else { auto &ap = scan->ap[i]; @@ -735,7 +735,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { yield(); - return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), + return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden()); } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 818ad1059cc..1baf21e2b2c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -78,7 +78,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; #endif - auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); + auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str()); if (ret != WL_CONNECTED) return false; @@ -149,9 +149,8 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re bssid_t bssid; std::copy(result->bssid, result->bssid + 6, bssid.begin()); - std::string ssid(ssid_cstr); - WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, - ssid_cstr[0] == '\0'); + WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi, + result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0'); if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { this->scan_result_.push_back(res); } @@ -204,7 +203,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } #endif - WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1); + WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1); return true; } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index a63b30b892e..b5ebfd73904 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t end) break; diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index 65639de61b1..c103363b4a4 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -61,11 +61,19 @@ void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) { break; } + auto before = millis(); auto err = zigbee_default_signal_handler(bufid); if (err != RET_OK) { ESP_LOGE(TAG, "Zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err)); } + if (sig == ZB_COMMON_SIGNAL_CAN_SLEEP) { + this->sleep_remainder_ += millis() - before; + uint32_t seconds = this->sleep_remainder_ / 1000; + this->sleep_remainder_ -= seconds * 1000; + this->sleep_time_ += seconds; + } + switch (sig) { case ZB_BDB_SIGNAL_STEERING: ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status); @@ -213,6 +221,7 @@ void ZigbeeComponent::dump_config() { "Zigbee\n" " Wipe on boot: %s\n" " Device is joined to the network: %s\n" + " Sleep time: %us\n" " Current channel: %d\n" " Current page: %d\n" " Sleep threshold: %ums\n" @@ -221,9 +230,9 @@ void ZigbeeComponent::dump_config() { " Short addr: 0x%04X\n" " Long pan id: 0x%s\n" " Short pan id: 0x%04X", - get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(), - zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf, - zb_get_pan_id()); + get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(), + zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), + extended_pan_id_buf, zb_get_pan_id()); dump_reporting_(); } diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index bd4b092ad56..dcc2b40a166 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -92,6 +92,8 @@ class ZigbeeComponent : public Component { CallbackManager join_cb_; Trigger<> join_trigger_; bool force_report_{false}; + uint32_t sleep_time_{}; + uint32_t sleep_remainder_{}; }; class ZigbeeEntity { diff --git a/esphome/const.py b/esphome/const.py index 4bf47b8f835..f72cbc88933 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0-dev" +__version__ = "2026.3.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -639,6 +639,7 @@ CONF_MOVEMENT_COUNTER = "movement_counter" CONF_MOVING_DISTANCE = "moving_distance" CONF_MQTT = "mqtt" CONF_MQTT_ID = "mqtt_id" +CONF_MQTT_JSON_STATE_PAYLOAD = "mqtt_json_state_payload" CONF_MULTIPLE = "multiple" CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLY = "multiply" diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0daabc02828..449acc64cf4 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -204,36 +204,40 @@ void Application::loop() { this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { - if (this->dump_config_at_ == 0) { - char build_time_str[Application::BUILD_TIME_STR_SIZE]; - this->get_build_time_string(build_time_str); - ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str); + this->process_dump_config_(); + } +} + +void Application::process_dump_config_() { + if (this->dump_config_at_ == 0) { + char build_time_str[Application::BUILD_TIME_STR_SIZE]; + this->get_build_time_string(build_time_str); + ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str); #ifdef ESPHOME_PROJECT_NAME - ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); + ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); #endif #ifdef USE_ESP32 - esp_chip_info_t chip_info; - esp_chip_info(&chip_info); - ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, - chip_info.revision % 100, chip_info.cores); + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, + chip_info.revision % 100, chip_info.cores); #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) - // Suggest optimization for chips that don't need the PSRAM cache workaround - if (chip_info.revision >= 300) { + // Suggest optimization for chips that don't need the PSRAM cache workaround + if (chip_info.revision >= 300) { #ifdef USE_PSRAM - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, - chip_info.revision % 100); + ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, + chip_info.revision % 100); #else - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, - chip_info.revision % 100); -#endif - } -#endif + ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, + chip_info.revision % 100); #endif } - - this->components_[this->dump_config_at_]->call_dump_config(); - this->dump_config_at_++; +#endif +#endif } + + this->components_[this->dump_config_at_]->call_dump_config(); + this->dump_config_at_++; } void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { @@ -605,15 +609,6 @@ void Application::unregister_socket_fd(int fd) { } } -bool Application::is_socket_ready(int fd) const { - // This function is thread-safe for reading the result of select() - // However, it should only be called after select() has been executed in the main loop - // The read_fds_ is only modified by select() in the main loop - if (fd < 0 || fd >= FD_SETSIZE) - return false; - - return FD_ISSET(fd, &this->read_fds_); -} #endif void Application::yield_with_select_(uint32_t delay_ms) { diff --git a/esphome/core/application.h b/esphome/core/application.h index 592bf809f1d..30611227a28 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -101,6 +101,10 @@ #include "esphome/components/update/update_entity.h" #endif +namespace esphome::socket { +class Socket; +} // namespace esphome::socket + namespace esphome { // Teardown timeout constant (in milliseconds) @@ -491,7 +495,8 @@ class Application { void unregister_socket_fd(int fd); /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run - bool is_socket_ready(int fd) const; + /// The read_fds_ is only modified by select() in the main loop + bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); } #ifdef USE_WAKE_LOOP_THREADSAFE /// Wake the main event loop from a FreeRTOS task @@ -503,6 +508,15 @@ class Application { protected: friend Component; + friend class socket::Socket; + +#ifdef USE_SOCKET_SELECT_SUPPORT + /// Fast path for Socket::ready() via friendship - skips negative fd check. + /// Safe because: fd was validated in register_socket_fd() at registration time, + /// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded). + /// FD_ISSET may include its own upper bounds check depending on platform. + bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } +#endif void register_component_(Component *comp); @@ -519,6 +533,11 @@ class Application { void before_loop_tasks_(uint32_t loop_start_time); void after_loop_tasks_(); + /// Process dump_config output one component per loop iteration. + /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. + /// Caller must ensure dump_config_at_ < components_.size(). + void __attribute__((noinline)) process_dump_config_(); + void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index e13d81a8e4f..6c03b74a17c 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -26,6 +26,7 @@ class ComponentIterator { public: void begin(bool include_internal = false); void advance(); + bool completed() const { return this->state_ == IteratorState::NONE; } virtual bool on_begin(); #ifdef USE_BINARY_SENSOR virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 80f4d228ec1..7018a66b730 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -14,6 +14,7 @@ #define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" #define ESPHOME_DEBUG_SCHEDULER +#define ESPHOME_DEBUG_API // Default threading model for static analysis (ESP32 is multi-threaded with atomics) #define ESPHOME_THREAD_MULTI_ATOMICS @@ -146,10 +147,11 @@ #define USE_MD5 #define USE_SHA256 #define USE_MQTT +#define USE_MQTT_COVER_JSON #define USE_NETWORK -#define USE_ONLINE_IMAGE_BMP_SUPPORT -#define USE_ONLINE_IMAGE_PNG_SUPPORT -#define USE_ONLINE_IMAGE_JPEG_SUPPORT +#define USE_RUNTIME_IMAGE_BMP +#define USE_RUNTIME_IMAGE_PNG +#define USE_RUNTIME_IMAGE_JPEG #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_LISTENER @@ -238,9 +240,15 @@ #define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 6) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7) #define USE_ETHERNET +#define USE_ETHERNET_LAN8720 +#define USE_ETHERNET_RTL8201 +#define USE_ETHERNET_DP83848 +#define USE_ETHERNET_IP101 +#define USE_ETHERNET_JL1101 #define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_LAN8670 #define USE_ETHERNET_MANUAL_IP #define USE_ETHERNET_IP_STATE_LISTENERS #define USE_ETHERNET_CONNECT_TRIGGER @@ -321,6 +329,7 @@ #endif #ifdef USE_NRF52 +#define USE_ESPHOME_TASK_LOG_BUFFER #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 811b856b5e4..f6a7ec1dfdd 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -152,11 +152,13 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m this->unit_of_measurement_ = unit_of_measurement; } +#ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_icon_ref().empty()) { ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); } } +#endif void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) { if (!obj.get_device_class_ref().empty()) { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 86cb75495b2..cbc07cc44c0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -231,8 +231,13 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) }; /// Log entity icon if set (for use in dump_config) +#ifdef USE_ENTITY_ICON #define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj) void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj); +#else +#define LOG_ENTITY_ICON(tag, prefix, obj) ((void) 0) +inline void log_entity_icon(const char *, const char *, const EntityBase &) {} +#endif /// Log entity device class if set (for use in dump_config) #define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj) void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f7de34b6d5a..298b93fbc46 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1083,6 +1083,9 @@ template std::string format_hex(const std::array &dat * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator. * Optionally includes the total byte count in parentheses at the end. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Pointer to the byte array to format. * @param length Number of bytes in the array. * @param separator Character to use between hex bytes (default: '.'). @@ -1108,6 +1111,9 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator * * Similar to the byte array version, but formats 16-bit words as 4-digit hex values. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Pointer to the 16-bit word array to format. * @param length Number of 16-bit words in the array. * @param separator Character to use between hex words (default: '.'). @@ -1131,6 +1137,9 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato * Convenience overload for std::vector. Formats each byte as a two-digit * uppercase hex value with customizable separator. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Vector of bytes to format. * @param separator Character to use between hex bytes (default: '.'). * @param show_length Whether to append the byte count in parentheses (default: true). @@ -1154,6 +1163,9 @@ std::string format_hex_pretty(const std::vector &data, char separator = * Convenience overload for std::vector. Each 16-bit word is formatted * as a 4-digit uppercase hex value in big-endian order. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Vector of 16-bit words to format. * @param separator Character to use between hex words (default: '.'). * @param show_length Whether to append the word count in parentheses (default: true). @@ -1176,6 +1188,9 @@ std::string format_hex_pretty(const std::vector &data, char separator * Treats each character in the string as a byte and formats it in hex. * Useful for debugging binary data stored in std::string containers. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data String whose bytes should be formatted as hex. * @param separator Character to use between hex bytes (default: '.'). * @param show_length Whether to append the byte count in parentheses (default: true). @@ -1198,6 +1213,9 @@ std::string format_hex_pretty(const std::string &data, char separator = '.', boo * Converts the integer to big-endian byte order and formats each byte as hex. * The most significant byte appears first in the output string. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.). * @param val The unsigned integer value to format. * @param separator Character to use between hex bytes (default: '.'). @@ -1655,13 +1673,10 @@ template class RAMAllocator { ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. }; - RAMAllocator() = default; - RAMAllocator(uint8_t flags) { - // default is both external and internal - flags &= ALLOC_INTERNAL | ALLOC_EXTERNAL; - if (flags != 0) - this->flags_ = flags; - } + constexpr RAMAllocator() = default; + constexpr RAMAllocator(uint8_t flags) + : flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) + : (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {} template constexpr RAMAllocator(const RAMAllocator &other) : flags_{other.flags_} {} T *allocate(size_t n) { return this->allocate(n, sizeof(T)); } diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 97ac28b623c..4194c3aa9ef 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -107,6 +107,24 @@ static void validate_static_string(const char *name) { // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to // avoid the main thread modifying the list while it is being accessed. +// Calculate random offset for interval timers +// Extracted from set_timer_common_ to reduce code size - float math + random_float() +// only needed for intervals, not timeouts +uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { + return static_cast(std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); +} + +// Check if a retry was already cancelled in items_ or to_add_ +// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated +// Remove before 2026.8.0 along with all retry code +bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id) { + return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true); +} + // Common implementation for both timeout and interval // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, @@ -130,84 +148,66 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Create and populate the scheduler item auto item = this->get_item_from_pool_locked_(); item->component = component; - switch (name_type) { - case NameType::STATIC_STRING: - item->set_static_name(static_name); - break; - case NameType::HASHED_STRING: - item->set_hashed_name(hash_or_id); - break; - case NameType::NUMERIC_ID: - item->set_numeric_id(hash_or_id); - break; - case NameType::NUMERIC_ID_INTERNAL: - item->set_internal_id(hash_or_id); - break; - } + item->set_name(name_type, static_name, hash_or_id); item->type = type; item->callback = std::move(func); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item.get(), false); item->is_retry = is_retry; + // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. + // Using a pointer lets both paths share the cancel + push_back epilogue. + auto *target = &this->to_add_; + #ifndef ESPHOME_THREAD_SINGLE // Special handling for defer() (delay = 0, type = TIMEOUT) // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution - if (!skip_cancel) { - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); - } - this->defer_queue_.push_back(std::move(item)); - return; - } + target = &this->defer_queue_; + } else #endif /* not ESPHOME_THREAD_SINGLE */ - - // Type-specific setup - if (type == SchedulerItem::INTERVAL) { - item->interval = delay; - // first execution happens immediately after a random smallish offset - // Calculate random offset (0 to min(interval/2, 5s)) - uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); - item->set_next_execution(now + offset); + { + // Type-specific setup + if (type == SchedulerItem::INTERVAL) { + item->interval = delay; + // first execution happens immediately after a random smallish offset + uint32_t offset = this->calculate_interval_offset_(delay); + item->set_next_execution(now + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE - SchedulerNameLog name_log; - ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", - name_log.format(name_type, static_name, hash_or_id), delay, offset); + SchedulerNameLog name_log; + ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", + name_log.format(name_type, static_name, hash_or_id), delay, offset); #endif - } else { - item->interval = 0; - item->set_next_execution(now + delay); - } + } else { + item->interval = 0; + item->set_next_execution(now + delay); + } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ - // For retries, check if there's a cancelled timeout first - // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name - if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true) || - has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true))) { - // Skip scheduling - the retry was cancelled + // For retries, check if there's a cancelled timeout first + // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name + if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && + type == SchedulerItem::TIMEOUT && + this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { + // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER - SchedulerNameLog skip_name_log; - ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", - skip_name_log.format(name_type, static_name, hash_or_id)); + SchedulerNameLog skip_name_log; + ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", + skip_name_log.format(name_type, static_name, hash_or_id)); #endif - return; + return; + } } - // If name is provided, do atomic cancel-and-add (unless skip_cancel is true) - // Cancel existing items + // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) if (!skip_cancel) { this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(std::move(item)); + target->push_back(std::move(item)); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ede729d1641..394178a831c 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,28 +219,15 @@ class Scheduler { // Helper to get the name type NameType get_name_type() const { return name_type_; } - // Helper to set a static string name (no allocation) - void set_static_name(const char *name) { - name_.static_name = name; - name_type_ = NameType::STATIC_STRING; - } - - // Helper to set a hashed string name (hash computed from std::string) - void set_hashed_name(uint32_t hash) { - name_.hash_or_id = hash; - name_type_ = NameType::HASHED_STRING; - } - - // Helper to set a numeric ID name - void set_numeric_id(uint32_t id) { - name_.hash_or_id = id; - name_type_ = NameType::NUMERIC_ID; - } - - // Helper to set an internal numeric ID (separate namespace from NUMERIC_ID) - void set_internal_id(uint32_t id) { - name_.hash_or_id = id; - name_type_ = NameType::NUMERIC_ID_INTERNAL; + // Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id. + // Both union members occupy the same offset, so only one store is needed. + void set_name(NameType type, const char *static_name, uint32_t hash_or_id) { + if (type == NameType::STATIC_STRING) { + name_.static_name = static_name; + } else { + name_.hash_or_id = hash_or_id; + } + name_type_ = type; } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); @@ -355,6 +342,17 @@ class Scheduler { // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); + // Helper to calculate random offset for interval timers - extracted to reduce code size of set_timer_common_ + // IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash. + uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay); + + // Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_ + // Remove before 2026.8.0 along with all retry code. + // IMPORTANT: Must not be inlined - retry path is cold and deprecated. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + bool __attribute__((noinline)) + is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); + #ifdef ESPHOME_DEBUG_SCHEDULER // Helper for debug logging in set_timer_common_ - extracted to reduce code size void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id, diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index e66f9a2c978..d42f89d0290 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -133,6 +133,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int: ) # Suppress Python syntax warnings from third-party scripts during compilation os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") + # Increase uv retry count to handle transient network errors (default is 3) + os.environ.setdefault("UV_HTTP_RETRIES", "10") cmd = ["platformio"] + list(args) if not CORE.verbose: diff --git a/platformio.ini b/platformio.ini index 94b1f1a727e..09b3d8722de 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component @@ -169,7 +169,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz @@ -214,7 +214,7 @@ build_unflags = ; This are common settings for the LibreTiny (all variants) using Arduino. [common:libretiny-arduino] extends = common:arduino -platform = https://github.com/libretiny-eu/libretiny.git#v1.11.0 +platform = https://github.com/libretiny-eu/libretiny.git#v1.12.1 framework = arduino lib_compat_mode = soft lib_deps = diff --git a/pyproject.toml b/pyproject.toml index 339bc65eed0..c6a2c22a5d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ classifiers = [ "Topic :: Home Automation", ] -# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 -requires-python = ">=3.11.0,<3.14" +# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76 +requires-python = ">=3.11.0,<3.15" dynamic = ["dependencies", "optional-dependencies", "version"] diff --git a/requirements_test.txt b/requirements_test.txt index 2cf6f6456e6..9e99855f6fe 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.0 # also change in .pre-commit-config.yaml when updating +ruff==0.15.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 diff --git a/script/build_language_schema.py b/script/build_language_schema.py index c9501cb193f..bea540dc632 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -369,7 +369,7 @@ def get_logger_tags(): "api.service", ] for file in CORE_COMPONENTS_PATH.rglob("*.cpp"): - data = file.read_text() + data = file.read_text(encoding="utf-8") match = pattern.search(data) if match: tags.append(match.group(1)) diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08c..5d46419399e 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -27,9 +27,9 @@ sensor: name: Linearly combined temperatures sources: - source: template_temperature1 - coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" - source: template_temperature2 - coeffecient: 1.5 + coefficient: 1.5 - platform: combination type: max name: Max of combined temperatures diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 333ab567cdd..9593d0f6f07 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -57,6 +57,23 @@ display: allow_other_uses: true number: GPIO4 + - platform: epaper_spi + spi_id: spi_bus + model: waveshare-7.5in-H + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + inverted: true + - platform: epaper_spi model: seeed-reterminal-e1002 - platform: epaper_spi @@ -64,3 +81,66 @@ display: # Override pins to avoid conflict with other display configs busy_pin: 43 dc_pin: 42 + + # WeAct 2.13" 3-color e-paper (122x250, SSD1680) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.13in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 2.9" 3-color e-paper (128x296, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.9in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 4.2" 3-color e-paper (400x300, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-4.2in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0)); diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index d67787b3d5b..bc054f5aeea 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -6,8 +6,8 @@ esp32: type: esp-idf components: - espressif/mdns^1.8.2 - - name: espressif/esp_hosted - ref: 2.7.0 + - name: espressif/button + ref: 4.1.5 advanced: enable_idf_experimental_features: yes disable_debug_stubs: true diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml index 70b485daac2..821a1362507 100644 --- a/tests/components/logger/test.nrf52-adafruit.yaml +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -5,3 +5,4 @@ esphome: logger: level: DEBUG + task_log_buffer_size: 0 diff --git a/tests/components/mipi_spi/common.yaml b/tests/components/mipi_spi/common.yaml index 692a9f436e4..a867b726edd 100644 --- a/tests/components/mipi_spi/common.yaml +++ b/tests/components/mipi_spi/common.yaml @@ -3,9 +3,15 @@ display: spi_16: true pixel_mode: 18bit model: ili9488 - dc_pin: ${dc_pin} - cs_pin: ${cs_pin} - reset_pin: ${reset_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + cs_pin: + allow_other_uses: true + number: ${cs_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} data_rate: 20MHz invert_colors: true show_test_card: true @@ -24,3 +30,15 @@ display: height: 200 enable_pin: ${enable_pin} bus_mode: single + + - platform: mipi_spi + model: WAVESHARE-1.83-V2 + dc_pin: + allow_other_uses: true + number: ${dc_pin} + cs_pin: + allow_other_uses: true + number: ${cs_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 4cf26925934..8c58e9b0806 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -219,6 +219,7 @@ cover: name: Template Cover state_topic: some/topic/cover qos: 2 + mqtt_json_state_payload: true lambda: |- if (id(some_binary_sensor).state) { return COVER_OPEN; @@ -231,6 +232,53 @@ cover: stop_action: - logger.log: stop_action optimistic: true + - platform: template + name: Template Cover with Position and Tilt + state_topic: some/topic/cover_pt + position_state_topic: some/topic/cover_pt/position + position_command_topic: some/topic/cover_pt/position/set + tilt_state_topic: some/topic/cover_pt/tilt + tilt_command_topic: some/topic/cover_pt/tilt/set + qos: 2 + has_position: true + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + position_action: + - logger.log: position_action + tilt_action: + - logger.log: tilt_action + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + - platform: template + name: Template Cover with Position and Tilt JSON + state_topic: some/topic/cover_pt_json + qos: 2 + mqtt_json_state_payload: true + has_position: true + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + position_action: + - logger.log: position_action + tilt_action: + - logger.log: tilt_action + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true datetime: - platform: template diff --git a/tests/components/remote_receiver/test.esp32-c2-idf.yaml b/tests/components/remote_receiver/test.esp32-c2-idf.yaml new file mode 100644 index 00000000000..87154e19fc8 --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO2 + dump: all + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/test.esp32-c2-idf.yaml b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml new file mode 100644 index 00000000000..424cd8d2498 --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO2 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 50e8d4122bf..36df1bc83ec 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -197,6 +197,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s " platformio_options:\n" " build_flags:\n" ' - "-DDEBUG" # Enable assert() statements\n' + ' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n' ' - "-g" # Add debug symbols', ) diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py index 09348f5beaa..0b07710961e 100644 --- a/tests/integration/test_alarm_control_panel_state_transitions.py +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -270,6 +270,14 @@ async def test_alarm_control_panel_state_transitions( # The chime_sensor has chime: true, so opening it while disarmed # should trigger on_chime callback + # Set up future for the on_ready from opening the chime sensor + # (alarm becomes "not ready" when chime sensor opens). + # We must wait for this BEFORE creating the close future, otherwise + # the open event's log can arrive late and resolve the close future, + # causing the test to proceed before the chime close is processed. + ready_after_chime_open: asyncio.Future[bool] = loop.create_future() + ready_futures.append(ready_after_chime_open) + # We're currently DISARMED - open the chime sensor client.switch_command(chime_switch_info.key, True) @@ -279,11 +287,18 @@ async def test_alarm_control_panel_state_transitions( except TimeoutError: pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}") - # Close the chime sensor and wait for alarm to become ready again - # We need to wait for this transition before testing door sensor, - # otherwise there's a race where the door sensor state change could - # arrive before the chime sensor state change, leaving the alarm in - # a continuous "not ready" state with no on_ready callback fired. + # Wait for the on_ready from the chime sensor opening + try: + await asyncio.wait_for(ready_after_chime_open, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when chime sensor opened. " + f"Log lines: {log_lines[-20:]}" + ) + + # Now create the future for the close event and close the sensor. + # Since we waited for the open event above, the close event's + # on_ready log cannot be confused with the open event's. ready_after_chime_close: asyncio.Future[bool] = loop.create_future() ready_futures.append(ready_after_chime_close)