mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'guard_trq_allocation' into integration
This commit is contained in:
		| @@ -2310,11 +2310,13 @@ message ZWaveProxyFrame { | |||||||
| enum ZWaveProxyRequestType { | enum ZWaveProxyRequestType { | ||||||
|   ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; |   ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; | ||||||
|   ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; |   ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; | ||||||
|  |   ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2; | ||||||
| } | } | ||||||
| message ZWaveProxyRequest { | message ZWaveProxyRequest { | ||||||
|   option (id) = 129; |   option (id) = 129; | ||||||
|   option (source) = SOURCE_CLIENT; |   option (source) = SOURCE_BOTH; | ||||||
|   option (ifdef) = "USE_ZWAVE_PROXY"; |   option (ifdef) = "USE_ZWAVE_PROXY"; | ||||||
|  |  | ||||||
|   ZWaveProxyRequestType type = 1; |   ZWaveProxyRequestType type = 1; | ||||||
|  |   bytes data = 2 [(pointer_to_buffer) = true]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3112,6 +3112,27 @@ bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | |||||||
|   } |   } | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  | bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 2: { | ||||||
|  |       // Use raw data directly to avoid allocation | ||||||
|  |       this->data = value.data(); | ||||||
|  |       this->data_len = value.size(); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  | void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   buffer.encode_uint32(1, static_cast<uint32_t>(this->type)); | ||||||
|  |   buffer.encode_bytes(2, this->data, this->data_len); | ||||||
|  | } | ||||||
|  | void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { | ||||||
|  |   size.add_uint32(1, static_cast<uint32_t>(this->type)); | ||||||
|  |   size.add_length(2, this->data_len); | ||||||
|  | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| }  // namespace esphome::api | }  // namespace esphome::api | ||||||
|   | |||||||
| @@ -280,6 +280,7 @@ enum UpdateCommand : uint32_t { | |||||||
| enum ZWaveProxyRequestType : uint32_t { | enum ZWaveProxyRequestType : uint32_t { | ||||||
|   ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, |   ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, | ||||||
|   ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, |   ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, | ||||||
|  |   ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, | ||||||
| }; | }; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -2971,16 +2972,21 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { | |||||||
| class ZWaveProxyRequest final : public ProtoDecodableMessage { | class ZWaveProxyRequest final : public ProtoDecodableMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 129; |   static constexpr uint8_t MESSAGE_TYPE = 129; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 2; |   static constexpr uint8_t ESTIMATED_SIZE = 21; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "z_wave_proxy_request"; } |   const char *message_name() const override { return "z_wave_proxy_request"; } | ||||||
| #endif | #endif | ||||||
|   enums::ZWaveProxyRequestType type{}; |   enums::ZWaveProxyRequestType type{}; | ||||||
|  |   const uint8_t *data{nullptr}; | ||||||
|  |   uint16_t data_len{0}; | ||||||
|  |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|  |   void calculate_size(ProtoSize &size) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -662,6 +662,8 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums: | |||||||
|       return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; |       return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; | ||||||
|     case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: |     case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: | ||||||
|       return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; |       return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; | ||||||
|  |     case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE: | ||||||
|  |       return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"; | ||||||
|     default: |     default: | ||||||
|       return "UNKNOWN"; |       return "UNKNOWN"; | ||||||
|   } |   } | ||||||
| @@ -2161,6 +2163,9 @@ void ZWaveProxyFrame::dump_to(std::string &out) const { | |||||||
| void ZWaveProxyRequest::dump_to(std::string &out) const { | void ZWaveProxyRequest::dump_to(std::string &out) const { | ||||||
|   MessageDumpHelper helper(out, "ZWaveProxyRequest"); |   MessageDumpHelper helper(out, "ZWaveProxyRequest"); | ||||||
|   dump_field(out, "type", static_cast<enums::ZWaveProxyRequestType>(this->type)); |   dump_field(out, "type", static_cast<enums::ZWaveProxyRequestType>(this->type)); | ||||||
|  |   out.append("  data: "); | ||||||
|  |   out.append(format_hex_pretty(this->data, this->data_len)); | ||||||
|  |   out.append("\n"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -356,6 +356,15 @@ void APIServer::on_update(update::UpdateEntity *obj) { | |||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ZWAVE_PROXY | ||||||
|  | void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { | ||||||
|  |   // We could add code to manage a second subscription type, but, since this message type is | ||||||
|  |   //  very infrequent and small, we simply send it to all clients | ||||||
|  |   for (auto &c : this->clients_) | ||||||
|  |     c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
| #ifdef USE_ALARM_CONTROL_PANEL | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
| API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) | API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -126,6 +126,9 @@ class APIServer : public Component, public Controller { | |||||||
| #ifdef USE_UPDATE | #ifdef USE_UPDATE | ||||||
|   void on_update(update::UpdateEntity *obj) override; |   void on_update(update::UpdateEntity *obj) override; | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ZWAVE_PROXY | ||||||
|  |   void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   bool is_connected() const; |   bool is_connected() const; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -276,9 +276,6 @@ def get_spi_interface(index): | |||||||
|         return ["&SPI", "&SPI1"][index] |         return ["&SPI", "&SPI1"][index] | ||||||
|     if index == 0: |     if index == 0: | ||||||
|         return "&SPI" |         return "&SPI" | ||||||
|     # Following code can't apply to C2, H2 or 8266 since they have only one SPI |  | ||||||
|     if get_target_variant() in (VARIANT_ESP32S3, VARIANT_ESP32S2): |  | ||||||
|         return "new SPIClass(FSPI)" |  | ||||||
|     return "new SPIClass(HSPI)" |     return "new SPIClass(HSPI)" | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -217,7 +217,7 @@ void SX126x::configure() { | |||||||
|     this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4); |     this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4); | ||||||
|  |  | ||||||
|     // set packet params and sync word |     // set packet params and sync word | ||||||
|     this->set_packet_params_(this->payload_length_); |     this->set_packet_params_(this->get_max_packet_size()); | ||||||
|     if (this->sync_value_.size() == 2) { |     if (this->sync_value_.size() == 2) { | ||||||
|       this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); |       this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); | ||||||
|     } |     } | ||||||
| @@ -236,7 +236,7 @@ void SX126x::configure() { | |||||||
|     this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8); |     this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8); | ||||||
|  |  | ||||||
|     // set packet params and sync word |     // set packet params and sync word | ||||||
|     this->set_packet_params_(this->payload_length_); |     this->set_packet_params_(this->get_max_packet_size()); | ||||||
|     if (!this->sync_value_.empty()) { |     if (!this->sync_value_.empty()) { | ||||||
|       this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); |       this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); | ||||||
|     } |     } | ||||||
| @@ -274,7 +274,7 @@ void SX126x::set_packet_params_(uint8_t payload_length) { | |||||||
|     buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00; |     buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00; | ||||||
|     buf[3] = this->sync_value_.size() * 8; |     buf[3] = this->sync_value_.size() * 8; | ||||||
|     buf[4] = 0x00; |     buf[4] = 0x00; | ||||||
|     buf[5] = 0x00; |     buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01; | ||||||
|     buf[6] = payload_length; |     buf[6] = payload_length; | ||||||
|     buf[7] = this->crc_enable_ ? 0x06 : 0x01; |     buf[7] = this->crc_enable_ ? 0x06 : 0x01; | ||||||
|     buf[8] = 0x00; |     buf[8] = 0x00; | ||||||
| @@ -314,6 +314,9 @@ SX126xError SX126x::transmit_packet(const std::vector<uint8_t> &packet) { | |||||||
|   buf[0] = 0xFF; |   buf[0] = 0xFF; | ||||||
|   buf[1] = 0xFF; |   buf[1] = 0xFF; | ||||||
|   this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); |   this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); | ||||||
|  |   if (this->payload_length_ == 0) { | ||||||
|  |     this->set_packet_params_(this->get_max_packet_size()); | ||||||
|  |   } | ||||||
|   if (this->rx_start_) { |   if (this->rx_start_) { | ||||||
|     this->set_mode_rx(); |     this->set_mode_rx(); | ||||||
|   } else { |   } else { | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components.esp32 import ( | from esphome.components.esp32 import ( | ||||||
|  |     VARIANT_ESP32P4, | ||||||
|     VARIANT_ESP32S2, |     VARIANT_ESP32S2, | ||||||
|     VARIANT_ESP32S3, |     VARIANT_ESP32S3, | ||||||
|     add_idf_sdkconfig_option, |     add_idf_sdkconfig_option, | ||||||
| @@ -47,7 +48,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         } |         } | ||||||
|     ), |     ), | ||||||
|     cv.only_with_esp_idf, |     cv.only_with_esp_idf, | ||||||
|     only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3]), |     only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4]), | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| // Should not be needed, but it's required to pass CI clang-tidy checks | // Should not be needed, but it's required to pass CI clang-tidy checks | ||||||
| #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include <vector> | #include <vector> | ||||||
| #include "usb/usb_host.h" | #include "usb/usb_host.h" | ||||||
| @@ -9,11 +9,31 @@ | |||||||
| #include <freertos/task.h> | #include <freertos/task.h> | ||||||
| #include "esphome/core/lock_free_queue.h" | #include "esphome/core/lock_free_queue.h" | ||||||
| #include "esphome/core/event_pool.h" | #include "esphome/core/event_pool.h" | ||||||
| #include <list> | #include <atomic> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace usb_host { | namespace usb_host { | ||||||
|  |  | ||||||
|  | // THREADING MODEL: | ||||||
|  | // This component uses a dedicated USB task for event processing to prevent data loss. | ||||||
|  | // - USB Task (high priority): Handles USB events, executes transfer callbacks | ||||||
|  | // - Main Loop Task: Initiates transfers, processes completion events | ||||||
|  | // | ||||||
|  | // Thread-safe communication: | ||||||
|  | // - Lock-free queues for USB task -> main loop events (SPSC pattern) | ||||||
|  | // - Lock-free TransferRequest pool using atomic bitmask (MCSP pattern) | ||||||
|  | // | ||||||
|  | // TransferRequest pool access pattern: | ||||||
|  | // - get_trq_() [allocate]: Called from BOTH USB task and main loop threads | ||||||
|  | //   * USB task: via USB UART input callbacks that restart transfers immediately | ||||||
|  | //   * Main loop: for output transfers and flow-controlled input restarts | ||||||
|  | // - release_trq() [deallocate]: Called from main loop thread only | ||||||
|  | // | ||||||
|  | // The multi-threaded allocation is intentional for performance: | ||||||
|  | // - USB task can immediately restart input transfers without context switching | ||||||
|  | // - Main loop controls backpressure by deciding when to restart after consuming data | ||||||
|  | // The atomic bitmask ensures thread-safe allocation without mutex blocking. | ||||||
|  |  | ||||||
| static const char *const TAG = "usb_host"; | static const char *const TAG = "usb_host"; | ||||||
|  |  | ||||||
| // Forward declarations | // Forward declarations | ||||||
| @@ -98,13 +118,7 @@ class USBClient : public Component { | |||||||
|   friend class USBHost; |   friend class USBHost; | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
|   USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid) { init_pool(); } |   USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} | ||||||
|  |  | ||||||
|   void init_pool() { |  | ||||||
|     this->trq_pool_.clear(); |  | ||||||
|     for (size_t i = 0; i != MAX_REQUESTS; i++) |  | ||||||
|       this->trq_pool_.push_back(&this->requests_[i]); |  | ||||||
|   } |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
|   // setup must happen after the host bus has been setup |   // setup must happen after the host bus has been setup | ||||||
| @@ -126,10 +140,13 @@ class USBClient : public Component { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool register_(); |   bool register_(); | ||||||
|   TransferRequest *get_trq_(); |   TransferRequest *get_trq_();  // Lock-free allocation using atomic bitmask (multi-consumer safe) | ||||||
|   virtual void disconnect(); |   virtual void disconnect(); | ||||||
|   virtual void on_connected() {} |   virtual void on_connected() {} | ||||||
|   virtual void on_disconnected() { this->init_pool(); } |   virtual void on_disconnected() { | ||||||
|  |     // Reset all requests to available (all bits to 0) | ||||||
|  |     this->trq_in_use_.store(0); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // USB task management |   // USB task management | ||||||
|   static void usb_task_fn(void *arg); |   static void usb_task_fn(void *arg); | ||||||
| @@ -143,7 +160,11 @@ class USBClient : public Component { | |||||||
|   int state_{USB_CLIENT_INIT}; |   int state_{USB_CLIENT_INIT}; | ||||||
|   uint16_t vid_{}; |   uint16_t vid_{}; | ||||||
|   uint16_t pid_{}; |   uint16_t pid_{}; | ||||||
|   std::list<TransferRequest *> trq_pool_{}; |   // Lock-free pool management using atomic bitmask (no dynamic allocation) | ||||||
|  |   // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available | ||||||
|  |   // Supports multiple concurrent consumers (both threads can allocate) | ||||||
|  |   // Single producer for deallocation (main loop only) | ||||||
|  |   std::atomic<uint16_t> trq_in_use_; | ||||||
|   TransferRequest requests_[MAX_REQUESTS]{}; |   TransferRequest requests_[MAX_REQUESTS]{}; | ||||||
| }; | }; | ||||||
| class USBHost : public Component { | class USBHost : public Component { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| // Should not be needed, but it's required to pass CI clang-tidy checks | // Should not be needed, but it's required to pass CI clang-tidy checks | ||||||
| #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) | ||||||
| #include "usb_host.h" | #include "usb_host.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| @@ -7,6 +7,7 @@ | |||||||
|  |  | ||||||
| #include <cinttypes> | #include <cinttypes> | ||||||
| #include <cstring> | #include <cstring> | ||||||
|  | #include <atomic> | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace usb_host { | namespace usb_host { | ||||||
|  |  | ||||||
| @@ -185,9 +186,11 @@ void USBClient::setup() { | |||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   for (auto *trq : this->trq_pool_) { |   // Pre-allocate USB transfer buffers for all slots at startup | ||||||
|     usb_host_transfer_alloc(64, 0, &trq->transfer); |   // This avoids any dynamic allocation during runtime | ||||||
|     trq->client = this; |   for (size_t i = 0; i < MAX_REQUESTS; i++) { | ||||||
|  |     usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); | ||||||
|  |     this->requests_[i].client = this;  // Set once, never changes | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Create and start USB task |   // Create and start USB task | ||||||
| @@ -347,17 +350,39 @@ static void control_callback(const usb_transfer_t *xfer) { | |||||||
|   queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE); |   queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer) | ||||||
|  | // - USB task: USB UART input callbacks restart transfers for immediate data reception | ||||||
|  | // - Main loop: Output transfers and flow-controlled input restarts after consuming data | ||||||
|  | // | ||||||
|  | // THREAD SAFETY: Lock-free using atomic compare-and-swap on bitmask | ||||||
|  | // This multi-threaded access is intentional for performance - USB task can | ||||||
|  | // immediately restart transfers without waiting for main loop scheduling. | ||||||
| TransferRequest *USBClient::get_trq_() { | TransferRequest *USBClient::get_trq_() { | ||||||
|   if (this->trq_pool_.empty()) { |   uint16_t mask = this->trq_in_use_.load(std::memory_order_relaxed); | ||||||
|     ESP_LOGE(TAG, "Too many requests queued"); |  | ||||||
|     return nullptr; |   // Find first available slot (bit = 0) and try to claim it atomically | ||||||
|  |   for (size_t i = 0; i < MAX_REQUESTS; i++) { | ||||||
|  |     if (!(mask & (1U << i))) { | ||||||
|  |       // Slot i appears available, try to claim it atomically | ||||||
|  |       uint16_t expected = mask; | ||||||
|  |       uint16_t desired = mask | (1U << i);  // Set bit i to mark as in-use | ||||||
|  |  | ||||||
|  |       if (this->trq_in_use_.compare_exchange_weak(expected, desired, std::memory_order_acquire, | ||||||
|  |                                                   std::memory_order_relaxed)) { | ||||||
|  |         // Successfully claimed slot i - prepare the TransferRequest | ||||||
|  |         auto *trq = &this->requests_[i]; | ||||||
|  |         trq->transfer->context = trq; | ||||||
|  |         trq->transfer->device_handle = this->device_handle_; | ||||||
|  |         return trq; | ||||||
|  |       } | ||||||
|  |       // Another thread claimed this slot, retry with updated mask | ||||||
|  |       mask = expected; | ||||||
|  |       i--;  // Retry the same index with new mask value | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|   auto *trq = this->trq_pool_.front(); |  | ||||||
|   this->trq_pool_.pop_front(); |   ESP_LOGE(TAG, "Too many requests queued (all %d slots in use)", MAX_REQUESTS); | ||||||
|   trq->client = this; |   return nullptr; | ||||||
|   trq->transfer->context = trq; |  | ||||||
|   trq->transfer->device_handle = this->device_handle_; |  | ||||||
|   return trq; |  | ||||||
| } | } | ||||||
| void USBClient::disconnect() { | void USBClient::disconnect() { | ||||||
|   this->on_disconnected(); |   this->on_disconnected(); | ||||||
| @@ -370,6 +395,8 @@ void USBClient::disconnect() { | |||||||
|   this->device_addr_ = -1; |   this->device_addr_ = -1; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // THREAD CONTEXT: Called from main loop thread only | ||||||
|  | // - Used for device configuration and control operations | ||||||
| bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, | bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, | ||||||
|                                  const transfer_cb_t &callback, const std::vector<uint8_t> &data) { |                                  const transfer_cb_t &callback, const std::vector<uint8_t> &data) { | ||||||
|   auto *trq = this->get_trq_(); |   auto *trq = this->get_trq_(); | ||||||
| @@ -425,6 +452,9 @@ static void transfer_callback(usb_transfer_t *xfer) { | |||||||
| } | } | ||||||
| /** | /** | ||||||
|  * Performs a transfer input operation. |  * Performs a transfer input operation. | ||||||
|  |  * THREAD CONTEXT: Called from both USB task and main loop threads! | ||||||
|  |  * - USB task: USB UART input callbacks call start_input() which calls this | ||||||
|  |  * - Main loop: Initial setup and other components | ||||||
|  * |  * | ||||||
|  * @param ep_address The endpoint address. |  * @param ep_address The endpoint address. | ||||||
|  * @param callback The callback function to be called when the transfer is complete. |  * @param callback The callback function to be called when the transfer is complete. | ||||||
| @@ -451,6 +481,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u | |||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Performs an output transfer operation. |  * Performs an output transfer operation. | ||||||
|  |  * THREAD CONTEXT: Called from main loop thread only | ||||||
|  |  * - USB UART output uses defer() to ensure main loop context | ||||||
|  |  * - Modbus and other components call from loop() | ||||||
|  * |  * | ||||||
|  * @param ep_address The endpoint address. |  * @param ep_address The endpoint address. | ||||||
|  * @param callback The callback function to be called when the transfer is complete. |  * @param callback The callback function to be called when the transfer is complete. | ||||||
| @@ -483,7 +516,28 @@ void USBClient::dump_config() { | |||||||
|                 "  Product id %04X", |                 "  Product id %04X", | ||||||
|                 this->vid_, this->pid_); |                 this->vid_, this->pid_); | ||||||
| } | } | ||||||
| void USBClient::release_trq(TransferRequest *trq) { this->trq_pool_.push_back(trq); } | // THREAD CONTEXT: Only called from main loop thread (single producer for deallocation) | ||||||
|  | // - Via event processing when handling EVENT_TRANSFER_COMPLETE/EVENT_CONTROL_COMPLETE | ||||||
|  | // - Directly when transfer submission fails | ||||||
|  | // | ||||||
|  | // THREAD SAFETY: Lock-free using atomic AND to clear bit | ||||||
|  | // Single-producer pattern makes this simpler than allocation | ||||||
|  | void USBClient::release_trq(TransferRequest *trq) { | ||||||
|  |   if (trq == nullptr) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   // Calculate index from pointer arithmetic | ||||||
|  |   size_t index = trq - this->requests_; | ||||||
|  |   if (index >= MAX_REQUESTS) { | ||||||
|  |     ESP_LOGE(TAG, "Invalid TransferRequest pointer"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Atomically clear bit i to mark slot as available | ||||||
|  |   // fetch_and with inverted bitmask clears the bit atomically | ||||||
|  |   uint16_t bit = 1U << index; | ||||||
|  |   this->trq_in_use_.fetch_and(~bit, std::memory_order_release); | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace usb_host | }  // namespace usb_host | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| // Should not be needed, but it's required to pass CI clang-tidy checks | // Should not be needed, but it's required to pass CI clang-tidy checks | ||||||
| #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) | ||||||
| #include "usb_host.h" | #include "usb_host.h" | ||||||
| #include <cinttypes> | #include <cinttypes> | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|   | |||||||
| @@ -24,7 +24,6 @@ usb_uart_ns = cg.esphome_ns.namespace("usb_uart") | |||||||
| USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component) | USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component) | ||||||
| USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent) | USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent) | ||||||
|  |  | ||||||
|  |  | ||||||
| UARTParityOptions = usb_uart_ns.enum("UARTParityOptions") | UARTParityOptions = usb_uart_ns.enum("UARTParityOptions") | ||||||
| UART_PARITY_OPTIONS = { | UART_PARITY_OPTIONS = { | ||||||
|     "NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE, |     "NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) | ||||||
| #include "usb_uart.h" | #include "usb_uart.h" | ||||||
| #include "usb/usb_host.h" | #include "usb/usb_host.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| @@ -72,6 +72,7 @@ void USBUartTypeCH34X::enable_channels() { | |||||||
|     if (channel->index_ >= 2) |     if (channel->index_ >= 2) | ||||||
|       cmd += 0xE; |       cmd += 0xE; | ||||||
|     this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); |     this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); | ||||||
|  |     this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0, callback); | ||||||
|   } |   } | ||||||
|   USBUartTypeCdcAcm::enable_channels(); |   USBUartTypeCdcAcm::enable_channels(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) | ||||||
| #include "usb_uart.h" | #include "usb_uart.h" | ||||||
| #include "usb/usb_host.h" | #include "usb/usb_host.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| // Should not be needed, but it's required to pass CI clang-tidy checks | // Should not be needed, but it's required to pass CI clang-tidy checks | ||||||
| #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) | ||||||
| #include "usb_uart.h" | #include "usb_uart.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/components/uart/uart_debugger.h" | #include "esphome/components/uart/uart_debugger.h" | ||||||
| @@ -216,9 +216,16 @@ void USBUartComponent::dump_config() { | |||||||
| void USBUartComponent::start_input(USBUartChannel *channel) { | void USBUartComponent::start_input(USBUartChannel *channel) { | ||||||
|   if (!channel->initialised_.load() || channel->input_started_.load()) |   if (!channel->initialised_.load() || channel->input_started_.load()) | ||||||
|     return; |     return; | ||||||
|   // Note: This function is called from both USB task and main loop, so we cannot |   // THREAD CONTEXT: Called from both USB task and main loop threads | ||||||
|   // directly check ring buffer space here. Backpressure is handled by the chunk pool: |   // - USB task: Immediate restart after successful transfer for continuous data flow | ||||||
|   // when exhausted, USB input stops until chunks are freed by the main loop |   // - Main loop: Controlled restart after consuming data (backpressure mechanism) | ||||||
|  |   // | ||||||
|  |   // This dual-thread access is intentional for performance: | ||||||
|  |   // - USB task restarts avoid context switch delays for high-speed data | ||||||
|  |   // - Main loop restarts provide flow control when buffers are full | ||||||
|  |   // | ||||||
|  |   // The underlying transfer_in() uses lock-free atomic allocation from the | ||||||
|  |   // TransferRequest pool, making this multi-threaded access safe | ||||||
|   const auto *ep = channel->cdc_dev_.in_ep; |   const auto *ep = channel->cdc_dev_.in_ep; | ||||||
|   // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback |   // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback | ||||||
|   auto callback = [this, channel](const usb_host::TransferStatus &status) { |   auto callback = [this, channel](const usb_host::TransferStatus &status) { | ||||||
| @@ -297,7 +304,8 @@ static void fix_mps(const usb_ep_desc_t *ep) { | |||||||
|   if (ep != nullptr) { |   if (ep != nullptr) { | ||||||
|     auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep); |     auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep); | ||||||
|     if (ep->wMaxPacketSize > 64) { |     if (ep->wMaxPacketSize > 64) { | ||||||
|       ESP_LOGW(TAG, "Corrected MPS of EP %u from %u to 64", ep->bEndpointAddress, ep->wMaxPacketSize); |       ESP_LOGW(TAG, "Corrected MPS of EP 0x%02X from %u to 64", static_cast<uint8_t>(ep->bEndpointAddress & 0xFF), | ||||||
|  |                ep->wMaxPacketSize); | ||||||
|       ep_mutable->wMaxPacketSize = 64; |       ep_mutable->wMaxPacketSize = 64; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -314,7 +322,7 @@ void USBUartTypeCdcAcm::on_connected() { | |||||||
|   for (auto *channel : this->channels_) { |   for (auto *channel : this->channels_) { | ||||||
|     if (i == cdc_devs.size()) { |     if (i == cdc_devs.size()) { | ||||||
|       ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_); |       ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_); | ||||||
|       this->status_set_warning(LOG_STR("No configuration found for channel")); |       this->status_set_warning("No configuration found for channel"); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     channel->cdc_dev_ = cdc_devs[i++]; |     channel->cdc_dev_ = cdc_devs[i++]; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/components/uart/uart_component.h" | #include "esphome/components/uart/uart_component.h" | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| #include "zwave_proxy.h" | #include "zwave_proxy.h" | ||||||
|  | #include "esphome/components/api/api_server.h" | ||||||
| #include "esphome/core/application.h" | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| @@ -97,12 +98,19 @@ void ZWaveProxy::process_uart_() { | |||||||
|       // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS) |       // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS) | ||||||
|       if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE && |       if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE && | ||||||
|           this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) { |           this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) { | ||||||
|         // Extract the 4-byte Home ID starting at offset 4 |         // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed | ||||||
|         // The frame parser has already validated the checksum and ensured all bytes are present |         // The frame parser has already validated the checksum and ensured all bytes are present | ||||||
|         std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size()); |         if (this->set_home_id(&this->buffer_[4])) { | ||||||
|         this->home_id_ready_ = true; |           api::ZWaveProxyRequest msg; | ||||||
|         ESP_LOGI(TAG, "Home ID: %s", |           msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; | ||||||
|                  format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); |           msg.data = this->home_id_.data(); | ||||||
|  |           msg.data_len = this->home_id_.size(); | ||||||
|  |           if (api::global_api_server != nullptr) { | ||||||
|  |             // We could add code to manage a second subscription type, but, since this message is | ||||||
|  |             //  very infrequent and small, we simply send it to all clients | ||||||
|  |             api::global_api_server->on_zwave_proxy_request(msg); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|       ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); |       ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); | ||||||
|       if (this->api_connection_ != nullptr) { |       if (this->api_connection_ != nullptr) { | ||||||
| @@ -120,7 +128,12 @@ void ZWaveProxy::process_uart_() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); } | void ZWaveProxy::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "Z-Wave Proxy:\n" | ||||||
|  |                 "  Home ID: %s", | ||||||
|  |                 format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); | ||||||
|  | } | ||||||
|  |  | ||||||
| void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { | void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { | ||||||
|   switch (type) { |   switch (type) { | ||||||
| @@ -145,6 +158,17 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool ZWaveProxy::set_home_id(const uint8_t *new_home_id) { | ||||||
|  |   if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) { | ||||||
|  |     ESP_LOGV(TAG, "Home ID unchanged"); | ||||||
|  |     return false;  // No change | ||||||
|  |   } | ||||||
|  |   std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size()); | ||||||
|  |   ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); | ||||||
|  |   this->home_id_ready_ = true; | ||||||
|  |   return true;  // Home ID was changed | ||||||
|  | } | ||||||
|  |  | ||||||
| void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { | void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { | ||||||
|   if (length == 1 && data[0] == this->last_response_) { |   if (length == 1 && data[0] == this->last_response_) { | ||||||
|     ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]); |     ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]); | ||||||
|   | |||||||
| @@ -56,6 +56,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component { | |||||||
|   uint32_t get_home_id() { |   uint32_t get_home_id() { | ||||||
|     return encode_uint32(this->home_id_[0], this->home_id_[1], this->home_id_[2], this->home_id_[3]); |     return encode_uint32(this->home_id_[0], this->home_id_[1], this->home_id_[2], this->home_id_[3]); | ||||||
|   } |   } | ||||||
|  |   bool set_home_id(const uint8_t *new_home_id);  // Store a new home ID. Returns true if it changed. | ||||||
|  |  | ||||||
|   void send_frame(const uint8_t *data, size_t length); |   void send_frame(const uint8_t *data, size_t length); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								tests/components/spi/test.esp32-s3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/components/spi/test.esp32-s3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | spi: | ||||||
|  |   - id: three_spi | ||||||
|  |     interface: spi3 | ||||||
|  |     clk_pin: | ||||||
|  |       number: 47 | ||||||
|  |     mosi_pin: | ||||||
|  |       number: 40 | ||||||
|  |   - id: hw_spi | ||||||
|  |     interface: hardware | ||||||
|  |     clk_pin: | ||||||
|  |       number: 0 | ||||||
|  |     miso_pin: | ||||||
|  |       number: 41 | ||||||
		Reference in New Issue
	
	Block a user