mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00:00 
			
		
		
		
	Merge branch 'guard_trq_allocation' into integration
This commit is contained in:
		| @@ -2310,11 +2310,13 @@ message ZWaveProxyFrame { | ||||
| enum ZWaveProxyRequestType { | ||||
|   ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; | ||||
|   ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; | ||||
|   ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2; | ||||
| } | ||||
| message ZWaveProxyRequest { | ||||
|   option (id) = 129; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (source) = SOURCE_BOTH; | ||||
|   option (ifdef) = "USE_ZWAVE_PROXY"; | ||||
|  | ||||
|   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; | ||||
| } | ||||
| 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 | ||||
|  | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -280,6 +280,7 @@ enum UpdateCommand : uint32_t { | ||||
| enum ZWaveProxyRequestType : uint32_t { | ||||
|   ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, | ||||
|   ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, | ||||
|   ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, | ||||
| }; | ||||
| #endif | ||||
|  | ||||
| @@ -2971,16 +2972,21 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { | ||||
| class ZWaveProxyRequest final : public ProtoDecodableMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "z_wave_proxy_request"; } | ||||
| #endif | ||||
|   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 | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| #endif | ||||
|   | ||||
| @@ -662,6 +662,8 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums: | ||||
|       return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; | ||||
|     case enums::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: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| @@ -2161,6 +2163,9 @@ void ZWaveProxyFrame::dump_to(std::string &out) const { | ||||
| void ZWaveProxyRequest::dump_to(std::string &out) const { | ||||
|   MessageDumpHelper helper(out, "ZWaveProxyRequest"); | ||||
|   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 | ||||
|  | ||||
|   | ||||
| @@ -356,6 +356,15 @@ void APIServer::on_update(update::UpdateEntity *obj) { | ||||
| } | ||||
| #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 | ||||
| API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) | ||||
| #endif | ||||
|   | ||||
| @@ -126,6 +126,9 @@ class APIServer : public Component, public Controller { | ||||
| #ifdef USE_UPDATE | ||||
|   void on_update(update::UpdateEntity *obj) override; | ||||
| #endif | ||||
| #ifdef USE_ZWAVE_PROXY | ||||
|   void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); | ||||
| #endif | ||||
|  | ||||
|   bool is_connected() const; | ||||
|  | ||||
|   | ||||
| @@ -276,9 +276,6 @@ def get_spi_interface(index): | ||||
|         return ["&SPI", "&SPI1"][index] | ||||
|     if index == 0: | ||||
|         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)" | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -217,7 +217,7 @@ void SX126x::configure() { | ||||
|     this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4); | ||||
|  | ||||
|     // 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) { | ||||
|       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); | ||||
|  | ||||
|     // 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()) { | ||||
|       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[3] = this->sync_value_.size() * 8; | ||||
|     buf[4] = 0x00; | ||||
|     buf[5] = 0x00; | ||||
|     buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01; | ||||
|     buf[6] = payload_length; | ||||
|     buf[7] = this->crc_enable_ ? 0x06 : 0x01; | ||||
|     buf[8] = 0x00; | ||||
| @@ -314,6 +314,9 @@ SX126xError SX126x::transmit_packet(const std::vector<uint8_t> &packet) { | ||||
|   buf[0] = 0xFF; | ||||
|   buf[1] = 0xFF; | ||||
|   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_) { | ||||
|     this->set_mode_rx(); | ||||
|   } else { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.esp32 import ( | ||||
|     VARIANT_ESP32P4, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
|     add_idf_sdkconfig_option, | ||||
| @@ -47,7 +48,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|         } | ||||
|     ), | ||||
|     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 | ||||
|  | ||||
| // 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 <vector> | ||||
| #include "usb/usb_host.h" | ||||
| @@ -9,11 +9,31 @@ | ||||
| #include <freertos/task.h> | ||||
| #include "esphome/core/lock_free_queue.h" | ||||
| #include "esphome/core/event_pool.h" | ||||
| #include <list> | ||||
| #include <atomic> | ||||
|  | ||||
| namespace esphome { | ||||
| 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"; | ||||
|  | ||||
| // Forward declarations | ||||
| @@ -98,13 +118,7 @@ class USBClient : public Component { | ||||
|   friend class USBHost; | ||||
|  | ||||
|  public: | ||||
|   USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid) { init_pool(); } | ||||
|  | ||||
|   void init_pool() { | ||||
|     this->trq_pool_.clear(); | ||||
|     for (size_t i = 0; i != MAX_REQUESTS; i++) | ||||
|       this->trq_pool_.push_back(&this->requests_[i]); | ||||
|   } | ||||
|   USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   // setup must happen after the host bus has been setup | ||||
| @@ -126,10 +140,13 @@ class USBClient : public Component { | ||||
|  | ||||
|  protected: | ||||
|   bool register_(); | ||||
|   TransferRequest *get_trq_(); | ||||
|   TransferRequest *get_trq_();  // Lock-free allocation using atomic bitmask (multi-consumer safe) | ||||
|   virtual void disconnect(); | ||||
|   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 | ||||
|   static void usb_task_fn(void *arg); | ||||
| @@ -143,7 +160,11 @@ class USBClient : public Component { | ||||
|   int state_{USB_CLIENT_INIT}; | ||||
|   uint16_t vid_{}; | ||||
|   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]{}; | ||||
| }; | ||||
| class USBHost : public Component { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| // 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 "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| @@ -7,6 +7,7 @@ | ||||
|  | ||||
| #include <cinttypes> | ||||
| #include <cstring> | ||||
| #include <atomic> | ||||
| namespace esphome { | ||||
| namespace usb_host { | ||||
|  | ||||
| @@ -185,9 +186,11 @@ void USBClient::setup() { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   for (auto *trq : this->trq_pool_) { | ||||
|     usb_host_transfer_alloc(64, 0, &trq->transfer); | ||||
|     trq->client = this; | ||||
|   // Pre-allocate USB transfer buffers for all slots at startup | ||||
|   // This avoids any dynamic allocation during runtime | ||||
|   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 | ||||
| @@ -347,17 +350,39 @@ static void control_callback(const usb_transfer_t *xfer) { | ||||
|   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_() { | ||||
|   if (this->trq_pool_.empty()) { | ||||
|     ESP_LOGE(TAG, "Too many requests queued"); | ||||
|     return nullptr; | ||||
|   uint16_t mask = this->trq_in_use_.load(std::memory_order_relaxed); | ||||
|  | ||||
|   // 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(); | ||||
|   trq->client = this; | ||||
|   trq->transfer->context = trq; | ||||
|   trq->transfer->device_handle = this->device_handle_; | ||||
|   return trq; | ||||
|  | ||||
|   ESP_LOGE(TAG, "Too many requests queued (all %d slots in use)", MAX_REQUESTS); | ||||
|   return nullptr; | ||||
| } | ||||
| void USBClient::disconnect() { | ||||
|   this->on_disconnected(); | ||||
| @@ -370,6 +395,8 @@ void USBClient::disconnect() { | ||||
|   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, | ||||
|                                  const transfer_cb_t &callback, const std::vector<uint8_t> &data) { | ||||
|   auto *trq = this->get_trq_(); | ||||
| @@ -425,6 +452,9 @@ static void transfer_callback(usb_transfer_t *xfer) { | ||||
| } | ||||
| /** | ||||
|  * 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 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. | ||||
|  * 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 callback The callback function to be called when the transfer is complete. | ||||
| @@ -483,7 +516,28 @@ void USBClient::dump_config() { | ||||
|                 "  Product id %04X", | ||||
|                 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 esphome | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| // 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 <cinttypes> | ||||
| #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) | ||||
| USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent) | ||||
|  | ||||
|  | ||||
| UARTParityOptions = usb_uart_ns.enum("UARTParityOptions") | ||||
| UART_PARITY_OPTIONS = { | ||||
|     "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/usb_host.h" | ||||
| #include "esphome/core/log.h" | ||||
| @@ -72,6 +72,7 @@ void USBUartTypeCH34X::enable_channels() { | ||||
|     if (channel->index_ >= 2) | ||||
|       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 + 3, 0x80, 0, callback); | ||||
|   } | ||||
|   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/usb_host.h" | ||||
| #include "esphome/core/log.h" | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| // 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 "esphome/core/log.h" | ||||
| #include "esphome/components/uart/uart_debugger.h" | ||||
| @@ -216,9 +216,16 @@ void USBUartComponent::dump_config() { | ||||
| void USBUartComponent::start_input(USBUartChannel *channel) { | ||||
|   if (!channel->initialised_.load() || channel->input_started_.load()) | ||||
|     return; | ||||
|   // Note: This function is called from both USB task and main loop, so we cannot | ||||
|   // directly check ring buffer space here. Backpressure is handled by the chunk pool: | ||||
|   // when exhausted, USB input stops until chunks are freed by the main loop | ||||
|   // THREAD CONTEXT: Called from both USB task and main loop threads | ||||
|   // - USB task: Immediate restart after successful transfer for continuous data flow | ||||
|   // - 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; | ||||
|   // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback | ||||
|   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) { | ||||
|     auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep); | ||||
|     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; | ||||
|     } | ||||
|   } | ||||
| @@ -314,7 +322,7 @@ void USBUartTypeCdcAcm::on_connected() { | ||||
|   for (auto *channel : this->channels_) { | ||||
|     if (i == cdc_devs.size()) { | ||||
|       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; | ||||
|     } | ||||
|     channel->cdc_dev_ = cdc_devs[i++]; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #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/helpers.h" | ||||
| #include "esphome/components/uart/uart_component.h" | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "zwave_proxy.h" | ||||
| #include "esphome/components/api/api_server.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| @@ -97,12 +98,19 @@ void ZWaveProxy::process_uart_() { | ||||
|       // - 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 && | ||||
|           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 | ||||
|         std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size()); | ||||
|         this->home_id_ready_ = true; | ||||
|         ESP_LOGI(TAG, "Home ID: %s", | ||||
|                  format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); | ||||
|         if (this->set_home_id(&this->buffer_[4])) { | ||||
|           api::ZWaveProxyRequest msg; | ||||
|           msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; | ||||
|           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)); | ||||
|       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) { | ||||
|   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) { | ||||
|   if (length == 1 && data[0] == this->last_response_) { | ||||
|     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() { | ||||
|     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); | ||||
|  | ||||
|   | ||||
							
								
								
									
										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