mirror of
https://github.com/esphome/esphome.git
synced 2025-10-31 23:21:54 +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,18 +350,40 @@ 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++) {
|
||||||
auto *trq = this->trq_pool_.front();
|
if (!(mask & (1U << i))) {
|
||||||
this->trq_pool_.pop_front();
|
// Slot i appears available, try to claim it atomically
|
||||||
trq->client = this;
|
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->context = trq;
|
||||||
trq->transfer->device_handle = this->device_handle_;
|
trq->transfer->device_handle = this->device_handle_;
|
||||||
return trq;
|
return trq;
|
||||||
}
|
}
|
||||||
|
// Another thread claimed this slot, retry with updated mask
|
||||||
|
mask = expected;
|
||||||
|
i--; // Retry the same index with new mask value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGE(TAG, "Too many requests queued (all %d slots in use)", MAX_REQUESTS);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
void USBClient::disconnect() {
|
void USBClient::disconnect() {
|
||||||
this->on_disconnected();
|
this->on_disconnected();
|
||||||
auto err = usb_host_device_close(this->handle_, this->device_handle_);
|
auto err = usb_host_device_close(this->handle_, this->device_handle_);
|
||||||
@@ -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