mirror of
https://github.com/esphome/esphome.git
synced 2026-01-08 03:00:48 -07:00
async_tcp: Add AsyncClient for ESP-IDF and host (#12337)
Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,37 +1,50 @@
|
||||
# Dummy integration to allow relying on AsyncTCP
|
||||
# Async TCP client support for all platforms
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema({}),
|
||||
cv.only_with_arduino,
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
# Socket component needed for platforms using socket-based implementation
|
||||
# ESP32, ESP8266, RP2040, and LibreTiny use AsyncTCP libraries, others use sockets
|
||||
if (
|
||||
not CORE.is_esp32
|
||||
and not CORE.is_esp8266
|
||||
and not CORE.is_rp2040
|
||||
and not CORE.is_libretiny
|
||||
):
|
||||
return ["socket"]
|
||||
return []
|
||||
|
||||
|
||||
# Support all platforms - Arduino/ESP-IDF get libraries, other platforms use socket implementation
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
if CORE.using_esp_idf:
|
||||
# ESP-IDF needs the IDF component
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(name="esp32async/asynctcp", ref="3.4.91")
|
||||
elif CORE.is_esp32 or CORE.is_libretiny:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
|
||||
elif CORE.is_esp8266:
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
elif CORE.is_rp2040:
|
||||
# https://github.com/khoih-prog/AsyncTCP_RP2040W
|
||||
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
|
||||
# Other platforms (host, etc) use socket-based implementation
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
# Exclude socket implementation for platforms that use AsyncTCP libraries
|
||||
if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny:
|
||||
return ["async_tcp_socket.cpp"]
|
||||
return []
|
||||
|
||||
17
esphome/components/async_tcp/async_tcp.h
Normal file
17
esphome/components/async_tcp/async_tcp.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if (defined(USE_ESP32) || defined(USE_LIBRETINY)) && !defined(CLANG_TIDY)
|
||||
// Use AsyncTCP library for ESP32 (Arduino or ESP-IDF) and LibreTiny
|
||||
// But not for clang-tidy as the header file isn't present in that case
|
||||
#include <AsyncTCP.h>
|
||||
#elif defined(USE_ESP8266)
|
||||
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(USE_RP2040)
|
||||
// Use AsyncTCP_RP2040W library for RP2040
|
||||
#include <AsyncTCP_RP2040W.h>
|
||||
#else
|
||||
// Use socket-based implementation for other platforms and clang-tidy
|
||||
#include "async_tcp_socket.h"
|
||||
#endif
|
||||
161
esphome/components/async_tcp/async_tcp_socket.cpp
Normal file
161
esphome/components/async_tcp/async_tcp_socket.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
#include "async_tcp_socket.h"
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cerrno>
|
||||
#include <sys/select.h>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
static const char *const TAG = "async_tcp";
|
||||
|
||||
// Read buffer size matches TCP MSS (1500 MTU - 40 bytes IP/TCP headers).
|
||||
// This implementation only runs on ESP-IDF and host which have ample stack.
|
||||
static constexpr size_t READ_BUFFER_SIZE = 1460;
|
||||
|
||||
bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
if (connected_ || connecting_) {
|
||||
ESP_LOGW(TAG, "Already connected/connecting");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve address
|
||||
struct sockaddr_storage addr;
|
||||
socklen_t addrlen = esphome::socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, port);
|
||||
if (addrlen == 0) {
|
||||
ESP_LOGE(TAG, "Invalid address: %s", host);
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create socket with loop monitoring
|
||||
int family = ((struct sockaddr *) &addr)->sa_family;
|
||||
socket_ = esphome::socket::socket_loop_monitored(family, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (!socket_) {
|
||||
ESP_LOGE(TAG, "Failed to create socket");
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
socket_->setblocking(false);
|
||||
|
||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||
if (err == 0) {
|
||||
// Connection succeeded immediately (rare, but possible for localhost)
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
return true;
|
||||
}
|
||||
if (errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
connecting_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AsyncClient::close() {
|
||||
socket_.reset();
|
||||
bool was_connected = connected_;
|
||||
connected_ = false;
|
||||
connecting_ = false;
|
||||
if (was_connected && disconnect_cb_)
|
||||
disconnect_cb_(disconnect_arg_, this);
|
||||
}
|
||||
|
||||
size_t AsyncClient::write(const char *data, size_t len) {
|
||||
if (!socket_ || !connected_)
|
||||
return 0;
|
||||
|
||||
ssize_t sent = socket_->write(data, len);
|
||||
if (sent < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
void AsyncClient::loop() {
|
||||
if (!socket_)
|
||||
return;
|
||||
|
||||
if (connecting_) {
|
||||
// For connecting, we need to check writability, not readability
|
||||
// The Application's select() only monitors read FDs, so we do our own check here
|
||||
// For ESP platforms lwip_select() might be faster, but this code isn't used
|
||||
// on those platforms anyway. If it was, we'd fix the Application select()
|
||||
// to report writability instead of doing it this way.
|
||||
int fd = socket_->get_fd();
|
||||
if (fd < 0) {
|
||||
ESP_LOGW(TAG, "Invalid socket fd");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
|
||||
struct timeval tv = {0, 0};
|
||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
|
||||
if (ret > 0 && FD_ISSET(fd, &writefds)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
|
||||
connecting_ = false;
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Select error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
if (!socket_->ready())
|
||||
return;
|
||||
|
||||
uint8_t buf[READ_BUFFER_SIZE];
|
||||
ssize_t len = socket_->read(buf, READ_BUFFER_SIZE);
|
||||
|
||||
if (len == 0) {
|
||||
ESP_LOGI(TAG, "Connection closed by peer");
|
||||
close();
|
||||
} else if (len > 0) {
|
||||
if (data_cb_)
|
||||
data_cb_(data_arg_, this, buf, len);
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGW(TAG, "Read error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::async_tcp
|
||||
|
||||
#endif // defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
73
esphome/components/async_tcp/async_tcp_socket.h
Normal file
73
esphome/components/async_tcp/async_tcp_socket.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
/// AsyncClient API for platforms using sockets (ESP-IDF, host, etc.)
|
||||
/// NOTE: This class is NOT thread-safe. All methods must be called from the main loop.
|
||||
class AsyncClient {
|
||||
public:
|
||||
using AcConnectHandler = std::function<void(void *, AsyncClient *)>;
|
||||
using AcDataHandler = std::function<void(void *, AsyncClient *, void *data, size_t len)>;
|
||||
using AcErrorHandler = std::function<void(void *, AsyncClient *, int8_t error)>;
|
||||
|
||||
AsyncClient() = default;
|
||||
~AsyncClient() = default;
|
||||
|
||||
[[nodiscard]] bool connect(const char *host, uint16_t port);
|
||||
void close();
|
||||
[[nodiscard]] bool connected() const { return connected_; }
|
||||
size_t write(const char *data, size_t len);
|
||||
|
||||
void onConnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
connect_cb_ = std::move(cb);
|
||||
connect_arg_ = arg;
|
||||
}
|
||||
void onDisconnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
disconnect_cb_ = std::move(cb);
|
||||
disconnect_arg_ = arg;
|
||||
}
|
||||
/// Set data callback. NOTE: data pointer is only valid during callback execution.
|
||||
void onData(AcDataHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
data_cb_ = std::move(cb);
|
||||
data_arg_ = arg;
|
||||
}
|
||||
void onError(AcErrorHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
error_cb_ = std::move(cb);
|
||||
error_arg_ = arg;
|
||||
}
|
||||
|
||||
// Must be called from loop()
|
||||
void loop();
|
||||
|
||||
private:
|
||||
std::unique_ptr<esphome::socket::Socket> socket_;
|
||||
|
||||
AcConnectHandler connect_cb_{nullptr};
|
||||
void *connect_arg_{nullptr};
|
||||
AcConnectHandler disconnect_cb_{nullptr};
|
||||
void *disconnect_arg_{nullptr};
|
||||
AcDataHandler data_cb_{nullptr};
|
||||
void *data_arg_{nullptr};
|
||||
AcErrorHandler error_cb_{nullptr};
|
||||
void *error_arg_{nullptr};
|
||||
|
||||
bool connected_{false};
|
||||
bool connecting_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::async_tcp
|
||||
|
||||
// Expose AsyncClient in global namespace to match library behavior
|
||||
using esphome::async_tcp::AsyncClient; // NOLINT(google-global-names-in-headers)
|
||||
#define ESPHOME_ASYNC_TCP_SOCKET_IMPL
|
||||
#endif // defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
@@ -580,6 +580,7 @@ def lint_relative_py_import(fname: Path, line, col, content):
|
||||
],
|
||||
exclude=[
|
||||
"esphome/components/socket/headers.h",
|
||||
"esphome/components/async_tcp/async_tcp.h",
|
||||
"esphome/components/esp32/core.cpp",
|
||||
"esphome/components/esp8266/core.cpp",
|
||||
"esphome/components/rp2040/core.cpp",
|
||||
|
||||
Reference in New Issue
Block a user