mirror of
https://github.com/esphome/esphome.git
synced 2025-10-20 02:33:50 +01:00
Merge branch 'integration' into memory_api
This commit is contained in:
@@ -59,6 +59,8 @@ CONF_BATCH_DELAY = "batch_delay"
|
|||||||
CONF_CUSTOM_SERVICES = "custom_services"
|
CONF_CUSTOM_SERVICES = "custom_services"
|
||||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||||
|
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||||
|
CONF_MAX_CONNECTIONS = "max_connections"
|
||||||
|
|
||||||
|
|
||||||
def validate_encryption_key(value):
|
def validate_encryption_key(value):
|
||||||
@@ -158,6 +160,29 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
|
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
|
||||||
single=True
|
single=True
|
||||||
),
|
),
|
||||||
|
# Connection limits to prevent memory exhaustion on resource-constrained devices
|
||||||
|
# Each connection uses ~500-1000 bytes of RAM plus system resources
|
||||||
|
# Platform defaults based on available RAM and network stack implementation:
|
||||||
|
cv.SplitDefault(
|
||||||
|
CONF_LISTEN_BACKLOG,
|
||||||
|
esp8266=1, # Limited RAM (~40KB free), LWIP raw sockets
|
||||||
|
esp32=4, # More RAM (520KB), BSD sockets
|
||||||
|
rp2040=1, # Limited RAM (264KB), LWIP raw sockets like ESP8266
|
||||||
|
bk72xx=4, # Moderate RAM, BSD-style sockets
|
||||||
|
rtl87xx=4, # Moderate RAM, BSD-style sockets
|
||||||
|
host=4, # Abundant resources
|
||||||
|
ln882x=4, # Moderate RAM
|
||||||
|
): cv.int_range(min=1, max=10),
|
||||||
|
cv.SplitDefault(
|
||||||
|
CONF_MAX_CONNECTIONS,
|
||||||
|
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
|
||||||
|
esp32=8, # 520KB RAM available
|
||||||
|
rp2040=4, # 264KB RAM but LWIP constraints
|
||||||
|
bk72xx=8, # Moderate RAM
|
||||||
|
rtl87xx=8, # Moderate RAM
|
||||||
|
host=8, # Abundant resources
|
||||||
|
ln882x=8, # Moderate RAM
|
||||||
|
): cv.int_range(min=1, max=20),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||||
@@ -176,6 +201,10 @@ async def to_code(config):
|
|||||||
cg.add(var.set_password(config[CONF_PASSWORD]))
|
cg.add(var.set_password(config[CONF_PASSWORD]))
|
||||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||||
|
if CONF_LISTEN_BACKLOG in config:
|
||||||
|
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
||||||
|
if CONF_MAX_CONNECTIONS in config:
|
||||||
|
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
|
||||||
|
|
||||||
# Set USE_API_SERVICES if any services are enabled
|
# Set USE_API_SERVICES if any services are enabled
|
||||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||||
|
@@ -87,7 +87,7 @@ void APIServer::setup() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = this->socket_->listen(4);
|
err = this->socket_->listen(this->listen_backlog_);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
|
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
@@ -140,9 +140,19 @@ void APIServer::loop() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
struct sockaddr_storage source_addr;
|
struct sockaddr_storage source_addr;
|
||||||
socklen_t addr_len = sizeof(source_addr);
|
socklen_t addr_len = sizeof(source_addr);
|
||||||
|
|
||||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||||
if (!sock)
|
if (!sock)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Check if we're at the connection limit
|
||||||
|
if (this->clients_.size() >= this->max_connections_) {
|
||||||
|
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
|
||||||
|
// Immediately close - socket destructor will handle cleanup
|
||||||
|
sock.reset();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
|
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
|
||||||
|
|
||||||
auto *conn = new APIConnection(std::move(sock), this);
|
auto *conn = new APIConnection(std::move(sock), this);
|
||||||
@@ -206,8 +216,10 @@ void APIServer::loop() {
|
|||||||
void APIServer::dump_config() {
|
void APIServer::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
"Server:\n"
|
"Server:\n"
|
||||||
" Address: %s:%u",
|
" Address: %s:%u\n"
|
||||||
network::get_use_address().c_str(), this->port_);
|
" Listen backlog: %u\n"
|
||||||
|
" Max connections: %u",
|
||||||
|
network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||||
if (!this->noise_ctx_->has_psk()) {
|
if (!this->noise_ctx_->has_psk()) {
|
||||||
|
@@ -44,6 +44,8 @@ class APIServer : public Component, public Controller {
|
|||||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||||
void set_batch_delay(uint16_t batch_delay);
|
void set_batch_delay(uint16_t batch_delay);
|
||||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||||
|
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||||
|
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
|
||||||
|
|
||||||
// Get reference to shared buffer for API connections
|
// Get reference to shared buffer for API connections
|
||||||
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
|
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||||
@@ -189,8 +191,12 @@ class APIServer : public Component, public Controller {
|
|||||||
// Group smaller types together
|
// Group smaller types together
|
||||||
uint16_t port_{6053};
|
uint16_t port_{6053};
|
||||||
uint16_t batch_delay_{100};
|
uint16_t batch_delay_{100};
|
||||||
|
// Connection limits - these defaults will be overridden by config values
|
||||||
|
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
|
||||||
|
uint8_t listen_backlog_{4};
|
||||||
|
uint8_t max_connections_{8};
|
||||||
bool shutting_down_ = false;
|
bool shutting_down_ = false;
|
||||||
// 5 bytes used, 3 bytes padding
|
// 7 bytes used, 1 byte padding
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
|
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
#include "lwip/tcp.h"
|
#include "lwip/tcp.h"
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <queue>
|
#include <array>
|
||||||
|
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -50,12 +50,18 @@ class LWIPRawImpl : public Socket {
|
|||||||
errno = EBADF;
|
errno = EBADF;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (accepted_sockets_.empty()) {
|
if (this->accepted_socket_count_ == 0) {
|
||||||
errno = EWOULDBLOCK;
|
errno = EWOULDBLOCK;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
std::unique_ptr<LWIPRawImpl> sock = std::move(accepted_sockets_.front());
|
// Take from front for FIFO ordering
|
||||||
accepted_sockets_.pop();
|
std::unique_ptr<LWIPRawImpl> sock = std::move(this->accepted_sockets_[0]);
|
||||||
|
// Shift remaining sockets forward
|
||||||
|
for (uint8_t i = 1; i < this->accepted_socket_count_; i++) {
|
||||||
|
this->accepted_sockets_[i - 1] = std::move(this->accepted_sockets_[i]);
|
||||||
|
}
|
||||||
|
this->accepted_socket_count_--;
|
||||||
|
LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_);
|
||||||
if (addr != nullptr) {
|
if (addr != nullptr) {
|
||||||
sock->getpeername(addr, addrlen);
|
sock->getpeername(addr, addrlen);
|
||||||
}
|
}
|
||||||
@@ -494,9 +500,18 @@ class LWIPRawImpl : public Socket {
|
|||||||
// nothing to do here, we just don't push it to the queue
|
// nothing to do here, we just don't push it to the queue
|
||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
|
// Check if we've reached the maximum accept queue size
|
||||||
|
if (this->accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) {
|
||||||
|
LWIP_LOG("Rejecting connection, queue full (%d)", this->accepted_socket_count_);
|
||||||
|
// Abort the connection when queue is full
|
||||||
|
tcp_abort(newpcb);
|
||||||
|
// Must return ERR_ABRT since we called tcp_abort()
|
||||||
|
return ERR_ABRT;
|
||||||
|
}
|
||||||
auto sock = make_unique<LWIPRawImpl>(family_, newpcb);
|
auto sock = make_unique<LWIPRawImpl>(family_, newpcb);
|
||||||
sock->init();
|
sock->init();
|
||||||
accepted_sockets_.push(std::move(sock));
|
this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock);
|
||||||
|
LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
|
||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
void err_fn(err_t err) {
|
void err_fn(err_t err) {
|
||||||
@@ -587,7 +602,20 @@ class LWIPRawImpl : public Socket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct tcp_pcb *pcb_;
|
struct tcp_pcb *pcb_;
|
||||||
std::queue<std::unique_ptr<LWIPRawImpl>> accepted_sockets_;
|
// Accept queue - holds incoming connections briefly until the event loop calls accept()
|
||||||
|
// This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop
|
||||||
|
// 3 slots is plenty since connections are pulled out quickly by the event loop
|
||||||
|
//
|
||||||
|
// Memory analysis: std::array<3> vs original std::queue implementation:
|
||||||
|
// - std::queue uses std::deque internally which on 32-bit systems needs:
|
||||||
|
// 24 bytes (deque object) + 32+ bytes (map array) + heap allocations
|
||||||
|
// Total: ~56+ bytes minimum, plus heap fragmentation
|
||||||
|
// - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes)
|
||||||
|
// Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations
|
||||||
|
// Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation)
|
||||||
|
static constexpr size_t MAX_ACCEPTED_SOCKETS = 3;
|
||||||
|
std::array<std::unique_ptr<LWIPRawImpl>, MAX_ACCEPTED_SOCKETS> accepted_sockets_;
|
||||||
|
uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue
|
||||||
bool rx_closed_ = false;
|
bool rx_closed_ = false;
|
||||||
pbuf *rx_buf_ = nullptr;
|
pbuf *rx_buf_ = nullptr;
|
||||||
size_t rx_buf_offset_ = 0;
|
size_t rx_buf_offset_ = 0;
|
||||||
|
Reference in New Issue
Block a user