mirror of
https://github.com/esphome/esphome.git
synced 2025-10-29 22:24:26 +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