Compare commits

..

11 Commits

Author SHA1 Message Date
J. Nick Koston
db5a58d71e fix dhcp 2026-01-24 22:51:38 -10:00
J. Nick Koston
7a2d4cd801 fix log spam with scan 2026-01-24 22:46:52 -10:00
J. Nick Koston
7c26047e04 fix address sta, make empty password work 2026-01-24 22:39:53 -10:00
J. Nick Koston
539e2a3c90 fix address sta, make empty password work 2026-01-24 22:38:28 -10:00
J. Nick Koston
83ae089bad Add captive_portal RP2040 test 2026-01-24 22:19:54 -10:00
J. Nick Koston
39cdedfd89 test 2026-01-24 22:13:07 -10:00
J. Nick Koston
4a453d90ad [web_server][captive_portal] Add RP2040 platform support 2026-01-24 22:09:48 -10:00
J. Nick Koston
51bf568b8f fix 2026-01-24 21:52:47 -10:00
J. Nick Koston
8a0d99285c tweak 2026-01-24 21:50:42 -10:00
J. Nick Koston
7e456265a4 Update ESPAsyncWebServer in platformio.ini to 3.9.5 2026-01-24 21:49:57 -10:00
J. Nick Koston
6954a69ed2 3.9.5 2026-01-24 21:48:47 -10:00
23 changed files with 80 additions and 107 deletions

View File

@@ -1 +1 @@
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce
c0db7505713f2ebf5d18f274d35469cfbdaabe1e1def9fe2195594dc345f1a49

View File

@@ -292,7 +292,7 @@ CONFIG_SCHEMA = cv.All(
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
rp2040=8, # 264KB RAM, plenty of heap available
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=8, # Abundant resources

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
}
APIConnection::~APIConnection() {
@@ -1524,11 +1524,8 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
}
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_client_peername()));
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1547,9 +1544,8 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1866,8 +1862,7 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::on_fatal_error() {
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
this->helper_->close();
this->flags_.remove = true;
}
@@ -2223,14 +2218,12 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::SOCKADDR_STR_LEN];
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
this->helper_->get_client_peername(), LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}

View File

@@ -281,8 +281,8 @@ class APIConnection final : public APIServerConnection {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername_to(char *buf) const { return this->helper_->get_peername_to(buf); }
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
protected:
// Helper function to handle authentication completion

View File

@@ -16,12 +16,7 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
@@ -250,6 +245,8 @@ APIError APIFrameHelper::init_common_() {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -90,16 +90,8 @@ class APIFrameHelper {
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
// Returns pointer to buf for convenience in printf-style calls
const char *get_peername_to(char *buf) const {
if (this->socket_) {
this->socket_->getpeername_to(std::span<char, socket::SOCKADDR_STR_LEN>(buf, socket::SOCKADDR_STR_LEN));
} else {
buf[0] = '\0';
}
return buf;
}
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
@@ -113,8 +105,6 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
return APIError::OK; // Already closed
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
@@ -241,6 +231,8 @@ class APIFrameHelper {
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together
uint16_t rx_buf_len_ = 0;

View File

@@ -29,12 +29,7 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -21,12 +21,7 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -192,15 +192,11 @@ void APIServer::loop() {
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
// Save client info before removal for the trigger
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
std::string client_peername(client->get_peername());
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());

View File

@@ -38,8 +38,10 @@ async def to_code(config):
# 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")
# https://github.com/ayushsharma82/RPAsyncTCP
# RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better
# ESPAsyncWebServer compatibility
cg.add_library("ayushsharma82/RPAsyncTCP", "1.3.2")
# Other platforms (host, etc) use socket-based implementation

View File

@@ -8,8 +8,8 @@
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
#include <ESPAsyncTCP.h>
#elif defined(USE_RP2040)
// Use AsyncTCP_RP2040W library for RP2040
#include <AsyncTCP_RP2040W.h>
// Use RPAsyncTCP library for RP2040
#include <RPAsyncTCP.h>
#else
// Use socket-based implementation for other platforms
#include "async_tcp_socket.h"

View File

@@ -13,6 +13,7 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
PlatformFramework,
)
@@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
]
),
@@ -106,6 +108,8 @@ async def to_code(config):
cg.add_library("DNSServer", None)
if CORE.is_libretiny:
cg.add_library("DNSServer", None)
if CORE.is_rp2040:
cg.add_library("DNSServer", None)
# Only compile the ESP-IDF DNS server when using ESP-IDF framework

View File

@@ -12,7 +12,6 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -43,7 +42,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",

View File

@@ -31,6 +31,7 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -213,6 +214,7 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
]
),

View File

@@ -47,5 +47,10 @@ async def to_code(config):
cg.add_library("ESP8266WiFi", None)
if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"])
if CORE.is_rp2040:
# Ignore bundled AsyncTCP libraries - we use RPAsyncTCP from async_tcp component
CORE.add_platformio_option(
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5")

View File

@@ -698,10 +698,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
struct scan_config config {};
memset(&config, 0, sizeof(config));
config.ssid = nullptr;

View File

@@ -14,7 +14,6 @@
#include <algorithm>
#include <cinttypes>
#include <memory>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
@@ -829,29 +828,16 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
uint16_t number = it.number;
scan_result_.init(number);
#ifdef USE_ESP32_HOSTED
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
// Presumably an upstream bug, work-around by getting all records at once
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
if (err != ESP_OK) {
esp_wifi_clear_ap_list();
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t &record = records[i];
#else
// Process one record at a time to avoid large buffer allocation
wifi_ap_record_t record;
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t record;
err = esp_wifi_scan_get_ap_record(&record);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
break;
}
#endif // USE_ESP32_HOSTED
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));

View File

@@ -649,10 +649,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {

View File

@@ -88,6 +88,8 @@ bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); }
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
if (!manual_ip.has_value()) {
// Explicitly enable DHCP by passing all zeros - this resets any previous static IP config
WiFi.config(IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0));
return true;
}
@@ -161,19 +163,18 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
esphome::network::IPAddress ip_address, gateway, subnet, dns;
IPAddress ip_address, gateway, subnet;
if (manual_ip.has_value()) {
ip_address = manual_ip->static_ip;
gateway = manual_ip->gateway;
subnet = manual_ip->subnet;
dns = manual_ip->static_ip;
ip_address = IPAddress(manual_ip->static_ip);
gateway = IPAddress(manual_ip->gateway);
subnet = IPAddress(manual_ip->subnet);
} else {
ip_address = network::IPAddress(192, 168, 4, 1);
gateway = network::IPAddress(192, 168, 4, 1);
subnet = network::IPAddress(255, 255, 255, 0);
dns = network::IPAddress(192, 168, 4, 1);
ip_address = IPAddress(192, 168, 4, 1);
gateway = IPAddress(192, 168, 4, 1);
subnet = IPAddress(255, 255, 255, 0);
}
WiFi.config(ip_address, dns, gateway, subnet);
// Use softAPConfig for AP mode - WiFi.config() would disable DHCP for STA mode
WiFi.softAPConfig(ip_address, gateway, subnet);
return true;
}
@@ -192,12 +193,16 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
}
#endif
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1);
const char *password = ap.get_password().empty() ? nullptr : ap.get_password().c_str();
WiFi.beginAP(ap.get_ssid().c_str(), password, ap.has_channel() ? ap.get_channel() : 1);
return true;
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
struct netif *ap_netif = &cyw43_state.netif[CYW43_ITF_AP];
return {&ap_netif->ip_addr};
}
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() {
@@ -229,20 +234,29 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
network::IPAddresses addresses;
uint8_t index = 0;
for (auto addr : addrList) {
addresses[index++] = addr.ipFromNetifNum();
// Only include addresses from the STA interface (CYW43_ITF_STA = 0)
if (addr.ifnumber() == CYW43_ITF_STA) {
addresses[index++] = addr.ipFromNetifNum();
}
}
return addresses;
}
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_subnet_mask_() {
struct netif *sta_netif = &cyw43_state.netif[CYW43_ITF_STA];
return {&sta_netif->netmask};
}
network::IPAddress WiFiComponent::wifi_gateway_ip_() {
struct netif *sta_netif = &cyw43_state.netif[CYW43_ITF_STA];
return {&sta_netif->gw};
}
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
const ip_addr_t *dns_ip = dns_getserver(num);
return network::IPAddress(dns_ip);
}
void WiFiComponent::wifi_loop_() {
// Handle scan completion
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
// Handle scan completion - only process once when scan finishes
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !this->scan_done_ && !cyw43_wifi_scan_active(&cyw43_state)) {
this->scan_done_ = true;
ESP_LOGV(TAG, "Scan done");
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS

View File

@@ -42,7 +42,6 @@
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
#define USE_FAN

View File

@@ -114,7 +114,7 @@ lib_deps =
ESP8266WiFi ; wifi (Arduino built-in)
Update ; ota (Arduino built-in)
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
makuna/NeoPixelBus@2.7.3 ; neopixelbus
ESP8266HTTPClient ; http_request (Arduino built-in)
ESP8266mDNS ; mdns (Arduino built-in)
@@ -200,8 +200,9 @@ platform_packages =
framework = arduino
lib_deps =
${common:arduino.lib_deps}
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
build_flags =
${common:arduino.build_flags}
-DUSE_RP2040
@@ -217,7 +218,7 @@ framework = arduino
lib_compat_mode = soft
lib_deps =
bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
droscy/esp_wireguard@0.4.2 ; wireguard
build_flags =
${common:arduino.build_flags}

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common_v2.yaml