1
0
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:
J. Nick Koston
2025-09-28 18:27:18 -05:00
20 changed files with 220 additions and 52 deletions

View File

@@ -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];
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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)"

View File

@@ -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 {

View File

@@ -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]),
) )

View File

@@ -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 {

View File

@@ -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

View File

@@ -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"

View File

@@ -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,

View File

@@ -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();
} }

View File

@@ -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"

View File

@@ -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++];

View File

@@ -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"

View File

@@ -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]);

View File

@@ -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);

View 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