Merge remote-tracking branch 'upstream-ssh/api-overflow-buffer-cleanup' into integration

This commit is contained in:
J. Nick Koston
2026-03-16 13:11:13 -10:00
30 changed files with 346 additions and 245 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
- if: failure()
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
name: Request changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
@@ -53,7 +53,7 @@ jobs:
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
})
- if: success()
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
name: Dismiss review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
category: "/language:${{matrix.language}}"
+3 -3
View File
@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+1 -1
View File
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
uint8_t current_sensor_;
// The AM43 often gets into a state where it spams loads of battery update
// notifications. Here we will limit to no more than every 10s.
uint8_t last_battery_update_;
uint32_t last_battery_update_;
};
} // namespace am43
+3 -2
View File
@@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All(
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Platform defaults based on available RAM and typical message rates:
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=5, # Limited RAM, need to fail fast
esp8266=4, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
rp2040=8, # Moderate RAM
bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
+36 -125
View File
@@ -100,150 +100,61 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("UNKNOWN");
}
// Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() {
if (this->tx_buf_count_ > 0) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
return APIError::OK;
}
// Common socket write error handling
APIError APIFrameHelper::handle_socket_write_error_() {
const int err = errno;
if (err == EWOULDBLOCK || err == EAGAIN) {
return APIError::WOULD_BLOCK;
}
HELPER_LOG("Socket write failed with errno %d", err);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) {
// Check if queue is full
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
this->state_ = State::FAILED;
return;
}
uint16_t buffer_size = total_write_len - offset;
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
buffer = std::make_unique<SendBuffer>(SendBuffer{
.data = std::make_unique<uint8_t[]>(buffer_size),
.size = buffer_size,
.offset = 0,
});
uint16_t to_skip = offset;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
// Skip this entire segment
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
// Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer->data.get() + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
// Update circular buffer tracking
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_++;
}
// This method writes data to socket or buffers it
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
// Returns OK if all data was sent or successfully queued.
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
}
#endif
// Try to send any existing buffered data first if there is any
if (this->tx_buf_count_ > 0) {
APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
return send_result;
}
uint16_t skip = 0;
// If there is still data in the buffer, we can't send, buffer
// the new data and return
if (this->tx_buf_count_ > 0) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
// Drain any existing backlog first
if (!this->overflow_buf_.empty()) [[unlikely]] {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
// Try to send directly if no buffered data
// Optimize for single iovec case (common for plaintext API)
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
// If backlog is clear, try direct send
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
if (sent == -1) {
APIError err = this->handle_socket_write_error_();
if (err == APIError::WOULD_BLOCK) {
// Socket would block, buffer the data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
return err; // Socket write failed
} else if (static_cast<uint16_t>(sent) < total_write_len) {
// Partially sent, buffer the remaining data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
}
return APIError::OK; // Success, all data sent or buffered
}
// Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
while (this->tx_buf_count_ > 0) {
// Get the first buffer in the queue
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
// Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
if (sent == -1) {
return this->handle_socket_write_error_();
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer->offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
if (sent == -1) [[unlikely]] {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_[this->tx_buf_head_].reset();
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_--;
// Continue loop to try sending the next buffer
skip = static_cast<uint16_t>(sent);
}
}
return APIError::OK; // All buffers sent successfully
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
return APIError::OK;
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
+18 -25
View File
@@ -9,6 +9,7 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/api/api_overflow_buffer.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -104,9 +105,9 @@ class APIFrameHelper {
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop();
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
@@ -189,28 +190,23 @@ class APIFrameHelper {
}
protected:
// Buffer containing data to be sent
struct SendBuffer {
std::unique_ptr<uint8_t[]> data;
uint16_t size{0}; // Total size of the buffer
uint16_t offset{0}; // Current offset within the buffer
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return size - offset; }
const uint8_t *current_data() const { return data.get() + offset; }
};
// Drain backlogged overflow data to the socket and handle errors.
// Called when overflow_buf_.empty() is false. Out-of-line to keep the
// fast path (empty check) inline at call sites.
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
// Common socket write error handling
APIError handle_socket_write_error_();
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
APIError check_socket_write_err_(int err) {
if (err == EWOULDBLOCK || err == EAGAIN)
return APIError::WOULD_BLOCK;
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -245,8 +241,8 @@ class APIFrameHelper {
return APIError::WOULD_BLOCK;
}
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
// Backlog for unsent data when TCP send buffer is full (rarely used in production)
APIOverflowBuffer overflow_buf_;
APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
@@ -257,9 +253,6 @@ class APIFrameHelper {
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
// Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
@@ -153,8 +153,10 @@ APIError APINoiseFrameHelper::loop() {
}
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -64,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -0,0 +1,73 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
namespace esphome::api {
APIOverflowBuffer::~APIOverflowBuffer() {
for (auto *entry : this->queue_) {
if (entry != nullptr)
Entry::destroy(entry);
}
}
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
while (this->count_ > 0) {
Entry *front = this->queue_[this->head_];
ssize_t sent = socket->write(front->current_data(), front->remaining());
if (sent <= 0) {
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
// 0 = nothing sent (treat as no progress)
return sent;
}
if (static_cast<uint16_t>(sent) < front->remaining()) {
// Partially sent, update offset and stop
front->offset += static_cast<uint16_t>(sent);
return sent;
}
// Entry fully sent — free it and advance
Entry::destroy(front);
this->queue_[this->head_] = nullptr;
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
this->count_--;
}
return 0; // All drained
}
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
if (this->count_ >= API_MAX_SEND_QUEUE)
return false;
uint16_t buffer_size = total_len - skip;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
this->queue_[this->tail_] = entry;
uint16_t to_skip = skip;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(entry->data + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
this->count_++;
return true;
}
} // namespace esphome::api
#endif // USE_API
@@ -0,0 +1,76 @@
#pragma once
#include <array>
#include <cstdint>
#include <sys/types.h>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/socket/headers.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
///
/// Under normal operation this buffer is **never used** — data goes straight
/// from the frame helper to the socket. It only fills when the LWIP TCP
/// send buffer is full (slow client, congested network, heavy logging).
/// The queue drains automatically on subsequent write/loop calls once the
/// socket becomes writable again.
///
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
/// config). If the queue fills completely the connection is marked failed.
class APIOverflowBuffer {
public:
/// A single heap-allocated send-backlog entry.
/// Lifetime is manually managed — see destroy().
struct Entry {
uint8_t *data;
uint16_t size; // Total size of the buffer
uint16_t offset; // Current send offset within the buffer
uint16_t remaining() const { return this->size - this->offset; }
const uint8_t *current_data() const { return this->data + this->offset; }
/// Free this entry and its data buffer.
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
delete[] entry->data;
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
};
~APIOverflowBuffer();
/// True when no backlogged data is waiting.
bool empty() const { return this->count_ == 0; }
/// True when the queue has no room for another entry.
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
/// Number of entries currently queued.
uint8_t count() const { return this->count_; }
/// Try to drain queued data to the socket.
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
/// Callers only need to act on -1; 0 and positive values both mean "no error".
/// Frees entries as they are fully sent.
ssize_t try_drain(socket::Socket *socket);
/// Enqueue unsent IOV data into the backlog.
/// Copies iov data starting at byte offset `skip` into a new entry.
/// Returns false if the queue is full (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
protected:
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
uint8_t head_{0};
uint8_t tail_{0};
uint8_t count_{0};
};
} // namespace esphome::api
#endif // USE_API
+1 -1
View File
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
INT_MASK = 0xF0,
THRESH_MASK = 0x0F,
R_SPIKE_MASK = 0xF0,
ENERGY_MASK = 0xF0,
ENERGY_MASK = 0xE0,
CAP_MASK = 0xF0,
LIGHT_MASK = 0xCF,
DISTURB_MASK = 0xDF,
@@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
float DallasTemperatureSensor::get_temp_c_() {
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
if (this->scratch_pad_[7] == 0) {
return NAN;
}
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
}
switch (this->resolution_) {
@@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
InterruptLock lock;
uint64_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
for (uint8_t i = 0; i < 64; i++) {
ret |= (uint64_t(this->read_bit_()) << i);
}
return ret;
+1 -1
View File
@@ -587,7 +587,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.3.2",
ref="0.3.4",
)
# Set compile-time configuration via build flags (so external library sees them)
+1 -1
View File
@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
if (err == ESP_ERR_INVALID_STATE) {
if (err == ESP_ERR_INVALID_STATE || err == ESP_ERR_INVALID_RESPONSE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;
} else if (err == ESP_ERR_TIMEOUT) {
+70
View File
@@ -1,3 +1,5 @@
from dataclasses import dataclass, field
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
@@ -26,6 +28,9 @@ CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
MULTI_CONF = True
CONF_PDM = "pdm"
CONF_ADC_TYPE = "adc_type"
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
CONF_I2S_DIN_PIN = "i2s_din_pin"
CONF_I2S_MCLK_PIN = "i2s_mclk_pin"
@@ -254,7 +259,65 @@ CONFIG_SCHEMA = cv.All(
)
@dataclass
class I2SAudioData:
"""I2S audio component state stored in CORE.data."""
port_map: dict[str, int] = field(default_factory=dict)
def _get_data() -> I2SAudioData:
if CONF_I2S_AUDIO not in CORE.data:
CORE.data[CONF_I2S_AUDIO] = I2SAudioData()
return CORE.data[CONF_I2S_AUDIO]
def _assign_ports() -> None:
"""Assign I2S port numbers, prioritizing instances with microphone children.
Microphones (especially PDM) require port 0 on most ESP32 variants.
This runs once and stores the mapping in CORE.data.
"""
data = _get_data()
if data.port_map:
return
full_config = fv.full_config.get()
i2s_configs = full_config[CONF_I2S_AUDIO]
# Find i2s_audio instances with microphones that require port 0
# (PDM and internal ADC only work on I2S port 0)
port0_parent_id = None
for mic_config in full_config.get("microphone", []):
if CONF_I2S_AUDIO_ID not in mic_config:
continue
if mic_config.get(CONF_PDM) or mic_config.get(CONF_ADC_TYPE) == "internal":
if port0_parent_id is not None:
raise cv.Invalid(
"Only one PDM/ADC microphone is supported (requires I2S port 0)"
)
port0_parent_id = str(mic_config[CONF_I2S_AUDIO_ID])
# Assign ports: port 0 parent first (if any), rest get sequential
next_port = 0
if port0_parent_id is not None:
data.port_map[port0_parent_id] = next_port
next_port += 1
for config in i2s_configs:
config_id = str(config[CONF_ID])
if config_id != port0_parent_id:
data.port_map[config_id] = next_port
next_port += 1
def _final_validate(_):
from esphome.components.esp32 import idf_version
if use_legacy() and idf_version() >= cv.Version(6, 0, 0):
raise cv.Invalid(
"The legacy I2S driver is not available in ESP-IDF 6.0+. "
"Set 'use_legacy: false' in i2s_audio configuration."
)
i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO]
variant = get_esp32_variant()
if variant not in I2S_PORTS:
@@ -263,6 +326,7 @@ def _final_validate(_):
raise cv.Invalid(
f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}"
)
_assign_ports()
def use_legacy():
@@ -276,6 +340,12 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Assign I2S port from _final_validate computed mapping
data = _get_data()
if (port := data.port_map.get(str(config[CONF_ID]))) is None:
raise ValueError(f"No I2S port assigned for {config[CONF_ID]}")
cg.add(var.set_port(port))
# Re-enable ESP-IDF's I2S driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_i2s")
@@ -1,27 +0,0 @@
#include "i2s_audio.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
namespace esphome {
namespace i2s_audio {
static const char *const TAG = "i2s_audio";
void I2SAudioComponent::setup() {
static i2s_port_t next_port_num = I2S_NUM_0;
if (next_port_num >= SOC_I2S_NUM) {
ESP_LOGE(TAG, "Too many components");
this->mark_failed();
return;
}
this->port_ = next_port_num;
next_port_num = (i2s_port_t) (next_port_num + 1);
}
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32
+8 -5
View File
@@ -5,6 +5,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <esp_idf_version.h>
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
@@ -56,8 +57,6 @@ class I2SAudioOut : public I2SAudioBase {};
class I2SAudioComponent : public Component {
public:
void setup() override;
#ifdef USE_I2S_LEGACY
i2s_pin_config_t get_pin_config() const {
return {
@@ -86,13 +85,17 @@ class I2SAudioComponent : public Component {
void set_mclk_pin(int pin) { this->mclk_pin_ = pin; }
void set_bclk_pin(int pin) { this->bclk_pin_ = pin; }
void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; }
void set_port(int port) { this->port_ = port; }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
int get_port() const { return this->port_; }
#else
i2s_port_t get_port() const { return static_cast<i2s_port_t>(this->port_); }
#endif
void lock() { this->lock_.lock(); }
bool try_lock() { return this->lock_.try_lock(); }
void unlock() { this->lock_.unlock(); }
i2s_port_t get_port() const { return this->port_; }
protected:
Mutex lock_;
@@ -106,7 +109,7 @@ class I2SAudioComponent : public Component {
int bclk_pin_{I2S_GPIO_UNUSED};
#endif
int lrclk_pin_;
i2s_port_t port_{};
int port_{};
};
} // namespace i2s_audio
@@ -13,9 +13,11 @@ from esphome.const import (
)
from .. import (
CONF_ADC_TYPE,
CONF_I2S_DIN_PIN,
CONF_LEFT,
CONF_MONO,
CONF_PDM,
CONF_RIGHT,
I2SAudioIn,
i2s_audio_component_schema,
@@ -29,9 +31,7 @@ CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2s_audio"]
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_CORRECT_DC_OFFSET = "correct_dc_offset"
CONF_PDM = "pdm"
I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
@@ -37,27 +37,6 @@ enum MicrophoneEventGroupBits : uint32_t {
};
void I2SAudioMicrophone::setup() {
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
if (this->adc_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "Internal ADC only works on I2S0");
this->mark_failed();
return;
}
} else
#endif
#endif
{
if (this->pdm_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "PDM only works on I2S0");
this->mark_failed();
return;
}
}
}
this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS);
if (this->active_listeners_semaphore_ == nullptr) {
ESP_LOGE(TAG, "Creating semaphore failed");
@@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
this->y_raw_max_ = this->display_->get_native_height();
}
}
}
@@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() {
}
point = buffer[5] & 0xF;
if (point > 2) {
ESP_LOGW(TAG, "Invalid touch point count: %d", point);
point = 2;
}
if (point == 1) {
err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1);
+12 -6
View File
@@ -194,8 +194,12 @@ def model_schema(config):
CONF_DE_PIN, cv.UNDEFINED
): pins.internal_gpio_output_pin_schema,
model.option(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema,
model.option(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema,
model.option(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema,
model.option(
CONF_HSYNC_PIN, cv.UNDEFINED
): pins.internal_gpio_output_pin_schema,
model.option(
CONF_VSYNC_PIN, cv.UNDEFINED
): pins.internal_gpio_output_pin_schema,
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
}
)
@@ -307,10 +311,12 @@ async def to_code(config):
cg.add(var.set_de_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN])
cg.add(var.set_pclk_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN])
cg.add(var.set_hsync_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN])
cg.add(var.set_vsync_pin(pin))
if hsync_pin := config.get(CONF_HSYNC_PIN):
pin = await cg.gpio_pin_expression(hsync_pin)
cg.add(var.set_hsync_pin(pin))
if vsync_pin := config.get(CONF_VSYNC_PIN):
pin = await cg.gpio_pin_expression(vsync_pin)
cg.add(var.set_vsync_pin(pin))
await display.register_display(var, config)
if lamb := config.get(CONF_LAMBDA):
+10 -2
View File
@@ -158,8 +158,16 @@ void MipiRgb::common_setup_() {
}
config.data_width = data_pin_count;
config.disp_gpio_num = GPIO_NUM_NC;
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
if (this->hsync_pin_) {
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
} else {
config.hsync_gpio_num = GPIO_NUM_NC;
}
if (this->vsync_pin_) {
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
} else {
config.vsync_gpio_num = GPIO_NUM_NC;
}
if (this->de_pin_) {
config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin());
} else {
+2 -2
View File
@@ -55,8 +55,8 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
// firing during critical sections.
//
// When neither WiFi nor Ethernet is configured, LwIPLock is
// defined inline as a no-op in helpers.h.
// When neither WiFi nor Ethernet is configured, this is a no-op since
// there's no network stack and no lwip callbacks to race with.
#if defined(USE_WIFI)
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
@@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
this->sg_recv_data_state_ = FRAME_DATA_LEN_H;
break;
case FRAME_DATA_LEN_H:
if (value <= 4) {
this->sg_data_len_ = value * 256;
if (value == 0) {
this->sg_frame_buf_[4] = value;
this->sg_recv_data_state_ = FRAME_DATA_LEN_L;
} else {
this->sg_data_len_ = 0;
this->sg_recv_data_state_ = FRAME_IDLE;
ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value);
}
break;
case FRAME_DATA_LEN_L:
this->sg_data_len_ += value;
if (this->sg_data_len_ > 32) {
this->sg_data_len_ = value;
if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) {
ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value);
this->sg_data_len_ = 0;
this->sg_recv_data_state_ = FRAME_IDLE;
@@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
}
break;
case FRAME_DATA_BYTES:
this->sg_data_len_ -= 1;
this->sg_frame_buf_[this->sg_frame_len_++] = value;
if (this->sg_data_len_ <= 0) {
if (--this->sg_data_len_ == 0) {
this->sg_recv_data_state_ = FRAME_DATA_CRC;
}
break;
+1 -1
View File
@@ -416,7 +416,7 @@ void WebServer::setup() {
this->set_interval(10000, [this]() {
char buf[32];
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime);
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
});
}
+1 -1
View File
@@ -314,7 +314,7 @@ class Version:
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value)
match = re.match(r"^(\d+).(\d+).(\d+)[-.]?(\w*)$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
+1 -1
View File
@@ -64,7 +64,7 @@ dependencies:
rules:
- if: "target in [esp32s2, esp32s3, esp32p4]"
esphome/esp-hub75:
version: 0.3.2
version: 0.3.4
rules:
- if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]"
espressif/mqtt: