mirror of
https://github.com/esphome/esphome.git
synced 2026-02-13 03:02:02 +00:00
Compare commits
4 Commits
wifi-memcp
...
beta_preme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09eadc1980 | ||
|
|
50132038a2 | ||
|
|
56ba59a41f | ||
|
|
61746bd4b3 |
@@ -1 +1 @@
|
||||
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
|
||||
74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7
|
||||
|
||||
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -115,7 +115,6 @@ jobs:
|
||||
python-version:
|
||||
- "3.11"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.3.0-dev
|
||||
PROJECT_NUMBER = 2026.2.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
|
||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||
# Order matters! More specific categories must come before general ones.
|
||||
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
||||
"mdns_lib": ["mdns", "packet$"],
|
||||
"mdns_lib": ["mdns"],
|
||||
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
||||
"memory_mgmt": [
|
||||
"mem_",
|
||||
@@ -794,6 +794,7 @@ SYMBOL_PATTERNS = {
|
||||
"s_dp",
|
||||
"s_ni",
|
||||
"s_reg_dump",
|
||||
"packet$",
|
||||
"d_mult_table",
|
||||
"K",
|
||||
"fcstab",
|
||||
|
||||
@@ -295,8 +295,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
|
||||
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
|
||||
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
ProtoVarInt(msg.message_type)
|
||||
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||
|
||||
// Add iovec for this message (header + payload)
|
||||
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
||||
|
||||
@@ -117,7 +117,37 @@ void APIServer::setup() {
|
||||
void APIServer::loop() {
|
||||
// Accept new clients only if the socket exists and has incoming connections
|
||||
if (this->socket_ && this->socket_->ready()) {
|
||||
this->accept_new_connections_();
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// 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_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
@@ -148,84 +178,46 @@ void APIServer::loop() {
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
if (client->flags_.remove) {
|
||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
||||
this->remove_client_(client_index);
|
||||
} else {
|
||||
if (!client->flags_.remove) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
client_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
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];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#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());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::accept_new_connections_() {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// 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_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
// 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());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,11 +234,6 @@ class APIServer : public Component,
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
|
||||
@@ -57,16 +57,6 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Encode a varint directly into a pre-allocated buffer.
|
||||
/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate).
|
||||
inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
|
||||
while (val > 0x7F) {
|
||||
*buffer++ = static_cast<uint8_t>(val | 0x80);
|
||||
val >>= 7;
|
||||
}
|
||||
*buffer = static_cast<uint8_t>(val);
|
||||
}
|
||||
|
||||
/*
|
||||
* StringRef Ownership Model for API Protocol Messages
|
||||
* ===================================================
|
||||
@@ -103,17 +93,17 @@ class ProtoVarInt {
|
||||
ProtoVarInt() : value_(0) {}
|
||||
explicit ProtoVarInt(uint64_t value) : value_(value) {}
|
||||
|
||||
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
|
||||
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(consumed != nullptr);
|
||||
#endif
|
||||
if (len == 0)
|
||||
if (len == 0) {
|
||||
if (consumed != nullptr)
|
||||
*consumed = 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Most common case: single-byte varint (values 0-127)
|
||||
if ((buffer[0] & 0x80) == 0) {
|
||||
*consumed = 1;
|
||||
if (consumed != nullptr)
|
||||
*consumed = 1;
|
||||
return ProtoVarInt(buffer[0]);
|
||||
}
|
||||
|
||||
@@ -132,11 +122,14 @@ class ProtoVarInt {
|
||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||
bitpos += 7;
|
||||
if ((val & 0x80) == 0) {
|
||||
*consumed = i + 1;
|
||||
if (consumed != nullptr)
|
||||
*consumed = i + 1;
|
||||
return ProtoVarInt(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (consumed != nullptr)
|
||||
*consumed = 0;
|
||||
return {}; // Incomplete or invalid varint
|
||||
}
|
||||
|
||||
@@ -160,6 +153,50 @@ class ProtoVarInt {
|
||||
// with ZigZag encoding
|
||||
return decode_zigzag64(this->value_);
|
||||
}
|
||||
/**
|
||||
* Encode the varint value to a pre-allocated buffer without bounds checking.
|
||||
*
|
||||
* @param buffer The pre-allocated buffer to write the encoded varint to
|
||||
* @param len The size of the buffer in bytes
|
||||
*
|
||||
* @note The caller is responsible for ensuring the buffer is large enough
|
||||
* to hold the encoded value. Use ProtoSize::varint() to calculate
|
||||
* the exact size needed before calling this method.
|
||||
* @note No bounds checking is performed for performance reasons.
|
||||
*/
|
||||
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
buffer[0] = val;
|
||||
return;
|
||||
}
|
||||
size_t i = 0;
|
||||
while (val && i < len) {
|
||||
uint8_t temp = val & 0x7F;
|
||||
val >>= 7;
|
||||
if (val) {
|
||||
buffer[i++] = temp | 0x80;
|
||||
} else {
|
||||
buffer[i++] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
void encode(std::vector<uint8_t> &out) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
out.push_back(val);
|
||||
return;
|
||||
}
|
||||
while (val) {
|
||||
uint8_t temp = val & 0x7F;
|
||||
val >>= 7;
|
||||
if (val) {
|
||||
out.push_back(temp | 0x80);
|
||||
} else {
|
||||
out.push_back(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint64_t value_;
|
||||
@@ -219,20 +256,8 @@ class ProtoWriteBuffer {
|
||||
public:
|
||||
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
|
||||
void write(uint8_t value) { this->buffer_->push_back(value); }
|
||||
void encode_varint_raw(uint32_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
}
|
||||
void encode_varint_raw_64(uint64_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
}
|
||||
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
|
||||
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
|
||||
/**
|
||||
* Encode a field key (tag/wire type combination).
|
||||
*
|
||||
@@ -282,13 +307,13 @@ class ProtoWriteBuffer {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
|
||||
this->encode_varint_raw_64(value);
|
||||
this->encode_varint_raw(ProtoVarInt(value));
|
||||
}
|
||||
void encode_bool(uint32_t field_id, bool value, bool force = false) {
|
||||
if (!value && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
||||
this->buffer_->push_back(value ? 0x01 : 0x00);
|
||||
this->write(0x01);
|
||||
}
|
||||
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
@@ -913,15 +938,13 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
|
||||
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
||||
|
||||
// Write the length varint directly
|
||||
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
|
||||
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
|
||||
|
||||
// Now encode the message content - it will append to the buffer
|
||||
value.encode(*this);
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
// Verify that the encoded size matches what we calculated
|
||||
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||
|
||||
@@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
request->send(stream);
|
||||
}
|
||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
const auto &ssid = request->arg("ssid");
|
||||
const auto &psk = request->arg("psk");
|
||||
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
ESP_LOGI(TAG,
|
||||
"Requested WiFi Settings Change:\n"
|
||||
" SSID='%s'\n"
|
||||
@@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
ssid.c_str(), psk.c_str());
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 is single-threaded, call directly
|
||||
wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str());
|
||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
||||
#else
|
||||
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
|
||||
#endif
|
||||
request->redirect(ESPHOME_F("/?save"));
|
||||
}
|
||||
|
||||
@@ -645,12 +645,11 @@ def _is_framework_url(source: str) -> bool:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 7),
|
||||
"latest": cv.Version(3, 3, 7),
|
||||
"dev": cv.Version(3, 3, 7),
|
||||
"recommended": cv.Version(3, 3, 6),
|
||||
"latest": cv.Version(3, 3, 6),
|
||||
"dev": cv.Version(3, 3, 6),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||
@@ -669,7 +668,6 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 7): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
||||
@@ -693,7 +691,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(5, 5, 2),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||
@@ -710,8 +708,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 37),
|
||||
"latest": cv.Version(55, 3, 37),
|
||||
"recommended": cv.Version(55, 3, 36),
|
||||
"latest": cv.Version(55, 3, 36),
|
||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
}
|
||||
|
||||
|
||||
@@ -1686,10 +1686,6 @@ BOARDS = {
|
||||
"name": "Espressif ESP32-C6-DevKitM-1",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"esp32-c61-devkitc1": {
|
||||
"name": "Espressif ESP32-C61-DevKitC-1 (4 MB Flash)",
|
||||
"variant": VARIANT_ESP32C61,
|
||||
},
|
||||
"esp32-c61-devkitc1-n8r2": {
|
||||
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
|
||||
"variant": VARIANT_ESP32C61,
|
||||
@@ -1722,10 +1718,6 @@ BOARDS = {
|
||||
"name": "Espressif ESP32-P4 rev.300 generic",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"esp32-p4_r3-evboard": {
|
||||
"name": "Espressif ESP32-P4 Function EV Board v1.6 (rev.301)",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"esp32-pico-devkitm-2": {
|
||||
"name": "Espressif ESP32-PICO-DevKitM-2",
|
||||
"variant": VARIANT_ESP32,
|
||||
@@ -2562,10 +2554,6 @@ BOARDS = {
|
||||
"name": "XinaBox CW02",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"yb_esp32s3_amp": {
|
||||
"name": "YelloByte YB-ESP32-S3-AMP",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_amp_v2": {
|
||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.2)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2574,10 +2562,6 @@ BOARDS = {
|
||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_dac": {
|
||||
"name": "YelloByte YB-ESP32-S3-DAC",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_drv": {
|
||||
"name": "YelloByte YB-ESP32-S3-DRV",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
|
||||
@@ -338,8 +338,8 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
||||
return;
|
||||
}
|
||||
wifi::WiFiAP sta{};
|
||||
sta.set_ssid(command.ssid.c_str());
|
||||
sta.set_password(command.password.c_str());
|
||||
sta.set_ssid(command.ssid);
|
||||
sta.set_password(command.password);
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
|
||||
@@ -130,16 +130,11 @@ ETHERNET_TYPES = {
|
||||
}
|
||||
|
||||
# PHY types that need compile-time defines for conditional compilation
|
||||
# Each RMII PHY type gets a define so unused PHY drivers are excluded by the linker
|
||||
_PHY_TYPE_TO_DEFINE = {
|
||||
"LAN8720": "USE_ETHERNET_LAN8720",
|
||||
"RTL8201": "USE_ETHERNET_RTL8201",
|
||||
"DP83848": "USE_ETHERNET_DP83848",
|
||||
"IP101": "USE_ETHERNET_IP101",
|
||||
"JL1101": "USE_ETHERNET_JL1101",
|
||||
"KSZ8081": "USE_ETHERNET_KSZ8081",
|
||||
"KSZ8081RNA": "USE_ETHERNET_KSZ8081",
|
||||
"LAN8670": "USE_ETHERNET_LAN8670",
|
||||
# Add other PHY types here only if they need conditional compilation
|
||||
}
|
||||
|
||||
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
|
||||
|
||||
@@ -186,43 +186,31 @@ void EthernetComponent::setup() {
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ETH_USE_ESP32_EMAC
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720: {
|
||||
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
case ETHERNET_TYPE_RTL8201: {
|
||||
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
case ETHERNET_TYPE_DP83848: {
|
||||
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
case ETHERNET_TYPE_IP101: {
|
||||
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_JL1101
|
||||
case ETHERNET_TYPE_JL1101: {
|
||||
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
case ETHERNET_TYPE_KSZ8081RNA: {
|
||||
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670: {
|
||||
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
|
||||
@@ -355,32 +343,26 @@ void EthernetComponent::loop() {
|
||||
void EthernetComponent::dump_config() {
|
||||
const char *eth_type;
|
||||
switch (this->type_) {
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720:
|
||||
eth_type = "LAN8720";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
|
||||
case ETHERNET_TYPE_RTL8201:
|
||||
eth_type = "RTL8201";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
|
||||
case ETHERNET_TYPE_DP83848:
|
||||
eth_type = "DP83848";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
|
||||
case ETHERNET_TYPE_IP101:
|
||||
eth_type = "IP101";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_JL1101
|
||||
|
||||
case ETHERNET_TYPE_JL1101:
|
||||
eth_type = "JL1101";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
eth_type = "KSZ8081";
|
||||
break;
|
||||
@@ -388,22 +370,19 @@ void EthernetComponent::dump_config() {
|
||||
case ETHERNET_TYPE_KSZ8081RNA:
|
||||
eth_type = "KSZ8081RNA";
|
||||
break;
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_W5500
|
||||
|
||||
case ETHERNET_TYPE_W5500:
|
||||
eth_type = "W5500";
|
||||
break;
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_DM9051
|
||||
case ETHERNET_TYPE_DM9051:
|
||||
eth_type = "DM9051";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_OPENETH
|
||||
|
||||
case ETHERNET_TYPE_OPENETH:
|
||||
eth_type = "OPENETH";
|
||||
break;
|
||||
#endif
|
||||
|
||||
case ETHERNET_TYPE_DM9051:
|
||||
eth_type = "DM9051";
|
||||
break;
|
||||
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670:
|
||||
eth_type = "LAN8670";
|
||||
@@ -707,22 +686,16 @@ void EthernetComponent::dump_connect_params_() {
|
||||
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
" Hostname: '%s'\n"
|
||||
" Subnet: %s\n"
|
||||
" Gateway: %s\n"
|
||||
" DNS1: %s\n"
|
||||
" DNS2: %s\n"
|
||||
" MAC Address: %s\n"
|
||||
" Is Full Duplex: %s\n"
|
||||
" Link Speed: %u",
|
||||
" DNS2: %s",
|
||||
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
|
||||
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
|
||||
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
|
||||
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
|
||||
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf));
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
@@ -733,6 +706,14 @@ void EthernetComponent::dump_connect_params_() {
|
||||
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" MAC Address: %s\n"
|
||||
" Is Full Duplex: %s\n"
|
||||
" Link Speed: %u",
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
|
||||
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
@@ -856,15 +837,13 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
|
||||
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
|
||||
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Writing to PHY Register Address: 0x%02" PRIX32 "\n"
|
||||
@@ -873,13 +852,11 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
|
||||
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -110,8 +110,6 @@ class EthernetComponent : public Component {
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string get_eth_mac_address_pretty();
|
||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
eth_duplex_t get_duplex_mode();
|
||||
|
||||
@@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
switch (command.command) {
|
||||
case improv::WIFI_SETTINGS: {
|
||||
wifi::WiFiAP sta{};
|
||||
sta.set_ssid(command.ssid.c_str());
|
||||
sta.set_password(command.password.c_str());
|
||||
sta.set_ssid(command.ssid);
|
||||
sta.set_password(command.password);
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const char *ssid_cstr = scan.get_ssid().c_str();
|
||||
// Check if we've already sent this SSID
|
||||
bool duplicate = false;
|
||||
for (const auto &seen : networks) {
|
||||
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
||||
continue;
|
||||
// Only allocate std::string after confirming it's not a duplicate
|
||||
std::string ssid(ssid_cstr);
|
||||
// Send each ssid separately to avoid overflowing the buffer
|
||||
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
||||
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
||||
std::vector<uint8_t> data =
|
||||
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
||||
this->send_response_(data);
|
||||
networks.push_back(std::move(ssid));
|
||||
networks.push_back(ssid);
|
||||
}
|
||||
// Send empty response to signify the end of the list.
|
||||
std::vector<uint8_t> data =
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
from esphome.components.mipi import (
|
||||
ETMOD,
|
||||
FRMCTR2,
|
||||
GMCTRN1,
|
||||
GMCTRP1,
|
||||
IFCTR,
|
||||
MODE_RGB,
|
||||
PWCTR1,
|
||||
PWCTR3,
|
||||
PWCTR4,
|
||||
PWCTR5,
|
||||
PWSET,
|
||||
DriverChip,
|
||||
)
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .amoled import CO5300
|
||||
@@ -142,16 +129,6 @@ DriverChip(
|
||||
),
|
||||
),
|
||||
)
|
||||
ST7789P = DriverChip(
|
||||
"ST7789P",
|
||||
# Max supported dimensions
|
||||
width=240,
|
||||
height=320,
|
||||
# SPI: RGB layout
|
||||
color_order=MODE_RGB,
|
||||
invert_colors=True,
|
||||
draw_rounding=1,
|
||||
)
|
||||
|
||||
ILI9488_A.extend(
|
||||
"PICO-RESTOUCH-LCD-3.5",
|
||||
@@ -185,61 +162,3 @@ AXS15231.extend(
|
||||
cs_pin=9,
|
||||
reset_pin=21,
|
||||
)
|
||||
|
||||
# Waveshare 1.83-v2
|
||||
#
|
||||
# Do not use on 1.83-v1: Vendor warning on different chip!
|
||||
ST7789P.extend(
|
||||
"WAVESHARE-1.83-V2",
|
||||
# Panel size smaller than ST7789 max allowed
|
||||
width=240,
|
||||
height=284,
|
||||
# Vendor specific init derived from vendor sample code
|
||||
# "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp"
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
initsequence=(
|
||||
(FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33),
|
||||
(ETMOD, 0x35),
|
||||
(0xBB, 0x19),
|
||||
(PWCTR1, 0x2C),
|
||||
(PWCTR3, 0x01),
|
||||
(PWCTR4, 0x12),
|
||||
(PWCTR5, 0x20),
|
||||
(IFCTR, 0x0F),
|
||||
(PWSET, 0xA4, 0xA1),
|
||||
(
|
||||
GMCTRP1,
|
||||
0xD0,
|
||||
0x04,
|
||||
0x0D,
|
||||
0x11,
|
||||
0x13,
|
||||
0x2B,
|
||||
0x3F,
|
||||
0x54,
|
||||
0x4C,
|
||||
0x18,
|
||||
0x0D,
|
||||
0x0B,
|
||||
0x1F,
|
||||
0x23,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0xD0,
|
||||
0x04,
|
||||
0x0C,
|
||||
0x11,
|
||||
0x13,
|
||||
0x2C,
|
||||
0x3F,
|
||||
0x44,
|
||||
0x51,
|
||||
0x2F,
|
||||
0x1F,
|
||||
0x1F,
|
||||
0x20,
|
||||
0x23,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ void OpenThreadComponent::ot_main() {
|
||||
esp_cli_custom_command_init();
|
||||
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
||||
|
||||
otLinkModeConfig link_mode_config{};
|
||||
otLinkModeConfig link_mode_config = {0};
|
||||
#if CONFIG_OPENTHREAD_FTD
|
||||
link_mode_config.mRxOnWhenIdle = true;
|
||||
link_mode_config.mDeviceType = true;
|
||||
|
||||
@@ -38,7 +38,8 @@ void PulseMeterSensor::setup() {
|
||||
}
|
||||
|
||||
void PulseMeterSensor::loop() {
|
||||
State state;
|
||||
// Reset the count in get before we pass it back to the ISR as set
|
||||
this->get_->count_ = 0;
|
||||
|
||||
{
|
||||
// Lock the interrupt so the interrupt code doesn't interfere with itself
|
||||
@@ -57,35 +58,31 @@ void PulseMeterSensor::loop() {
|
||||
}
|
||||
this->last_pin_val_ = current;
|
||||
|
||||
// Get the latest state from the ISR and reset the count in the ISR
|
||||
state.last_detected_edge_us_ = this->state_.last_detected_edge_us_;
|
||||
state.last_rising_edge_us_ = this->state_.last_rising_edge_us_;
|
||||
state.count_ = this->state_.count_;
|
||||
this->state_.count_ = 0;
|
||||
// Swap out set and get to get the latest state from the ISR
|
||||
std::swap(this->set_, this->get_);
|
||||
}
|
||||
|
||||
const uint32_t now = micros();
|
||||
|
||||
// If an edge was peeked, repay the debt
|
||||
if (this->peeked_edge_ && state.count_ > 0) {
|
||||
if (this->peeked_edge_ && this->get_->count_ > 0) {
|
||||
this->peeked_edge_ = false;
|
||||
state.count_--;
|
||||
this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early.
|
||||
// Wait for the debt to be repaid before counting another unprocessed edge early.
|
||||
if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ &&
|
||||
now - state.last_rising_edge_us_ >= this->filter_us_) {
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early
|
||||
if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ &&
|
||||
now - this->get_->last_rising_edge_us_ >= this->filter_us_) {
|
||||
this->peeked_edge_ = true;
|
||||
state.last_detected_edge_us_ = state.last_rising_edge_us_;
|
||||
state.count_++;
|
||||
this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_;
|
||||
this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// Check if we detected a pulse this loop
|
||||
if (state.count_ > 0) {
|
||||
if (this->get_->count_ > 0) {
|
||||
// Keep a running total of pulses if a total sensor is configured
|
||||
if (this->total_sensor_ != nullptr) {
|
||||
this->total_pulses_ += state.count_;
|
||||
this->total_pulses_ += this->get_->count_;
|
||||
const uint32_t total = this->total_pulses_;
|
||||
this->total_sensor_->publish_state(total);
|
||||
}
|
||||
@@ -97,15 +94,15 @@ void PulseMeterSensor::loop() {
|
||||
this->meter_state_ = MeterState::RUNNING;
|
||||
} break;
|
||||
case MeterState::RUNNING: {
|
||||
uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(state.count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_,
|
||||
pulse_width_us);
|
||||
uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(this->get_->count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us,
|
||||
this->get_->count_, pulse_width_us);
|
||||
this->publish_state((60.0f * 1000000.0f) / pulse_width_us);
|
||||
} break;
|
||||
}
|
||||
|
||||
this->last_processed_edge_us_ = state.last_detected_edge_us_;
|
||||
this->last_processed_edge_us_ = this->get_->last_detected_edge_us_;
|
||||
}
|
||||
// No detected edges this loop
|
||||
else {
|
||||
@@ -144,14 +141,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) {
|
||||
// This is an interrupt handler - we can't call any virtual method from this method
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
auto &edge_state = sensor->edge_state_;
|
||||
auto &state = sensor->state_;
|
||||
auto &state = sensor->edge_state_;
|
||||
auto &set = *sensor->set_;
|
||||
|
||||
if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
edge_state.last_sent_edge_us_ = now;
|
||||
state.last_detected_edge_us_ = now;
|
||||
state.last_rising_edge_us_ = now;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
state.last_sent_edge_us_ = now;
|
||||
set.last_detected_edge_us_ = now;
|
||||
set.last_rising_edge_us_ = now;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// This ISR is bound to rising edges, so the pin is high
|
||||
@@ -163,26 +160,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) {
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
const bool pin_val = sensor->isr_pin_.digital_read();
|
||||
auto &pulse_state = sensor->pulse_state_;
|
||||
auto &state = sensor->state_;
|
||||
auto &state = sensor->pulse_state_;
|
||||
auto &set = *sensor->set_;
|
||||
|
||||
// Filter length has passed since the last interrupt
|
||||
const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_;
|
||||
const bool length = now - state.last_intr_ >= sensor->filter_us_;
|
||||
|
||||
if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
pulse_state.latched_ = false;
|
||||
} else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
pulse_state.latched_ = true;
|
||||
state.last_detected_edge_us_ = pulse_state.last_intr_;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
state.latched_ = false;
|
||||
} else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
state.latched_ = true;
|
||||
set.last_detected_edge_us_ = state.last_intr_;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// Due to order of operations this includes
|
||||
// length && latched && rising (just reset from a long low edge)
|
||||
// !latched && (rising || high) (noise on the line resetting the potential rising edge)
|
||||
state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_;
|
||||
set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_;
|
||||
|
||||
pulse_state.last_intr_ = now;
|
||||
state.last_intr_ = now;
|
||||
sensor->last_pin_val_ = pin_val;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,16 +46,17 @@ class PulseMeterSensor : public sensor::Sensor, public Component {
|
||||
uint32_t total_pulses_ = 0;
|
||||
uint32_t last_processed_edge_us_ = 0;
|
||||
|
||||
// This struct and variable are used to pass data between the ISR and loop.
|
||||
// The data from state_ is read and then count_ in state_ is reset in each loop.
|
||||
// This must be done while guarded by an InterruptLock. Use this variable to send data
|
||||
// from the ISR to the loop not the other way around (except for resetting count_).
|
||||
// This struct (and the two pointers) are used to pass data between the ISR and loop.
|
||||
// These two pointers are exchanged each loop.
|
||||
// Use these to send data from the ISR to the loop not the other way around (except for resetting the values).
|
||||
struct State {
|
||||
uint32_t last_detected_edge_us_ = 0;
|
||||
uint32_t last_rising_edge_us_ = 0;
|
||||
uint32_t count_ = 0;
|
||||
};
|
||||
volatile State state_{};
|
||||
State state_[2];
|
||||
volatile State *set_ = state_;
|
||||
volatile State *get_ = state_ + 1;
|
||||
|
||||
// Only use the following variables in the ISR or while guarded by an InterruptLock
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
|
||||
@@ -16,13 +16,19 @@ namespace esphome::socket {
|
||||
|
||||
class BSDSocketImpl final : public Socket {
|
||||
public:
|
||||
BSDSocketImpl(int fd, bool monitor_loop = false) {
|
||||
this->fd_ = fd;
|
||||
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Register new socket with the application for select() if monitoring requested
|
||||
if (monitor_loop && this->fd_ >= 0) {
|
||||
// Only set loop_monitored_ to true if registration succeeds
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
} else {
|
||||
this->loop_monitored_ = false;
|
||||
}
|
||||
#else
|
||||
// Without select support, ignore monitor_loop parameter
|
||||
(void) monitor_loop;
|
||||
#endif
|
||||
}
|
||||
~BSDSocketImpl() override {
|
||||
if (!this->closed_) {
|
||||
@@ -46,10 +52,12 @@ class BSDSocketImpl final : public Socket {
|
||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
|
||||
int close() override {
|
||||
if (!this->closed_) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Unregister from select() before closing if monitored
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = ::close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
@@ -122,6 +130,23 @@ class BSDSocketImpl final : public Socket {
|
||||
::fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_fd() const override { return this->fd_; }
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool ready() const override {
|
||||
if (!this->loop_monitored_)
|
||||
return true;
|
||||
return App.is_socket_ready(this->fd_);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
int fd_;
|
||||
bool closed_{false};
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
// Helper to create a socket with optional monitoring
|
||||
|
||||
@@ -452,8 +452,6 @@ class LWIPRawImpl : public Socket {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
|
||||
|
||||
int setblocking(bool blocking) final {
|
||||
if (pcb_ == nullptr) {
|
||||
errno = ECONNRESET;
|
||||
@@ -578,8 +576,6 @@ class LWIPRawListenImpl final : public LWIPRawImpl {
|
||||
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
|
||||
}
|
||||
|
||||
bool ready() const override { return this->accepted_socket_count_ > 0; }
|
||||
|
||||
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
if (pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
|
||||
@@ -11,13 +11,19 @@ namespace esphome::socket {
|
||||
|
||||
class LwIPSocketImpl final : public Socket {
|
||||
public:
|
||||
LwIPSocketImpl(int fd, bool monitor_loop = false) {
|
||||
this->fd_ = fd;
|
||||
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Register new socket with the application for select() if monitoring requested
|
||||
if (monitor_loop && this->fd_ >= 0) {
|
||||
// Only set loop_monitored_ to true if registration succeeds
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
} else {
|
||||
this->loop_monitored_ = false;
|
||||
}
|
||||
#else
|
||||
// Without select support, ignore monitor_loop parameter
|
||||
(void) monitor_loop;
|
||||
#endif
|
||||
}
|
||||
~LwIPSocketImpl() override {
|
||||
if (!this->closed_) {
|
||||
@@ -43,10 +49,12 @@ class LwIPSocketImpl final : public Socket {
|
||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
|
||||
int close() override {
|
||||
if (!this->closed_) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Unregister from select() before closing if monitored
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = lwip_close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
@@ -89,6 +97,23 @@ class LwIPSocketImpl final : public Socket {
|
||||
lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_fd() const override { return this->fd_; }
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool ready() const override {
|
||||
if (!this->loop_monitored_)
|
||||
return true;
|
||||
return App.is_socket_ready(this->fd_);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
int fd_;
|
||||
bool closed_{false};
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
// Helper to create a socket with optional monitoring
|
||||
|
||||
@@ -10,10 +10,6 @@ namespace esphome::socket {
|
||||
|
||||
Socket::~Socket() {}
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
|
||||
#endif
|
||||
|
||||
// Platform-specific inet_ntop wrappers
|
||||
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value
|
||||
|
||||
@@ -63,29 +63,13 @@ class Socket {
|
||||
virtual int setblocking(bool blocking) = 0;
|
||||
virtual int loop() { return 0; };
|
||||
|
||||
/// Get the underlying file descriptor (returns -1 if not supported)
|
||||
/// Non-virtual: only one socket implementation is active per build.
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
int get_fd() const { return this->fd_; }
|
||||
#else
|
||||
int get_fd() const { return -1; }
|
||||
#endif
|
||||
/// Get the underlying file descriptor (returns -1 if not supported)
|
||||
virtual int get_fd() const { return -1; }
|
||||
|
||||
/// Check if socket has data ready to read
|
||||
/// For select()-based sockets: non-virtual, checks Application's select() results
|
||||
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool ready() const;
|
||||
#else
|
||||
/// For loop-monitored sockets, checks with the Application's select() results
|
||||
/// For non-monitored sockets, always returns true (assumes data may be available)
|
||||
virtual bool ready() const { return true; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
int fd_{-1};
|
||||
bool closed_{false};
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Create a socket of the given domain, type and protocol.
|
||||
|
||||
@@ -90,6 +90,7 @@ void IDFUARTComponent::setup() {
|
||||
return;
|
||||
}
|
||||
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
|
||||
this->lock_ = xSemaphoreCreateMutex();
|
||||
|
||||
#if (SOC_UART_LP_NUM >= 1)
|
||||
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
|
||||
@@ -101,7 +102,11 @@ void IDFUARTComponent::setup() {
|
||||
this->rx_buffer_size_ = fifo_len * 2;
|
||||
}
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
|
||||
this->load_settings(false);
|
||||
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
@@ -121,20 +126,13 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
constexpr int event_queue_size = 20;
|
||||
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
|
||||
#else
|
||||
constexpr int event_queue_size = 0;
|
||||
QueueHandle_t *event_queue_ptr = nullptr;
|
||||
#endif
|
||||
err = uart_driver_install(this->uart_num_, // UART number
|
||||
this->rx_buffer_size_, // RX ring buffer size
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
event_queue_size, // event queue size/depth
|
||||
event_queue_ptr, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
20, // event queue size/depth
|
||||
&this->uart_event_queue_, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
||||
@@ -284,7 +282,9 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
|
||||
}
|
||||
|
||||
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
|
||||
xSemaphoreGive(this->lock_);
|
||||
if (write_len != (int32_t) len) {
|
||||
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
|
||||
this->mark_failed();
|
||||
@@ -299,6 +299,7 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
if (!this->check_read_timeout_())
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
*data = this->peek_byte_;
|
||||
} else {
|
||||
@@ -310,6 +311,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
this->peek_byte_ = *data;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(this->lock_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -318,6 +320,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
int32_t read_len = 0;
|
||||
if (!this->check_read_timeout_(len))
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
length_to_read--;
|
||||
*data = this->peek_byte_;
|
||||
@@ -326,6 +329,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
}
|
||||
if (length_to_read > 0)
|
||||
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
xSemaphoreGive(this->lock_);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||
@@ -338,7 +342,9 @@ size_t IDFUARTComponent::available() {
|
||||
size_t available = 0;
|
||||
esp_err_t err;
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
err = uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
xSemaphoreGive(this->lock_);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
|
||||
@@ -352,7 +358,9 @@ size_t IDFUARTComponent::available() {
|
||||
|
||||
void IDFUARTComponent::flush() {
|
||||
ESP_LOGVV(TAG, " Flushing");
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
uart_wait_tx_done(this->uart_num_, portMAX_DELAY);
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::check_logger_conflict() {}
|
||||
@@ -376,13 +384,6 @@ void IDFUARTComponent::start_rx_event_task_() {
|
||||
ESP_LOGV(TAG, "RX event task started");
|
||||
}
|
||||
|
||||
// FreeRTOS task that relays UART ISR events to the main loop.
|
||||
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
|
||||
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
|
||||
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
|
||||
// is done by the main loop. This task only reads from the event queue and
|
||||
// calls App.wake_loop_threadsafe().
|
||||
void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
auto *self = static_cast<IDFUARTComponent *>(param);
|
||||
uart_event_t event;
|
||||
@@ -404,14 +405,8 @@ void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
|
||||
case UART_FIFO_OVF:
|
||||
case UART_BUFFER_FULL:
|
||||
// Don't call uart_flush_input() here — this task does not own the read side.
|
||||
// ESP-IDF examples flush on overflow because the same task handles both events
|
||||
// and reads, so flush and read are serialized. Here, reads happen on the main
|
||||
// loop, so flushing from this task races with read_array() and can destroy data
|
||||
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
|
||||
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
|
||||
// into the ring buffer and re-enables RX interrupts once space is freed.
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing");
|
||||
uart_flush_input(self->uart_num_);
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
|
||||
@@ -8,13 +8,6 @@
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
/// ESP-IDF UART driver wrapper.
|
||||
///
|
||||
/// Thread safety: All public methods must only be called from the main loop.
|
||||
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
|
||||
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
|
||||
/// (when enabled) must not call any of these methods — it communicates with the
|
||||
/// main loop exclusively via App.wake_loop_threadsafe().
|
||||
class IDFUARTComponent : public UARTComponent, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -33,9 +26,7 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
void flush() override;
|
||||
|
||||
uint8_t get_hw_serial_number() { return this->uart_num_; }
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Load the UART with the current settings.
|
||||
@@ -55,20 +46,18 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
protected:
|
||||
void check_logger_conflict() override;
|
||||
uart_port_t uart_num_;
|
||||
QueueHandle_t uart_event_queue_;
|
||||
uart_config_t get_config_();
|
||||
SemaphoreHandle_t lock_;
|
||||
|
||||
bool has_peek_{false};
|
||||
uint8_t peek_byte_;
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// RX notification support — runs on a separate FreeRTOS task.
|
||||
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
|
||||
// event queue and call App.wake_loop_threadsafe().
|
||||
// RX notification support
|
||||
void start_rx_event_task_();
|
||||
static void rx_event_task_func(void *param);
|
||||
|
||||
QueueHandle_t uart_event_queue_;
|
||||
TaskHandle_t rx_event_task_handle_{nullptr};
|
||||
#endif // USE_UART_WAKE_LOOP_ON_RX
|
||||
};
|
||||
|
||||
@@ -557,9 +557,7 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
||||
root[ESPHOME_F("device")] = device_name;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ENTITY_ICON
|
||||
root[ESPHOME_F("icon")] = obj->get_icon_ref();
|
||||
#endif
|
||||
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
|
||||
bool is_disabled = obj->is_disabled_by_default();
|
||||
if (is_disabled)
|
||||
@@ -585,7 +583,8 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
|
||||
|
||||
// Helper to get request detail parameter
|
||||
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
||||
return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
|
||||
auto *param = request->getParam(ESPHOME_F("detail"));
|
||||
return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE;
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
@@ -862,10 +861,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
}
|
||||
auto call = is_on ? obj->turn_on() : obj->turn_off();
|
||||
|
||||
parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
|
||||
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
|
||||
|
||||
if (request->hasArg(ESPHOME_F("oscillation"))) {
|
||||
auto speed = request->arg(ESPHOME_F("oscillation"));
|
||||
if (request->hasParam(ESPHOME_F("oscillation"))) {
|
||||
auto speed = request->getParam(ESPHOME_F("oscillation"))->value();
|
||||
auto val = parse_on_off(speed.c_str());
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
@@ -1041,14 +1040,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) ||
|
||||
(request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
|
||||
if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) ||
|
||||
(request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
|
||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1107,7 +1106,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1175,13 +1174,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
const auto &value = request->arg(ESPHOME_F("value"));
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
call.set_date(value.c_str(), value.length());
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1236,13 +1234,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
const auto &value = request->arg(ESPHOME_F("value"));
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
call.set_time(value.c_str(), value.length());
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1296,13 +1293,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
const auto &value = request->arg(ESPHOME_F("value"));
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
call.set_datetime(value.c_str(), value.length());
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1481,14 +1477,10 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
|
||||
|
||||
// Parse temperature parameters
|
||||
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
|
||||
using ClimateCall = decltype(call);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||
&decltype(call)::set_target_temperature_high);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1729,12 +1721,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) {
|
||||
if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1878,12 +1870,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
|
||||
|
||||
// Parse temperature parameters
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_low);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_high);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_low);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_high);
|
||||
|
||||
// Parse away mode parameter
|
||||
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
|
||||
@@ -1987,16 +1979,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
auto call = obj->make_call();
|
||||
|
||||
// Parse carrier frequency (optional)
|
||||
{
|
||||
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("carrier_frequency")).c_str());
|
||||
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_carrier_frequency(*value);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse repeat count (optional, defaults to 1)
|
||||
{
|
||||
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
|
||||
if (request->hasParam(ESPHOME_F("repeat_count"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_repeat_count(*value);
|
||||
}
|
||||
@@ -2004,12 +1996,18 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
// Parse base64url-encoded raw timings (required)
|
||||
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
|
||||
const auto &data_arg = request->arg(ESPHOME_F("data"));
|
||||
if (!request->hasParam(ESPHOME_F("data"))) {
|
||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
|
||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
||||
std::string encoded =
|
||||
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
|
||||
// Validate base64url is not empty
|
||||
if (encoded.empty()) {
|
||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2017,7 +2015,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
|
||||
// must remain valid until perform() completes.
|
||||
// ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
|
||||
this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
|
||||
this->defer([call, encoded = std::move(encoded)]() mutable {
|
||||
call.set_raw_timings_base64url(encoded);
|
||||
call.perform();
|
||||
});
|
||||
|
||||
@@ -513,9 +513,11 @@ class WebServer : public Controller,
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
|
||||
float scale = 1.0f) {
|
||||
auto value = parse_number<float>(request->arg(param_name).c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value / scale);
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value / scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,19 +525,34 @@ class WebServer : public Controller,
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
|
||||
auto value = parse_number<uint32_t>(request->arg(param_name).c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value * scale);
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Generic helper to parse and apply a numeric parameter
|
||||
template<typename NumT, typename T, typename Ret>
|
||||
void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) {
|
||||
auto value = parse_number<NumT>(request->arg(param_name).c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
// Generic helper to parse and apply a float parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic helper to parse and apply an int parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,9 +560,10 @@ class WebServer : public Controller,
|
||||
template<typename T, typename Ret>
|
||||
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||
Ret (T::*setter)(const std::string &)) {
|
||||
if (request->hasArg(param_name)) {
|
||||
const auto &value = request->arg(param_name);
|
||||
(call.*setter)(std::string(value.c_str(), value.length()));
|
||||
if (request->hasParam(param_name)) {
|
||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
||||
std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
(call.*setter)(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,9 +573,8 @@ class WebServer : public Controller,
|
||||
// Invalid values are ignored (setter not called)
|
||||
template<typename T, typename Ret>
|
||||
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
|
||||
const auto ¶m_value = request->arg(param_name);
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (param_value.length() > 0) { // NOLINT(readability-container-size-empty)
|
||||
if (request->hasParam(param_name)) {
|
||||
auto param_value = request->getParam(param_name)->value();
|
||||
// First check on/off (default), then true/false (custom)
|
||||
auto val = parse_on_off(param_value.c_str());
|
||||
if (val == PARSE_NONE) {
|
||||
|
||||
@@ -54,15 +54,14 @@ size_t MultipartReader::parse(const char *data, size_t len) {
|
||||
|
||||
void MultipartReader::process_header_(const char *value, size_t length) {
|
||||
// Process the completed header (field + value pair)
|
||||
const char *field = current_header_field_.c_str();
|
||||
size_t field_len = current_header_field_.length();
|
||||
std::string value_str(value, length);
|
||||
|
||||
if (str_startswith_case_insensitive(field, field_len, "content-disposition")) {
|
||||
if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) {
|
||||
// Parse name and filename from Content-Disposition
|
||||
extract_header_param(value, length, "name", current_part_.name);
|
||||
extract_header_param(value, length, "filename", current_part_.filename);
|
||||
} else if (str_startswith_case_insensitive(field, field_len, "content-type")) {
|
||||
str_trim(value, length, current_part_.content_type);
|
||||
current_part_.name = extract_header_param(value_str, "name");
|
||||
current_part_.filename = extract_header_param(value_str, "filename");
|
||||
} else if (str_startswith_case_insensitive(current_header_field_, "content-type")) {
|
||||
current_part_.content_type = str_trim(value_str);
|
||||
}
|
||||
|
||||
// Clear field for next header
|
||||
@@ -108,29 +107,25 @@ int MultipartReader::on_part_data_end(multipart_parser *parser) {
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
// Case-insensitive string prefix check
|
||||
bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix) {
|
||||
size_t prefix_len = strlen(prefix);
|
||||
if (str_len < prefix_len) {
|
||||
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) {
|
||||
if (str.length() < prefix.length()) {
|
||||
return false;
|
||||
}
|
||||
return str_ncmp_ci(str, prefix, prefix_len);
|
||||
return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length());
|
||||
}
|
||||
|
||||
// Extract a parameter value from a header line
|
||||
// Handles both quoted and unquoted values
|
||||
// Assigns to out if found, clears out otherwise
|
||||
void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out) {
|
||||
size_t param_len = strlen(param);
|
||||
std::string extract_header_param(const std::string &header, const std::string ¶m) {
|
||||
size_t search_pos = 0;
|
||||
|
||||
while (search_pos < header_len) {
|
||||
while (search_pos < header.length()) {
|
||||
// Look for param name
|
||||
const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param);
|
||||
const char *found = stristr(header.c_str() + search_pos, param.c_str());
|
||||
if (!found) {
|
||||
out.clear();
|
||||
return;
|
||||
return "";
|
||||
}
|
||||
size_t pos = found - header;
|
||||
size_t pos = found - header.c_str();
|
||||
|
||||
// Check if this is a word boundary (not part of another parameter)
|
||||
if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') {
|
||||
@@ -139,14 +134,14 @@ void extract_header_param(const char *header, size_t header_len, const char *par
|
||||
}
|
||||
|
||||
// Move past param name
|
||||
pos += param_len;
|
||||
pos += param.length();
|
||||
|
||||
// Skip whitespace and find '='
|
||||
while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos >= header_len || header[pos] != '=') {
|
||||
if (pos >= header.length() || header[pos] != '=') {
|
||||
search_pos = pos;
|
||||
continue;
|
||||
}
|
||||
@@ -154,39 +149,36 @@ void extract_header_param(const char *header, size_t header_len, const char *par
|
||||
pos++; // Skip '='
|
||||
|
||||
// Skip whitespace after '='
|
||||
while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos >= header_len) {
|
||||
out.clear();
|
||||
return;
|
||||
if (pos >= header.length()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Check if value is quoted
|
||||
if (header[pos] == '"') {
|
||||
pos++;
|
||||
const char *end = static_cast<const char *>(memchr(header + pos, '"', header_len - pos));
|
||||
if (end) {
|
||||
out.assign(header + pos, end - (header + pos));
|
||||
return;
|
||||
size_t end = header.find('"', pos);
|
||||
if (end != std::string::npos) {
|
||||
return header.substr(pos, end - pos);
|
||||
}
|
||||
// Malformed - no closing quote
|
||||
out.clear();
|
||||
return;
|
||||
return "";
|
||||
}
|
||||
|
||||
// Unquoted value - find the end (semicolon, comma, or end of string)
|
||||
size_t end = pos;
|
||||
while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') {
|
||||
while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' &&
|
||||
header[end] != '\t') {
|
||||
end++;
|
||||
}
|
||||
|
||||
out.assign(header + pos, end - pos);
|
||||
return;
|
||||
return header.substr(pos, end - pos);
|
||||
}
|
||||
|
||||
out.clear();
|
||||
return "";
|
||||
}
|
||||
|
||||
// Parse boundary from Content-Type header
|
||||
@@ -197,15 +189,13 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t content_type_len = strlen(content_type);
|
||||
|
||||
// Check for multipart/form-data (case-insensitive)
|
||||
if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) {
|
||||
if (!stristr(content_type, "multipart/form-data")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for boundary parameter
|
||||
const char *b = strcasestr_n(content_type, content_type_len, "boundary=");
|
||||
const char *b = stristr(content_type, "boundary=");
|
||||
if (!b) {
|
||||
return false;
|
||||
}
|
||||
@@ -248,15 +238,14 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
|
||||
return true;
|
||||
}
|
||||
|
||||
// Trim whitespace from both ends, assign result to out
|
||||
void str_trim(const char *str, size_t len, std::string &out) {
|
||||
const char *start = str;
|
||||
const char *end = str + len;
|
||||
while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n'))
|
||||
start++;
|
||||
while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n'))
|
||||
end--;
|
||||
out.assign(start, end - start);
|
||||
// Trim whitespace from both ends of a string
|
||||
std::string str_trim(const std::string &str) {
|
||||
size_t start = str.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
size_t end = str.find_last_not_of(" \t\r\n");
|
||||
return str.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
|
||||
@@ -66,20 +66,19 @@ class MultipartReader {
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
// Case-insensitive string prefix check
|
||||
bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix);
|
||||
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix);
|
||||
|
||||
// Extract a parameter value from a header line
|
||||
// Handles both quoted and unquoted values
|
||||
// Assigns to out if found, clears out otherwise
|
||||
void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out);
|
||||
std::string extract_header_param(const std::string &header, const std::string ¶m);
|
||||
|
||||
// Parse boundary from Content-Type header
|
||||
// Returns true if boundary found, false otherwise
|
||||
// boundary_start and boundary_len will point to the boundary value
|
||||
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len);
|
||||
|
||||
// Trim whitespace from both ends, assign result to out
|
||||
void str_trim(const char *str, size_t len, std::string &out);
|
||||
// Trim whitespace from both ends of a string
|
||||
std::string str_trim(const std::string &str);
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "http_parser.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
|
||||
static const char *const TAG = "web_server_idf_utils";
|
||||
|
||||
size_t url_decode(char *str) {
|
||||
char *start = str;
|
||||
char *ptr = str, buf;
|
||||
@@ -50,15 +54,32 @@ optional<std::string> request_get_header(httpd_req_t *req, const char *name) {
|
||||
return {str};
|
||||
}
|
||||
|
||||
optional<std::string> request_get_url_query(httpd_req_t *req) {
|
||||
auto len = httpd_req_get_url_query_len(req);
|
||||
if (len == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string str;
|
||||
str.resize(len);
|
||||
|
||||
auto res = httpd_req_get_url_query_str(req, &str[0], len + 1);
|
||||
if (res != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res));
|
||||
return {};
|
||||
}
|
||||
|
||||
return {str};
|
||||
}
|
||||
|
||||
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
|
||||
if (query_url == nullptr || query_len == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Value can't exceed query_len. Use small stack buffer for typical values,
|
||||
// heap fallback for long ones (e.g. base64 IR data) to limit stack usage
|
||||
// since callers may also have stack buffers for the query string.
|
||||
SmallBufferWithHeapFallback<128, char> val(query_len);
|
||||
// Use stack buffer for typical query strings, heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256, char> val(query_len);
|
||||
|
||||
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
|
||||
return {};
|
||||
}
|
||||
@@ -67,18 +88,6 @@ optional<std::string> query_key_value(const char *query_url, size_t query_len, c
|
||||
return {val.get()};
|
||||
}
|
||||
|
||||
bool query_has_key(const char *query_url, size_t query_len, const char *key) {
|
||||
if (query_url == nullptr || query_len == 0) {
|
||||
return false;
|
||||
}
|
||||
// Minimal buffer — we only care if the key exists, not the value
|
||||
char buf[1];
|
||||
// httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found
|
||||
// but value truncated (expected with 1-byte buffer), or other errors for invalid input
|
||||
auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf));
|
||||
return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC;
|
||||
}
|
||||
|
||||
// Helper function for case-insensitive string region comparison
|
||||
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
@@ -89,8 +98,8 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bounded case-insensitive string search (like strcasestr but length-bounded)
|
||||
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) {
|
||||
// Case-insensitive string search (like strstr but case-insensitive)
|
||||
const char *stristr(const char *haystack, const char *needle) {
|
||||
if (!haystack) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -100,12 +109,7 @@ const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *
|
||||
return haystack;
|
||||
}
|
||||
|
||||
if (haystack_len < needle_len) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *end = haystack + haystack_len - needle_len + 1;
|
||||
for (const char *p = haystack; p < end; p++) {
|
||||
for (const char *p = haystack; *p; p++) {
|
||||
if (str_ncmp_ci(p, needle, needle_len)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ size_t url_decode(char *str);
|
||||
|
||||
bool request_has_header(httpd_req_t *req, const char *name);
|
||||
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
|
||||
optional<std::string> request_get_url_query(httpd_req_t *req);
|
||||
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
|
||||
bool query_has_key(const char *query_url, size_t query_len, const char *key);
|
||||
inline optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
|
||||
return query_key_value(query_url.c_str(), query_url.size(), key.c_str());
|
||||
}
|
||||
|
||||
// Helper function for case-insensitive character comparison
|
||||
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
|
||||
@@ -22,8 +25,8 @@ inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b)
|
||||
// Helper function for case-insensitive string region comparison
|
||||
bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
|
||||
|
||||
// Bounded case-insensitive string search (like strcasestr but length-bounded)
|
||||
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle);
|
||||
// Case-insensitive string search (like strstr but case-insensitive)
|
||||
const char *stristr(const char *haystack, const char *needle);
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -171,11 +171,10 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
|
||||
const char *content_type_char = content_type.value().c_str();
|
||||
|
||||
// Check most common case first
|
||||
size_t content_type_len = strlen(content_type_char);
|
||||
if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
|
||||
if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
|
||||
// Normal form data - proceed with regular handling
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
} else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
|
||||
} else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
|
||||
auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
|
||||
return server->handle_multipart_upload_(r, content_type_char);
|
||||
#endif
|
||||
@@ -393,7 +392,13 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
|
||||
}
|
||||
|
||||
// Look up value from query strings
|
||||
auto val = this->find_query_value_(name);
|
||||
optional<std::string> val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name);
|
||||
if (!val.has_value()) {
|
||||
auto url_query = request_get_url_query(*this);
|
||||
if (url_query.has_value()) {
|
||||
val = query_key_value(url_query.value().c_str(), url_query.value().size(), name);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't cache misses to avoid wasting memory when handlers check for
|
||||
// optional parameters that don't exist in the request
|
||||
@@ -406,50 +411,6 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
|
||||
return param;
|
||||
}
|
||||
|
||||
/// Search post_query then URL query with a callback.
|
||||
/// Returns first truthy result, or value-initialized default.
|
||||
/// URL query is accessed directly from req->uri (same pattern as url_to()).
|
||||
template<typename Func>
|
||||
static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
|
||||
-> decltype(func(nullptr, size_t{0}, name)) {
|
||||
if (!post_query.empty()) {
|
||||
auto result = func(post_query.c_str(), post_query.size(), name);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// Use httpd API for query length, then access string directly from URI.
|
||||
// http_parser identifies components by offset/length without modifying the URI string.
|
||||
// This is the same pattern used by url_to().
|
||||
auto len = httpd_req_get_url_query_len(req);
|
||||
if (len == 0) {
|
||||
return {};
|
||||
}
|
||||
const char *query = strchr(req->uri, '?');
|
||||
if (query == nullptr) {
|
||||
return {};
|
||||
}
|
||||
query++; // skip '?'
|
||||
return func(query, len, name);
|
||||
}
|
||||
|
||||
optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
|
||||
return search_query_sources(this->req_, this->post_query_, name,
|
||||
[](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
|
||||
}
|
||||
|
||||
bool AsyncWebServerRequest::hasArg(const char *name) {
|
||||
return search_query_sources(this->req_, this->post_query_, name, query_has_key);
|
||||
}
|
||||
|
||||
std::string AsyncWebServerRequest::arg(const char *name) {
|
||||
auto val = this->find_query_value_(name);
|
||||
if (val.has_value()) {
|
||||
return std::move(val.value());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||
httpd_resp_set_hdr(*this->req_, name, value);
|
||||
}
|
||||
@@ -920,12 +881,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
|
||||
}
|
||||
});
|
||||
|
||||
// Use heap buffer - 1460 bytes is too large for the httpd task stack
|
||||
auto buffer = std::make_unique<char[]>(MULTIPART_CHUNK_SIZE);
|
||||
// Process data - use stack buffer to avoid heap allocation
|
||||
char buffer[MULTIPART_CHUNK_SIZE];
|
||||
size_t bytes_since_yield = 0;
|
||||
|
||||
for (size_t remaining = r->content_len; remaining > 0;) {
|
||||
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
|
||||
int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
|
||||
|
||||
if (recv_len <= 0) {
|
||||
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
|
||||
@@ -933,7 +894,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
|
||||
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
|
||||
}
|
||||
|
||||
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
|
||||
if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
|
||||
ESP_LOGW(TAG, "Multipart parser error");
|
||||
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
|
||||
return ESP_FAIL;
|
||||
|
||||
@@ -116,8 +116,7 @@ class AsyncWebServerRequest {
|
||||
/// Write URL (without query string) to buffer, returns StringRef pointing to buffer.
|
||||
/// URL is decoded (e.g., %20 -> space).
|
||||
StringRef url_to(std::span<char, URL_BUF_SIZE> buffer) const;
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
/// Get URL as std::string. Prefer url_to() to avoid heap allocation.
|
||||
std::string url() const {
|
||||
char buffer[URL_BUF_SIZE];
|
||||
return std::string(this->url_to(buffer));
|
||||
@@ -171,8 +170,14 @@ class AsyncWebServerRequest {
|
||||
AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); }
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
bool hasArg(const char *name);
|
||||
std::string arg(const char *name);
|
||||
bool hasArg(const char *name) { return this->hasParam(name); }
|
||||
std::string arg(const char *name) {
|
||||
auto *param = this->getParam(name);
|
||||
if (param) {
|
||||
return param->value();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
|
||||
|
||||
operator httpd_req_t *() const { return this->req_; }
|
||||
@@ -187,7 +192,6 @@ class AsyncWebServerRequest {
|
||||
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
|
||||
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
|
||||
// handlers check for optional parameters that don't exist.
|
||||
optional<std::string> find_query_value_(const char *name) const;
|
||||
std::vector<AsyncWebParameter *> params_;
|
||||
std::string post_query_;
|
||||
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
|
||||
|
||||
@@ -288,6 +288,11 @@ def _validate(config):
|
||||
config = config.copy()
|
||||
config[CONF_NETWORKS] = []
|
||||
|
||||
if config.get(CONF_FAST_CONNECT, False):
|
||||
networks = config.get(CONF_NETWORKS, [])
|
||||
if not networks:
|
||||
raise cv.Invalid("At least one network required for fast_connect!")
|
||||
|
||||
if CONF_USE_ADDRESS not in config:
|
||||
use_address = CORE.name + config[CONF_DOMAIN]
|
||||
if CONF_MANUAL_IP in config:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <cassert>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||
@@ -21,7 +20,6 @@
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/err.h"
|
||||
@@ -49,69 +47,6 @@ namespace esphome::wifi {
|
||||
|
||||
static const char *const TAG = "wifi";
|
||||
|
||||
// CompactString implementation
|
||||
CompactString::CompactString(const char *str, size_t len) {
|
||||
if (len > MAX_LENGTH) {
|
||||
len = MAX_LENGTH; // Clamp to max valid length
|
||||
}
|
||||
|
||||
this->length_ = len;
|
||||
if (len <= INLINE_CAPACITY) {
|
||||
// Store inline with null terminator
|
||||
this->is_heap_ = 0;
|
||||
if (len > 0) {
|
||||
std::memcpy(this->storage_, str, len);
|
||||
}
|
||||
this->storage_[len] = '\0';
|
||||
} else {
|
||||
// Heap allocate with null terminator
|
||||
this->is_heap_ = 1;
|
||||
char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
std::memcpy(heap_data, str, len);
|
||||
heap_data[len] = '\0';
|
||||
this->set_heap_ptr_(heap_data);
|
||||
}
|
||||
}
|
||||
|
||||
CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
|
||||
|
||||
CompactString &CompactString::operator=(const CompactString &other) {
|
||||
if (this != &other) {
|
||||
this->~CompactString();
|
||||
new (this) CompactString(other);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) {
|
||||
// Copy full storage (includes null terminator for inline, or pointer for heap)
|
||||
std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1);
|
||||
other.length_ = 0;
|
||||
other.is_heap_ = 0;
|
||||
other.storage_[0] = '\0';
|
||||
}
|
||||
|
||||
CompactString &CompactString::operator=(CompactString &&other) noexcept {
|
||||
if (this != &other) {
|
||||
this->~CompactString();
|
||||
new (this) CompactString(std::move(other));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
CompactString::~CompactString() {
|
||||
if (this->is_heap_) {
|
||||
delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
}
|
||||
|
||||
bool CompactString::operator==(const CompactString &other) const {
|
||||
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
|
||||
}
|
||||
bool CompactString::operator==(const StringRef &other) const {
|
||||
return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0;
|
||||
}
|
||||
|
||||
/// WiFi Retry Logic - Priority-Based BSSID Selection
|
||||
///
|
||||
/// The WiFi component uses a state machine with priority degradation to handle connection failures
|
||||
@@ -414,18 +349,18 @@ bool WiFiComponent::needs_scan_results_() const {
|
||||
return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
|
||||
}
|
||||
|
||||
bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const {
|
||||
bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
|
||||
// Check if this SSID is configured as hidden
|
||||
// If explicitly marked hidden, we should always try hidden mode regardless of scan results
|
||||
for (const auto &conf : this->sta_) {
|
||||
if (conf.ssid_ == ssid && conf.get_hidden()) {
|
||||
if (conf.get_ssid() == ssid && conf.get_hidden()) {
|
||||
return false; // Treat as not seen - force hidden mode attempt
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, check if we saw it in scan results
|
||||
for (const auto &scan : this->scan_result_) {
|
||||
if (scan.ssid_ == ssid) {
|
||||
if (scan.get_ssid() == ssid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -474,14 +409,14 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t
|
||||
continue;
|
||||
}
|
||||
// For BSSID-only configs (empty SSID), match by BSSID
|
||||
if (sta.ssid_.empty()) {
|
||||
if (sta.get_ssid().empty()) {
|
||||
if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Match by SSID
|
||||
if (sta.ssid_ == ssid) {
|
||||
if (sta.get_ssid() == ssid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -530,18 +465,18 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
|
||||
if (!include_explicit_hidden && sta.get_hidden()) {
|
||||
int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
|
||||
if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
|
||||
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str());
|
||||
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// In BLIND_RETRY mode, treat all networks as candidates
|
||||
// In SCAN_BASED mode, only retry networks that weren't seen in the scan
|
||||
if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.ssid_)) {
|
||||
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast<int>(i));
|
||||
if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.get_ssid())) {
|
||||
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
|
||||
return static_cast<int8_t>(i);
|
||||
}
|
||||
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str());
|
||||
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str());
|
||||
}
|
||||
// No hidden SSIDs found
|
||||
return -1;
|
||||
@@ -658,11 +593,11 @@ void WiFiComponent::start() {
|
||||
// Fast connect optimization: only use when we have saved BSSID+channel data
|
||||
// Without saved data, try first configured network or use normal flow
|
||||
if (loaded_fast_connect) {
|
||||
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str());
|
||||
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
|
||||
this->start_connecting(params);
|
||||
} else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
|
||||
// No saved data, but have configured networks - try first non-hidden network
|
||||
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str());
|
||||
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
|
||||
this->selected_sta_index_ = 0;
|
||||
params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params);
|
||||
@@ -892,7 +827,7 @@ void WiFiComponent::setup_ap_config_() {
|
||||
if (this->ap_setup_)
|
||||
return;
|
||||
|
||||
if (this->ap_.ssid_.empty()) {
|
||||
if (this->ap_.get_ssid().empty()) {
|
||||
// Build AP SSID from app name without heap allocation
|
||||
// WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7
|
||||
static constexpr size_t AP_SSID_MAX_LEN = 32;
|
||||
@@ -928,7 +863,7 @@ void WiFiComponent::setup_ap_config_() {
|
||||
" AP SSID: '%s'\n"
|
||||
" AP Password: '%s'\n"
|
||||
" IP Address: %s",
|
||||
this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
|
||||
this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
auto manual_ip = this->ap_.get_manual_ip();
|
||||
@@ -1025,12 +960,9 @@ WiFiAP WiFiComponent::get_sta() const {
|
||||
return config ? *config : WiFiAP{};
|
||||
}
|
||||
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
|
||||
this->save_wifi_sta(ssid.c_str(), password.c_str());
|
||||
}
|
||||
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
|
||||
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
|
||||
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
|
||||
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
|
||||
strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
|
||||
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
|
||||
this->pref_.save(&save);
|
||||
// ensure it's written immediately
|
||||
global_preferences->sync();
|
||||
@@ -1064,14 +996,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
|
||||
ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
|
||||
ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
|
||||
get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
ESP_LOGV(TAG,
|
||||
"Connection Params:\n"
|
||||
" SSID: '%s'",
|
||||
ap.ssid_.c_str());
|
||||
ap.get_ssid().c_str());
|
||||
if (ap.has_bssid()) {
|
||||
ESP_LOGV(TAG, " BSSID: %s", bssid_s);
|
||||
} else {
|
||||
@@ -1104,7 +1036,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
client_key_present ? "present" : "not present");
|
||||
} else {
|
||||
#endif
|
||||
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str());
|
||||
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
}
|
||||
#endif
|
||||
@@ -1320,61 +1252,20 @@ void WiFiComponent::start_scanning() {
|
||||
// Using insertion sort instead of std::stable_sort saves flash memory
|
||||
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
||||
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
||||
//
|
||||
// Uses raw memcpy instead of copy assignment to avoid CompactString's
|
||||
// destructor/constructor overhead (heap delete[]/new[] for long SSIDs).
|
||||
// Copy assignment calls ~CompactString() then placement-new for every shift,
|
||||
// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+
|
||||
// networks (e.g., captive portal showing full scan results), this caused
|
||||
// event loop blocking from hundreds of heap operations in a tight loop.
|
||||
//
|
||||
// This is safe because we're permuting elements within the same array —
|
||||
// each slot is overwritten exactly once, so no ownership duplication occurs.
|
||||
// All members of WiFiScanResult are either trivially copyable (bssid, channel,
|
||||
// rssi, priority, flags) or CompactString, which stores either inline data or
|
||||
// a heap pointer — never a self-referential pointer (unlike std::string's SSO
|
||||
// on some implementations). This was not possible before PR#13472 replaced
|
||||
// std::string with CompactString, since std::string's internal layout is
|
||||
// implementation-defined and may use self-referential pointers.
|
||||
//
|
||||
// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for
|
||||
// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee.
|
||||
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
|
||||
// memcpy-based sort requires no self-referential pointers or virtual dispatch.
|
||||
// These static_asserts guard the assumptions. If any fire, the memcpy sort
|
||||
// must be reviewed for safety before updating the expected values.
|
||||
//
|
||||
// No vtable pointers (memcpy would corrupt vptr)
|
||||
static_assert(!std::is_polymorphic<WiFiScanResult>::value, "WiFiScanResult must not have vtable");
|
||||
static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
|
||||
// Standard layout ensures predictable memory layout with no virtual bases
|
||||
// and no mixed-access-specifier reordering
|
||||
static_assert(std::is_standard_layout<WiFiScanResult>::value, "WiFiScanResult must be standard layout");
|
||||
static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
|
||||
// Size checks catch added/removed fields that may need safety review
|
||||
static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe");
|
||||
static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe");
|
||||
// Alignment must match for reinterpret_cast of key_buf to be valid
|
||||
static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t");
|
||||
const size_t size = results.size();
|
||||
constexpr size_t elem_size = sizeof(WiFiScanResult);
|
||||
// Suppress warnings for intentional memcpy on non-trivially-copyable type.
|
||||
// Safety is guaranteed by the static_asserts above and the permutation invariant.
|
||||
// NOLINTNEXTLINE(bugprone-undefined-memory-manipulation)
|
||||
auto *memcpy_fn = &memcpy;
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
alignas(WiFiScanResult) uint8_t key_buf[elem_size];
|
||||
memcpy_fn(key_buf, &results[i], elem_size);
|
||||
const auto &key = *reinterpret_cast<const WiFiScanResult *>(key_buf);
|
||||
// Make a copy to avoid issues with move semantics during comparison
|
||||
WiFiScanResult key = results[i];
|
||||
int32_t j = i - 1;
|
||||
|
||||
// Move elements that are worse than key to the right
|
||||
// For stability, we only move if key is strictly better than results[j]
|
||||
while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
|
||||
memcpy_fn(&results[j + 1], &results[j], elem_size);
|
||||
results[j + 1] = results[j];
|
||||
j--;
|
||||
}
|
||||
memcpy_fn(&results[j + 1], key_buf, elem_size);
|
||||
results[j + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1520,7 +1411,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
|
||||
config && !config->get_hidden() &&
|
||||
this->scan_result_.empty()) {
|
||||
ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str());
|
||||
ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str());
|
||||
}
|
||||
// Reset to initial phase on successful connection (don't log transition, just reset state)
|
||||
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
||||
@@ -1934,11 +1825,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
}
|
||||
|
||||
// Get SSID for logging (use pointer to avoid copy)
|
||||
const char *ssid = nullptr;
|
||||
const std::string *ssid = nullptr;
|
||||
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
||||
ssid = this->scan_result_[0].ssid_.c_str();
|
||||
ssid = &this->scan_result_[0].get_ssid();
|
||||
} else if (const WiFiAP *config = this->get_selected_sta_()) {
|
||||
ssid = config->ssid_.c_str();
|
||||
ssid = &config->get_ssid();
|
||||
}
|
||||
|
||||
// Only decrease priority on the last attempt for this phase
|
||||
@@ -1958,8 +1849,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
}
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
|
||||
bssid_s, old_priority, new_priority);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
|
||||
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
|
||||
|
||||
// After adjusting priority, check if all priorities are now at minimum
|
||||
// If so, clear the vector to save memory and reset for fresh start
|
||||
@@ -2207,14 +2098,10 @@ void WiFiComponent::save_fast_connect_settings_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
|
||||
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
|
||||
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
|
||||
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
|
||||
void WiFiAP::set_password(const std::string &password) {
|
||||
this->password_ = CompactString(password.c_str(), password.size());
|
||||
}
|
||||
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
|
||||
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
|
||||
#endif
|
||||
@@ -2224,8 +2111,10 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
|
||||
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
|
||||
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
|
||||
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
|
||||
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
|
||||
const std::string &WiFiAP::get_password() const { return this->password_; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
|
||||
#endif
|
||||
@@ -2236,12 +2125,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
|
||||
#endif
|
||||
bool WiFiAP::get_hidden() const { return this->hidden_; }
|
||||
|
||||
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
|
||||
bool with_auth, bool is_hidden)
|
||||
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
|
||||
bool is_hidden)
|
||||
: bssid_(bssid),
|
||||
channel_(channel),
|
||||
rssi_(rssi),
|
||||
ssid_(ssid, ssid_len),
|
||||
ssid_(std::move(ssid)),
|
||||
with_auth_(with_auth),
|
||||
is_hidden_(is_hidden) {}
|
||||
bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
@@ -2250,9 +2139,9 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
// don't match SSID
|
||||
if (!this->is_hidden_)
|
||||
return false;
|
||||
} else if (!config.ssid_.empty()) {
|
||||
} else if (!config.get_ssid().empty()) {
|
||||
// check if SSID matches
|
||||
if (this->ssid_ != config.ssid_)
|
||||
if (config.get_ssid() != this->ssid_)
|
||||
return false;
|
||||
} else {
|
||||
// network is configured without SSID - match other settings
|
||||
@@ -2263,15 +2152,15 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
// BSSID requires auth but no PSK or EAP credentials given
|
||||
if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value()))
|
||||
if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value()))
|
||||
return false;
|
||||
|
||||
// BSSID does not require auth, but PSK or EAP credentials given
|
||||
if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value()))
|
||||
if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value()))
|
||||
return false;
|
||||
#else
|
||||
// If PSK given, only match for networks with auth (and vice versa)
|
||||
if (config.password_.empty() == this->with_auth_)
|
||||
if (config.get_password().empty() == this->with_auth_)
|
||||
return false;
|
||||
#endif
|
||||
|
||||
@@ -2284,6 +2173,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
bool WiFiScanResult::get_matches() const { return this->matches_; }
|
||||
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
|
||||
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
|
||||
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
|
||||
uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
|
||||
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
|
||||
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
|
||||
@@ -2394,7 +2284,7 @@ void WiFiComponent::process_roaming_scan_() {
|
||||
|
||||
for (const auto &result : this->scan_result_) {
|
||||
// Must be same SSID, different BSSID
|
||||
if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid)
|
||||
if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
|
||||
continue;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
@@ -173,75 +172,12 @@ template<typename T> using wifi_scan_vector_t = std::vector<T>;
|
||||
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
#endif
|
||||
|
||||
/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated.
|
||||
/// Used internally for WiFi SSID/password storage to reduce heap fragmentation.
|
||||
class CompactString {
|
||||
public:
|
||||
static constexpr uint8_t MAX_LENGTH = 127;
|
||||
static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes
|
||||
|
||||
CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; }
|
||||
CompactString(const char *str, size_t len);
|
||||
CompactString(const CompactString &other);
|
||||
CompactString(CompactString &&other) noexcept;
|
||||
CompactString &operator=(const CompactString &other);
|
||||
CompactString &operator=(CompactString &&other) noexcept;
|
||||
~CompactString();
|
||||
|
||||
const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; }
|
||||
const char *c_str() const { return this->data(); } // Always null-terminated
|
||||
size_t size() const { return this->length_; }
|
||||
bool empty() const { return this->length_ == 0; }
|
||||
|
||||
/// Return a StringRef view of this string (zero-copy)
|
||||
StringRef ref() const { return StringRef(this->data(), this->size()); }
|
||||
|
||||
bool operator==(const CompactString &other) const;
|
||||
bool operator!=(const CompactString &other) const { return !(*this == other); }
|
||||
bool operator==(const StringRef &other) const;
|
||||
bool operator!=(const StringRef &other) const { return !(*this == other); }
|
||||
bool operator==(const char *other) const { return *this == StringRef(other); }
|
||||
bool operator!=(const char *other) const { return !(*this == other); }
|
||||
|
||||
protected:
|
||||
char *get_heap_ptr_() const {
|
||||
char *ptr;
|
||||
std::memcpy(&ptr, this->storage_, sizeof(ptr));
|
||||
return ptr;
|
||||
}
|
||||
void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); }
|
||||
|
||||
// Storage for string data. When is_heap_=0, contains the string directly (null-terminated).
|
||||
// When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation.
|
||||
char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator
|
||||
uint8_t length_ : 7; // String length (0-127)
|
||||
uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage
|
||||
// Total size: 20 bytes (19 bytes storage + 1 byte bitfields)
|
||||
};
|
||||
|
||||
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
|
||||
// CompactString is not trivially copyable (non-trivial destructor/copy for heap case).
|
||||
// However, its layout has no self-referential pointers: storage_[] contains either inline
|
||||
// data or an external heap pointer — never a pointer to itself. This is unlike libstdc++
|
||||
// std::string SSO where _M_p points to _M_local_buf within the same object.
|
||||
// This property allows memcpy-based permutation sorting where each element ends up in
|
||||
// exactly one slot (no ownership duplication). These asserts document that layout property.
|
||||
static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
|
||||
static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
|
||||
|
||||
class WiFiAP {
|
||||
friend class WiFiComponent;
|
||||
friend class WiFiScanResult;
|
||||
|
||||
public:
|
||||
void set_ssid(const std::string &ssid);
|
||||
void set_ssid(const char *ssid);
|
||||
void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
|
||||
void set_bssid(const bssid_t &bssid);
|
||||
void clear_bssid();
|
||||
void set_password(const std::string &password);
|
||||
void set_password(const char *password);
|
||||
void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void set_eap(optional<EAPAuth> eap_auth);
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -252,10 +188,10 @@ class WiFiAP {
|
||||
void set_manual_ip(optional<ManualIP> manual_ip);
|
||||
#endif
|
||||
void set_hidden(bool hidden);
|
||||
StringRef get_ssid() const { return this->ssid_.ref(); }
|
||||
StringRef get_password() const { return this->password_.ref(); }
|
||||
const std::string &get_ssid() const;
|
||||
const bssid_t &get_bssid() const;
|
||||
bool has_bssid() const;
|
||||
const std::string &get_password() const;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &get_eap() const;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -268,8 +204,8 @@ class WiFiAP {
|
||||
bool get_hidden() const;
|
||||
|
||||
protected:
|
||||
CompactString ssid_;
|
||||
CompactString password_;
|
||||
std::string ssid_;
|
||||
std::string password_;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
optional<EAPAuth> eap_;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -284,18 +220,15 @@ class WiFiAP {
|
||||
};
|
||||
|
||||
class WiFiScanResult {
|
||||
friend class WiFiComponent;
|
||||
|
||||
public:
|
||||
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
|
||||
bool is_hidden);
|
||||
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
|
||||
|
||||
bool matches(const WiFiAP &config) const;
|
||||
|
||||
bool get_matches() const;
|
||||
void set_matches(bool matches);
|
||||
const bssid_t &get_bssid() const;
|
||||
StringRef get_ssid() const { return this->ssid_.ref(); }
|
||||
const std::string &get_ssid() const;
|
||||
uint8_t get_channel() const;
|
||||
int8_t get_rssi() const;
|
||||
bool get_with_auth() const;
|
||||
@@ -309,7 +242,7 @@ class WiFiScanResult {
|
||||
bssid_t bssid_;
|
||||
uint8_t channel_;
|
||||
int8_t rssi_;
|
||||
CompactString ssid_;
|
||||
std::string ssid_;
|
||||
int8_t priority_{0};
|
||||
bool matches_{false};
|
||||
bool with_auth_;
|
||||
@@ -448,8 +381,6 @@ class WiFiComponent : public Component {
|
||||
void set_passive_scan(bool passive);
|
||||
|
||||
void save_wifi_sta(const std::string &ssid, const std::string &password);
|
||||
void save_wifi_sta(const char *ssid, const char *password);
|
||||
void save_wifi_sta(StringRef ssid, StringRef password) { this->save_wifi_sta(ssid.c_str(), password.c_str()); }
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
@@ -511,8 +442,6 @@ class WiFiComponent : public Component {
|
||||
}
|
||||
|
||||
network::IPAddresses wifi_sta_ip_addresses();
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string wifi_ssid();
|
||||
/// Write SSID to buffer without heap allocation.
|
||||
/// Returns pointer to buffer, or empty string if not connected.
|
||||
@@ -616,7 +545,7 @@ class WiFiComponent : public Component {
|
||||
int8_t find_first_non_hidden_index_() const;
|
||||
/// Check if an SSID was seen in the most recent scan results
|
||||
/// Used to skip hidden mode for SSIDs we know are visible
|
||||
bool ssid_was_seen_in_scan_(const CompactString &ssid) const;
|
||||
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
|
||||
/// Check if full scan results are needed (captive portal active, improv, listeners)
|
||||
bool needs_full_scan_results_() const;
|
||||
/// Check if network matches any configured network (for scan result filtering)
|
||||
|
||||
@@ -247,16 +247,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
|
||||
struct station_config conf {};
|
||||
memset(&conf, 0, sizeof(conf));
|
||||
if (ap.ssid_.size() > sizeof(conf.ssid)) {
|
||||
if (ap.get_ssid().size() > sizeof(conf.ssid)) {
|
||||
ESP_LOGE(TAG, "SSID too long");
|
||||
return false;
|
||||
}
|
||||
if (ap.password_.size() > sizeof(conf.password)) {
|
||||
if (ap.get_password().size() > sizeof(conf.password)) {
|
||||
ESP_LOGE(TAG, "Password too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
|
||||
|
||||
if (ap.has_bssid()) {
|
||||
conf.bssid_set = 1;
|
||||
@@ -266,7 +266,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
}
|
||||
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
|
||||
if (ap.password_.empty()) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.threshold.authmode = AUTH_OPEN;
|
||||
} else {
|
||||
// Set threshold based on configured minimum auth mode
|
||||
@@ -738,8 +738,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
|
||||
this->scan_result_.emplace_back(
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
|
||||
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
|
||||
}
|
||||
@@ -832,27 +832,27 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
return false;
|
||||
|
||||
struct softap_config conf {};
|
||||
if (ap.ssid_.size() > sizeof(conf.ssid)) {
|
||||
if (ap.get_ssid().size() > sizeof(conf.ssid)) {
|
||||
ESP_LOGE(TAG, "AP SSID too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
|
||||
conf.ssid_len = static_cast<uint8>(ap.ssid_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
|
||||
conf.channel = ap.has_channel() ? ap.get_channel() : 1;
|
||||
conf.ssid_hidden = ap.get_hidden();
|
||||
conf.max_connection = 5;
|
||||
conf.beacon_interval = 100;
|
||||
|
||||
if (ap.password_.empty()) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.authmode = AUTH_OPEN;
|
||||
*conf.password = 0;
|
||||
} else {
|
||||
conf.authmode = AUTH_WPA2_PSK;
|
||||
if (ap.password_.size() > sizeof(conf.password)) {
|
||||
if (ap.get_password().size() > sizeof(conf.password)) {
|
||||
ESP_LOGE(TAG, "AP password too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
|
||||
}
|
||||
|
||||
ETS_UART_INTR_DISABLE();
|
||||
|
||||
@@ -300,19 +300,19 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
|
||||
wifi_config_t conf;
|
||||
memset(&conf, 0, sizeof(conf));
|
||||
if (ap.ssid_.size() > sizeof(conf.sta.ssid)) {
|
||||
if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
|
||||
ESP_LOGE(TAG, "SSID too long");
|
||||
return false;
|
||||
}
|
||||
if (ap.password_.size() > sizeof(conf.sta.password)) {
|
||||
if (ap.get_password().size() > sizeof(conf.sta.password)) {
|
||||
ESP_LOGE(TAG, "Password too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.password_.c_str(), ap.password_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
|
||||
|
||||
// The weakest authmode to accept in the fast scan mode
|
||||
if (ap.password_.empty()) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
} else {
|
||||
// Set threshold based on configured minimum auth mode
|
||||
@@ -864,7 +864,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
|
||||
std::string ssid(ssid_cstr);
|
||||
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
|
||||
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
|
||||
@@ -1054,26 +1055,26 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
|
||||
wifi_config_t conf;
|
||||
memset(&conf, 0, sizeof(conf));
|
||||
if (ap.ssid_.size() > sizeof(conf.ap.ssid)) {
|
||||
if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
|
||||
ESP_LOGE(TAG, "AP SSID too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1;
|
||||
conf.ap.ssid_hidden = ap.get_hidden();
|
||||
conf.ap.ssid_hidden = ap.get_ssid().size();
|
||||
conf.ap.max_connection = 5;
|
||||
conf.ap.beacon_interval = 100;
|
||||
|
||||
if (ap.password_.empty()) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.ap.authmode = WIFI_AUTH_OPEN;
|
||||
*conf.ap.password = 0;
|
||||
} else {
|
||||
conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
if (ap.password_.size() > sizeof(conf.ap.password)) {
|
||||
if (ap.get_password().size() > sizeof(conf.ap.password)) {
|
||||
ESP_LOGE(TAG, "AP password too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.password_.c_str(), ap.password_.size());
|
||||
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
|
||||
}
|
||||
|
||||
// pairwise cipher of SoftAP, group cipher will be derived using this.
|
||||
|
||||
@@ -193,7 +193,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
return false;
|
||||
|
||||
String ssid = WiFi.SSID();
|
||||
if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) {
|
||||
if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) {
|
||||
WiFi.disconnect();
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
s_sta_state = LTWiFiSTAState::CONNECTING;
|
||||
s_ignored_disconnect_count = 0;
|
||||
|
||||
WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
|
||||
WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
|
||||
ap.get_channel(), // 0 = auto
|
||||
ap.has_bssid() ? ap.get_bssid().data() : NULL);
|
||||
if (status != WL_CONNECTED) {
|
||||
@@ -688,7 +688,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
auto &ap = scan->ap[i];
|
||||
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
|
||||
ap.bssid.addr[4], ap.bssid.addr[5]},
|
||||
ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
|
||||
std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
auto &ap = scan->ap[i];
|
||||
@@ -735,7 +735,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
|
||||
yield();
|
||||
|
||||
return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
|
||||
return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
|
||||
ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden());
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
return false;
|
||||
#endif
|
||||
|
||||
auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str());
|
||||
auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
|
||||
if (ret != WL_CONNECTED)
|
||||
return false;
|
||||
|
||||
@@ -149,8 +149,9 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
|
||||
|
||||
bssid_t bssid;
|
||||
std::copy(result->bssid, result->bssid + 6, bssid.begin());
|
||||
WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi,
|
||||
result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0');
|
||||
std::string ssid(ssid_cstr);
|
||||
WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
|
||||
this->scan_result_.push_back(res);
|
||||
}
|
||||
@@ -203,7 +204,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
}
|
||||
#endif
|
||||
|
||||
WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1);
|
||||
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t<wi
|
||||
for (const auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const auto &ssid = scan.get_ssid();
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
|
||||
if (ptr + ssid.size() + 9 > end)
|
||||
break;
|
||||
|
||||
@@ -61,19 +61,11 @@ void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto before = millis();
|
||||
auto err = zigbee_default_signal_handler(bufid);
|
||||
if (err != RET_OK) {
|
||||
ESP_LOGE(TAG, "Zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err));
|
||||
}
|
||||
|
||||
if (sig == ZB_COMMON_SIGNAL_CAN_SLEEP) {
|
||||
this->sleep_remainder_ += millis() - before;
|
||||
uint32_t seconds = this->sleep_remainder_ / 1000;
|
||||
this->sleep_remainder_ -= seconds * 1000;
|
||||
this->sleep_time_ += seconds;
|
||||
}
|
||||
|
||||
switch (sig) {
|
||||
case ZB_BDB_SIGNAL_STEERING:
|
||||
ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status);
|
||||
@@ -221,7 +213,6 @@ void ZigbeeComponent::dump_config() {
|
||||
"Zigbee\n"
|
||||
" Wipe on boot: %s\n"
|
||||
" Device is joined to the network: %s\n"
|
||||
" Sleep time: %us\n"
|
||||
" Current channel: %d\n"
|
||||
" Current page: %d\n"
|
||||
" Sleep threshold: %ums\n"
|
||||
@@ -230,9 +221,9 @@ void ZigbeeComponent::dump_config() {
|
||||
" Short addr: 0x%04X\n"
|
||||
" Long pan id: 0x%s\n"
|
||||
" Short pan id: 0x%04X",
|
||||
get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(),
|
||||
zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(),
|
||||
extended_pan_id_buf, zb_get_pan_id());
|
||||
get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(),
|
||||
zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf,
|
||||
zb_get_pan_id());
|
||||
dump_reporting_();
|
||||
}
|
||||
|
||||
|
||||
@@ -92,8 +92,6 @@ class ZigbeeComponent : public Component {
|
||||
CallbackManager<void()> join_cb_;
|
||||
Trigger<> join_trigger_;
|
||||
bool force_report_{false};
|
||||
uint32_t sleep_time_{};
|
||||
uint32_t sleep_remainder_{};
|
||||
};
|
||||
|
||||
class ZigbeeEntity {
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.3.0-dev"
|
||||
__version__ = "2026.2.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -609,6 +609,15 @@ void Application::unregister_socket_fd(int fd) {
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::is_socket_ready(int fd) const {
|
||||
// This function is thread-safe for reading the result of select()
|
||||
// However, it should only be called after select() has been executed in the main loop
|
||||
// The read_fds_ is only modified by select() in the main loop
|
||||
if (fd < 0 || fd >= FD_SETSIZE)
|
||||
return false;
|
||||
|
||||
return FD_ISSET(fd, &this->read_fds_);
|
||||
}
|
||||
#endif
|
||||
|
||||
void Application::yield_with_select_(uint32_t delay_ms) {
|
||||
|
||||
@@ -101,10 +101,6 @@
|
||||
#include "esphome/components/update/update_entity.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::socket {
|
||||
class Socket;
|
||||
} // namespace esphome::socket
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Teardown timeout constant (in milliseconds)
|
||||
@@ -495,8 +491,7 @@ class Application {
|
||||
void unregister_socket_fd(int fd);
|
||||
/// Check if there's data available on a socket without blocking
|
||||
/// This function is thread-safe for reading, but should be called after select() has run
|
||||
/// The read_fds_ is only modified by select() in the main loop
|
||||
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
|
||||
bool is_socket_ready(int fd) const;
|
||||
|
||||
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||
/// Wake the main event loop from a FreeRTOS task
|
||||
@@ -508,15 +503,6 @@ class Application {
|
||||
|
||||
protected:
|
||||
friend Component;
|
||||
friend class socket::Socket;
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
/// Fast path for Socket::ready() via friendship - skips negative fd check.
|
||||
/// Safe because: fd was validated in register_socket_fd() at registration time,
|
||||
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
|
||||
/// FD_ISSET may include its own upper bounds check depending on platform.
|
||||
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
|
||||
#endif
|
||||
|
||||
void register_component_(Component *comp);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#define ESPHOME_PROJECT_VERSION_30 "v2"
|
||||
#define ESPHOME_VARIANT "ESP32"
|
||||
#define ESPHOME_DEBUG_SCHEDULER
|
||||
#define ESPHOME_DEBUG_API
|
||||
|
||||
// Default threading model for static analysis (ESP32 is multi-threaded with atomics)
|
||||
#define ESPHOME_THREAD_MULTI_ATOMICS
|
||||
@@ -239,15 +238,9 @@
|
||||
#define USB_HOST_MAX_REQUESTS 16
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7)
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 6)
|
||||
#define USE_ETHERNET
|
||||
#define USE_ETHERNET_LAN8720
|
||||
#define USE_ETHERNET_RTL8201
|
||||
#define USE_ETHERNET_DP83848
|
||||
#define USE_ETHERNET_IP101
|
||||
#define USE_ETHERNET_JL1101
|
||||
#define USE_ETHERNET_KSZ8081
|
||||
#define USE_ETHERNET_LAN8670
|
||||
#define USE_ETHERNET_MANUAL_IP
|
||||
#define USE_ETHERNET_IP_STATE_LISTENERS
|
||||
#define USE_ETHERNET_CONNECT_TRIGGER
|
||||
|
||||
@@ -1083,9 +1083,6 @@ template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &dat
|
||||
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
|
||||
* Optionally includes the total byte count in parentheses at the end.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Pointer to the byte array to format.
|
||||
* @param length Number of bytes in the array.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
@@ -1111,9 +1108,6 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
|
||||
*
|
||||
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Pointer to the 16-bit word array to format.
|
||||
* @param length Number of 16-bit words in the array.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
@@ -1137,9 +1131,6 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato
|
||||
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
|
||||
* uppercase hex value with customizable separator.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Vector of bytes to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
@@ -1163,9 +1154,6 @@ std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator =
|
||||
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
|
||||
* as a 4-digit uppercase hex value in big-endian order.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Vector of 16-bit words to format.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||
@@ -1188,9 +1176,6 @@ std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator
|
||||
* Treats each character in the string as a byte and formats it in hex.
|
||||
* Useful for debugging binary data stored in std::string containers.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data String whose bytes should be formatted as hex.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
@@ -1213,9 +1198,6 @@ std::string format_hex_pretty(const std::string &data, char separator = '.', boo
|
||||
* Converts the integer to big-endian byte order and formats each byte as hex.
|
||||
* The most significant byte appears first in the output string.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
|
||||
* @param val The unsigned integer value to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
|
||||
@@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||
[common:esp32-arduino]
|
||||
extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
|
||||
|
||||
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||
@@ -169,7 +169,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
[common:esp32-idf]
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ classifiers = [
|
||||
"Topic :: Home Automation",
|
||||
]
|
||||
|
||||
# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76
|
||||
requires-python = ">=3.11.0,<3.15"
|
||||
# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502
|
||||
requires-python = ">=3.11.0,<3.14"
|
||||
|
||||
dynamic = ["dependencies", "optional-dependencies", "version"]
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ def get_logger_tags():
|
||||
"api.service",
|
||||
]
|
||||
for file in CORE_COMPONENTS_PATH.rglob("*.cpp"):
|
||||
data = file.read_text(encoding="utf-8")
|
||||
data = file.read_text()
|
||||
match = pattern.search(data)
|
||||
if match:
|
||||
tags.append(match.group(1))
|
||||
|
||||
@@ -3,15 +3,9 @@ display:
|
||||
spi_16: true
|
||||
pixel_mode: 18bit
|
||||
model: ili9488
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: ${dc_pin}
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: ${cs_pin}
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: ${reset_pin}
|
||||
dc_pin: ${dc_pin}
|
||||
cs_pin: ${cs_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
data_rate: 20MHz
|
||||
invert_colors: true
|
||||
show_test_card: true
|
||||
@@ -30,15 +24,3 @@ display:
|
||||
height: 200
|
||||
enable_pin: ${enable_pin}
|
||||
bus_mode: single
|
||||
|
||||
- platform: mipi_spi
|
||||
model: WAVESHARE-1.83-V2
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: ${dc_pin}
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: ${cs_pin}
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: ${reset_pin}
|
||||
|
||||
@@ -197,7 +197,6 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
|
||||
" platformio_options:\n"
|
||||
" build_flags:\n"
|
||||
' - "-DDEBUG" # Enable assert() statements\n'
|
||||
' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n'
|
||||
' - "-g" # Add debug symbols',
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user