mirror of
https://github.com/esphome/esphome.git
synced 2026-09-05 12:36:07 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e86be05f74 | ||
|
|
8ce65558e1 | ||
|
|
84f78831f9 | ||
|
|
13dbbcaa32 | ||
|
|
b66822d9bd | ||
|
|
d1829c495d | ||
|
|
ce87bf9b17 | ||
|
|
51ea97deff |
@@ -553,6 +553,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
4. **Lint:** Run `prek` to ensure code is compliant.
|
||||
5. **Commit:** Commit your changes. There is no strict format for commit messages.
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
|
||||
7. **Comments:** When commenting on GitHub PRs or issues, don't tag contributors, especially bots. Avoid referring to list items (e.g. from reviews) with the form #nn - this will be interpreted by GitHub as a reference to issue or PR nn. Keep comments short and exclude irrelevant details, backstories, restatement of previous comments and anything that is already obvious to the reader.
|
||||
|
||||
* **Documentation Contributions:**
|
||||
* Documentation is hosted in the separate `esphome/esphome.io` repository.
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.1
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cerrno>
|
||||
#include <sys/select.h>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
@@ -42,7 +41,16 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
socket_->setblocking(false);
|
||||
if (socket_->setblocking(false) != 0) {
|
||||
// Capture before the log and reset() below can clobber errno; a blocking
|
||||
// connect()/read() would otherwise stall the whole loop
|
||||
const int saved_errno = errno;
|
||||
ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, saved_errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||
if (err == 0) {
|
||||
@@ -97,45 +105,22 @@ void AsyncClient::loop() {
|
||||
return;
|
||||
|
||||
if (connecting_) {
|
||||
// For connecting, we need to check writability, not readability
|
||||
// The Application's select() only monitors read FDs, so we do our own check here
|
||||
// For ESP platforms lwip_select() might be faster, but this code isn't used
|
||||
// on those platforms anyway. If it was, we'd fix the Application select()
|
||||
// to report writability instead of doing it this way.
|
||||
int fd = socket_->get_fd();
|
||||
if (fd < 0) {
|
||||
ESP_LOGW(TAG, "Invalid socket fd");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
|
||||
struct timeval tv = {0, 0};
|
||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
|
||||
if (ret > 0 && FD_ISSET(fd, &writefds)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
|
||||
int err = 0;
|
||||
switch (socket::poll_connect(*socket_, err)) {
|
||||
case socket::ConnectPollResult::CONNECT_POLL_PENDING:
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_CONNECTED:
|
||||
connecting_ = false;
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_ERROR:
|
||||
ESP_LOGW(TAG, "Connection failed: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
const int err = errno;
|
||||
ESP_LOGE(TAG, "Select error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
error_cb_(error_arg_, this, err);
|
||||
break;
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
|
||||
@@ -100,21 +100,38 @@ void ESP32BLE::disable() {
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_start() {
|
||||
this->advertising_init_();
|
||||
if (!this->is_active())
|
||||
this->advertising_ref_count_++;
|
||||
this->advertising_refresh();
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_stop() {
|
||||
if (this->advertising_ref_count_ == 0)
|
||||
return;
|
||||
this->advertising_->start();
|
||||
this->advertising_ref_count_--;
|
||||
this->advertising_refresh();
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_refresh() {
|
||||
if (this->advertising_ == nullptr || !this->is_active())
|
||||
return;
|
||||
// Advertise while any component still needs it, otherwise stop
|
||||
if (this->advertising_ref_count_ == 0) {
|
||||
this->advertising_->stop();
|
||||
} else {
|
||||
this->advertising_->start();
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_service_data(data);
|
||||
this->advertising_start();
|
||||
this->advertising_refresh();
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
this->advertising_refresh();
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name) {
|
||||
@@ -136,7 +153,7 @@ void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> da
|
||||
this->advertising_->set_service_data(data);
|
||||
}
|
||||
|
||||
this->advertising_start();
|
||||
this->advertising_refresh();
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
|
||||
@@ -147,13 +164,13 @@ void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<voi
|
||||
void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->add_service_uuid(uuid);
|
||||
this->advertising_start();
|
||||
this->advertising_refresh();
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->remove_service_uuid(uuid);
|
||||
this->advertising_start();
|
||||
this->advertising_refresh();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -575,6 +592,10 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
|
||||
}
|
||||
|
||||
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
// Requests made before the stack was up (or before it was re-enabled) take effect now
|
||||
this->advertising_refresh();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,17 @@ class ESP32BLE final : public Component {
|
||||
void set_name(const char *name) { this->name_ = name; }
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
/** Request advertising on behalf of a component.
|
||||
*
|
||||
* Requests are reference counted: advertising runs until every component that called
|
||||
* advertising_start() has released it again with advertising_stop(). Each component must
|
||||
* pair its calls, so nothing advertises until something actually asks for it.
|
||||
*/
|
||||
void advertising_start();
|
||||
/// Release a request made with advertising_start(); advertising stops at the last release.
|
||||
void advertising_stop();
|
||||
/// Apply the current payload and request count: advertise while requested, otherwise stop.
|
||||
void advertising_refresh();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
@@ -226,6 +236,9 @@ class ESP32BLE final : public Component {
|
||||
// 1-byte aligned members (grouped together to minimize padding)
|
||||
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
|
||||
bool enable_on_boot_{}; // 1 byte
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
uint8_t advertising_ref_count_{0}; // 1 byte, number of components requesting advertising
|
||||
#endif
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
optional<esp_ble_auth_req_t> auth_req_mode_;
|
||||
|
||||
@@ -67,6 +67,8 @@ void ESP32BLEBeacon::setup() {
|
||||
this->on_advertise_();
|
||||
}
|
||||
});
|
||||
// A beacon always needs the device to advertise, and never releases the request
|
||||
global_ble->advertising_start();
|
||||
}
|
||||
|
||||
void ESP32BLEBeacon::on_advertise_() {
|
||||
|
||||
@@ -596,6 +596,18 @@ async def to_code(config):
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
|
||||
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
|
||||
# Only advertise for the server itself when the configuration gives clients something to
|
||||
# find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays
|
||||
# silent until that service asks for advertising.
|
||||
cg.add(
|
||||
var.set_advertising_required(
|
||||
CONF_MANUFACTURER_DATA in config
|
||||
or any(
|
||||
not uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID)
|
||||
for service_config in config[CONF_SERVICES]
|
||||
)
|
||||
)
|
||||
)
|
||||
if CONF_MANUFACTURER_DATA in config:
|
||||
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
|
||||
for service_config in config[CONF_SERVICES]:
|
||||
|
||||
@@ -81,6 +81,7 @@ void BLEServer::loop() {
|
||||
if (this->device_information_service_->is_running()) {
|
||||
this->state_ = RUNNING;
|
||||
this->restart_advertising_();
|
||||
this->request_advertising_();
|
||||
ESP_LOGD(TAG, "BLE server setup successfully");
|
||||
} else if (this->device_information_service_->is_created()) {
|
||||
this->device_information_service_->start();
|
||||
@@ -98,6 +99,20 @@ void BLEServer::restart_advertising_() {
|
||||
}
|
||||
}
|
||||
|
||||
void BLEServer::request_advertising_() {
|
||||
if (!this->advertising_required_ || this->advertising_requested_)
|
||||
return;
|
||||
this->advertising_requested_ = true;
|
||||
this->parent_->advertising_start();
|
||||
}
|
||||
|
||||
void BLEServer::release_advertising_() {
|
||||
if (!this->advertising_requested_)
|
||||
return;
|
||||
this->advertising_requested_ = false;
|
||||
this->parent_->advertising_stop();
|
||||
}
|
||||
|
||||
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
@@ -170,7 +185,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
|
||||
this->add_client_(param->connect.conn_id);
|
||||
// Resume advertising so additional clients can discover and connect
|
||||
if (this->client_count_ < this->max_clients_) {
|
||||
this->parent_->advertising_start();
|
||||
this->parent_->advertising_refresh();
|
||||
}
|
||||
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
|
||||
break;
|
||||
@@ -178,7 +193,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
|
||||
case ESP_GATTS_DISCONNECT_EVT: {
|
||||
ESP_LOGD(TAG, "BLE Client disconnected");
|
||||
this->remove_client_(param->disconnect.conn_id);
|
||||
this->parent_->advertising_start();
|
||||
this->parent_->advertising_refresh();
|
||||
this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
|
||||
break;
|
||||
}
|
||||
@@ -226,6 +241,8 @@ void BLEServer::remove_client_(uint16_t conn_id) {
|
||||
}
|
||||
|
||||
void BLEServer::ble_before_disabled_event_handler() {
|
||||
// Advertising is re-requested once the server is running again after BLE is re-enabled
|
||||
this->release_advertising_();
|
||||
// Delete all clients
|
||||
this->client_count_ = 0;
|
||||
// Delete all services
|
||||
|
||||
@@ -38,6 +38,13 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
||||
this->restart_advertising_();
|
||||
}
|
||||
|
||||
/** Whether this server needs the device to advertise so clients can find and connect to it.
|
||||
*
|
||||
* False for a server that only hosts services created at runtime (e.g. esp32_improv), which
|
||||
* request advertising themselves for as long as they need it.
|
||||
*/
|
||||
void set_advertising_required(bool required) { this->advertising_required_ = required; }
|
||||
|
||||
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
|
||||
uint8_t get_max_clients() const { return this->max_clients_; }
|
||||
|
||||
@@ -82,6 +89,8 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
||||
};
|
||||
|
||||
void restart_advertising_();
|
||||
void request_advertising_();
|
||||
void release_advertising_();
|
||||
|
||||
int8_t find_client_index_(uint16_t conn_id) const;
|
||||
void add_client_(uint16_t conn_id);
|
||||
@@ -93,6 +102,8 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
||||
std::vector<uint8_t> manufacturer_data_{};
|
||||
esp_gatt_if_t gatts_if_{0};
|
||||
bool registered_{false};
|
||||
bool advertising_required_{true};
|
||||
bool advertising_requested_{false};
|
||||
|
||||
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
|
||||
uint8_t client_count_{0};
|
||||
|
||||
@@ -112,6 +112,7 @@ void ESP32ImprovComponent::loop() {
|
||||
this->state_callback_.call(this->state_, this->error_state_);
|
||||
#endif
|
||||
}
|
||||
this->release_advertising_();
|
||||
this->incoming_data_.clear();
|
||||
return;
|
||||
}
|
||||
@@ -143,8 +144,9 @@ void ESP32ImprovComponent::loop() {
|
||||
ESP_LOGV(TAG, "Starting with device name advertising");
|
||||
this->advertising_device_name_ = true;
|
||||
this->last_name_adv_time_ = App.get_loop_component_start_time();
|
||||
// Set the payload before requesting, so advertising starts exactly once
|
||||
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
|
||||
esp32_ble::global_ble->advertising_start();
|
||||
this->request_advertising_();
|
||||
|
||||
// Set initial state based on whether we have an authorizer
|
||||
this->set_state_(this->get_initial_state_(), false);
|
||||
@@ -326,6 +328,8 @@ void ESP32ImprovComponent::stop() {
|
||||
this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
|
||||
if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
|
||||
return;
|
||||
// Release first so removing the service UUID does not restart advertising on the way out
|
||||
this->release_advertising_();
|
||||
this->service_->stop();
|
||||
this->set_state_(improv::STATE_STOPPED);
|
||||
});
|
||||
@@ -520,6 +524,20 @@ void ESP32ImprovComponent::update_advertising_type_() {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32ImprovComponent::request_advertising_() {
|
||||
if (this->advertising_requested_)
|
||||
return;
|
||||
this->advertising_requested_ = true;
|
||||
esp32_ble::global_ble->advertising_start();
|
||||
}
|
||||
|
||||
void ESP32ImprovComponent::release_advertising_() {
|
||||
if (!this->advertising_requested_)
|
||||
return;
|
||||
this->advertising_requested_ = false;
|
||||
esp32_ble::global_ble->advertising_stop();
|
||||
}
|
||||
|
||||
improv::State ESP32ImprovComponent::get_initial_state_() const {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// If we have an authorizer, start in awaiting authorization state
|
||||
|
||||
@@ -104,8 +104,11 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
|
||||
bool status_indicator_state_{false};
|
||||
uint32_t last_name_adv_time_{0};
|
||||
bool advertising_device_name_{false};
|
||||
bool advertising_requested_{false};
|
||||
void set_status_indicator_state_(bool state);
|
||||
void update_advertising_type_();
|
||||
void request_advertising_();
|
||||
void release_advertising_();
|
||||
|
||||
void set_state_(improv::State state, bool update_advertising = true);
|
||||
void set_error_(improv::Error error);
|
||||
|
||||
@@ -407,7 +407,10 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
tv.tv_usec = 0;
|
||||
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
this->client_->setblocking(true);
|
||||
if (this->client_->setblocking(true) != 0) {
|
||||
this->log_socket_error_(LOG_STR("blocking"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
// Acknowledge auth OK - 1 byte
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
@@ -59,13 +59,15 @@ int BSDSocketImpl::close() {
|
||||
|
||||
int BSDSocketImpl::setblocking(bool blocking) {
|
||||
int fl = ::fcntl(this->fd_, F_GETFL, 0);
|
||||
if (fl < 0) {
|
||||
return fl;
|
||||
}
|
||||
if (blocking) {
|
||||
fl &= ~O_NONBLOCK;
|
||||
} else {
|
||||
fl |= O_NONBLOCK;
|
||||
}
|
||||
::fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
return ::fcntl(this->fd_, F_SETFL, fl);
|
||||
}
|
||||
|
||||
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
|
||||
@@ -205,6 +205,13 @@ static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN
|
||||
static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN
|
||||
#endif
|
||||
|
||||
/// Outcome of polling a non-blocking connect(); see socket::poll_connect().
|
||||
enum class ConnectPollResult : uint8_t {
|
||||
CONNECT_POLL_PENDING,
|
||||
CONNECT_POLL_CONNECTED,
|
||||
CONNECT_POLL_ERROR,
|
||||
};
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
#endif
|
||||
|
||||
@@ -48,8 +48,33 @@ static const char *const TAG = "socket";
|
||||
#ifdef USE_ESP8266
|
||||
// optimistic_yield() rate limit in microseconds of CONT time; cheap when hot.
|
||||
static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
|
||||
// Let SYS run so queued WiFi traffic reaches lwip; CONT and SYS are cooperative
|
||||
static inline void yield_to_sys() { optimistic_yield(ESP8266_YIELD_INTERVAL_US); }
|
||||
#else
|
||||
static inline void yield_to_sys() {}
|
||||
#endif
|
||||
|
||||
// errno for a failed tcp_* call
|
||||
static int lwip_err_to_errno(err_t err) {
|
||||
switch (err) {
|
||||
case ERR_MEM:
|
||||
return ENOMEM;
|
||||
case ERR_BUF:
|
||||
return EAGAIN; // no free local port
|
||||
case ERR_RTE:
|
||||
return EHOSTUNREACH; // no route or no address yet
|
||||
case ERR_VAL:
|
||||
case ERR_ARG:
|
||||
return EINVAL;
|
||||
case ERR_USE:
|
||||
return EADDRINUSE;
|
||||
case ERR_ISCONN:
|
||||
return EISCONN;
|
||||
default:
|
||||
return EIO;
|
||||
}
|
||||
}
|
||||
|
||||
// set to 1 to enable verbose lwip logging
|
||||
#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
|
||||
#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
|
||||
@@ -63,7 +88,8 @@ static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
|
||||
// tcp_abort() triggers the err callback synchronously, which would
|
||||
// otherwise call back into a partially-destroyed object.
|
||||
// tcp_sent/tcp_poll are not cleared because this implementation
|
||||
// never registers them.
|
||||
// never registers them, and the tcp_connect callback only fires on
|
||||
// SYN_SENT -> ESTABLISHED, which cannot follow an abort or close.
|
||||
static void pcb_detach_abort(struct tcp_pcb *pcb) {
|
||||
tcp_arg(pcb, nullptr);
|
||||
tcp_recv(pcb, nullptr);
|
||||
@@ -76,8 +102,8 @@ static void pcb_detach_abort(struct tcp_pcb *pcb) {
|
||||
// After tcp_close(), the PCB remains alive during the TCP close handshake
|
||||
// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP
|
||||
// would call recv/err on a destroyed socket object, corrupting the heap.
|
||||
// tcp_sent/tcp_poll are not cleared because this implementation
|
||||
// never registers them.
|
||||
// tcp_sent/tcp_poll and the tcp_connect callback are not cleared for the
|
||||
// same reasons as in pcb_detach_abort().
|
||||
// Returns ERR_OK on success; on failure the PCB is aborted instead.
|
||||
static err_t pcb_detach_close(struct tcp_pcb *pcb) {
|
||||
tcp_arg(pcb, nullptr);
|
||||
@@ -101,67 +127,51 @@ LWIPRawCommon::~LWIPRawCommon() {
|
||||
}
|
||||
}
|
||||
|
||||
bool LWIPRawCommon::sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const {
|
||||
if (name == nullptr) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
if (this->family_ == AF_INET6) {
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
|
||||
*port = ntohs(addr6->sin6_port);
|
||||
inet6_addr_to_ip6addr(ip_2_ip6(ip), &addr6->sin6_addr);
|
||||
// ANY lets bind() accept both families; connect() picks the concrete type
|
||||
IP_SET_TYPE_VAL(*ip, IPADDR_TYPE_ANY);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (this->family_ != AF_INET || addrlen < sizeof(sockaddr_in)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
|
||||
*port = ntohs(addr4->sin_port);
|
||||
ip_addr_set_ip4_u32(ip, addr4->sin_addr.s_addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) {
|
||||
LWIP_LOCK();
|
||||
if (this->pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
if (name == nullptr) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
ip_addr_t ip;
|
||||
in_port_t port;
|
||||
#if LWIP_IPV6
|
||||
if (this->family_ == AF_INET) {
|
||||
if (addrlen < sizeof(sockaddr_in)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
|
||||
port = ntohs(addr4->sin_port);
|
||||
ip.type = IPADDR_TYPE_V4;
|
||||
ip.u_addr.ip4.addr = addr4->sin_addr.s_addr;
|
||||
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port);
|
||||
} else if (this->family_ == AF_INET6) {
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
|
||||
port = ntohs(addr6->sin6_port);
|
||||
ip.type = IPADDR_TYPE_ANY;
|
||||
memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16);
|
||||
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port);
|
||||
} else {
|
||||
errno = EINVAL;
|
||||
uint16_t port;
|
||||
if (!this->sockaddr2ip_(name, addrlen, &ip, &port)) {
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
if (this->family_ != AF_INET) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
|
||||
port = ntohs(addr4->sin_port);
|
||||
ip.addr = addr4->sin_addr.s_addr;
|
||||
LWIP_LOG("tcp_bind(%p ip=%u port=%u)", this->pcb_, ip.addr, port);
|
||||
#endif
|
||||
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
|
||||
err_t err = tcp_bind(this->pcb_, &ip, port);
|
||||
if (err == ERR_USE) {
|
||||
LWIP_LOG(" -> err ERR_USE");
|
||||
errno = EADDRINUSE;
|
||||
return -1;
|
||||
}
|
||||
if (err == ERR_VAL) {
|
||||
LWIP_LOG(" -> err ERR_VAL");
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = EIO;
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -178,7 +188,7 @@ int LWIPRawCommon::close() {
|
||||
this->pcb_ = nullptr;
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = err == ERR_MEM ? ENOMEM : EIO;
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -205,7 +215,7 @@ int LWIPRawCommon::shutdown(int how) {
|
||||
err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx);
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = err == ERR_MEM ? ENOMEM : EIO;
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -425,7 +435,79 @@ void LWIPRawImpl::s_err_fn(void *arg, err_t err) {
|
||||
// ERR_ABRT: aborted through tcp_abort or TCP timer
|
||||
auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
|
||||
ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err);
|
||||
if (arg_this->connect_err_ == EINPROGRESS) {
|
||||
// A connect that never established: RST is a refusal, anything else is
|
||||
// the SYN retransmits giving up. Written before pcb_ so poll_connect()
|
||||
// never sees a dead pcb without its reason.
|
||||
arg_this->connect_err_ = err == ERR_RST ? ECONNREFUSED : ETIMEDOUT;
|
||||
}
|
||||
arg_this->pcb_ = nullptr;
|
||||
esphome::wake_loop_any_context();
|
||||
}
|
||||
|
||||
err_t LWIPRawImpl::s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err) {
|
||||
// LWIP CALLBACK — same constraints as s_err_fn. err is always ERR_OK; a
|
||||
// failed connect arrives through s_err_fn instead.
|
||||
auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
|
||||
arg_this->connect_err_ = 0;
|
||||
esphome::wake_loop_any_context();
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
int LWIPRawImpl::connect(const struct sockaddr *addr, socklen_t addrlen) {
|
||||
LWIP_LOCK();
|
||||
if (this->pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
if (this->connect_err_ != 0) {
|
||||
errno = EALREADY;
|
||||
return -1;
|
||||
}
|
||||
ip_addr_t ip;
|
||||
uint16_t port;
|
||||
if (!this->sockaddr2ip_(addr, addrlen, &ip, &port)) {
|
||||
return -1;
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
// tcp_connect needs a concrete address type. A remembered IPv4 peer on an
|
||||
// IPv6 build arrives as a v4-mapped address and must be dialed as IPv4.
|
||||
if (IP_IS_ANY_TYPE_VAL(ip)) {
|
||||
if (ip6_addr_isipv4mappedipv6(ip_2_ip6(&ip))) {
|
||||
unmap_ipv4_mapped_ipv6(ip_2_ip4(&ip), ip_2_ip6(&ip));
|
||||
IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V4);
|
||||
} else {
|
||||
IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V6);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
LWIP_LOG("tcp_connect(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
|
||||
err_t err = tcp_connect(this->pcb_, &ip, port, LWIPRawImpl::s_connected_fn);
|
||||
if (err != ERR_OK) {
|
||||
LWIP_LOG(" -> err %d", err);
|
||||
errno = lwip_err_to_errno(err);
|
||||
return -1;
|
||||
}
|
||||
this->connect_err_ = EINPROGRESS;
|
||||
errno = EINPROGRESS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
ConnectPollResult LWIPRawImpl::poll_connect(int &err_out) const {
|
||||
// pcb_ first; see the ordering note on the declaration
|
||||
if (this->pcb_ == nullptr) {
|
||||
err_out = this->connect_err_ == 0 || this->connect_err_ == EINPROGRESS ? ECONNRESET : this->connect_err_;
|
||||
return ConnectPollResult::CONNECT_POLL_ERROR;
|
||||
}
|
||||
if (this->connect_err_ == EINPROGRESS) {
|
||||
yield_to_sys(); // so the SYN-ACK is processed between polls
|
||||
return ConnectPollResult::CONNECT_POLL_PENDING;
|
||||
}
|
||||
if (this->connect_err_ != 0) {
|
||||
err_out = this->connect_err_;
|
||||
return ConnectPollResult::CONNECT_POLL_ERROR;
|
||||
}
|
||||
return ConnectPollResult::CONNECT_POLL_CONNECTED;
|
||||
}
|
||||
|
||||
err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
|
||||
@@ -540,14 +622,12 @@ ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) {
|
||||
}
|
||||
|
||||
ssize_t LWIPRawImpl::read(void *buf, size_t len) {
|
||||
#ifdef USE_ESP8266
|
||||
// Would block: yield to SYS so queued WiFi RX reaches lwip and this read
|
||||
// may succeed. Without this, inbound segments can sit unprocessed for
|
||||
// seconds while the main loop polls (CONT/SYS are cooperative on ESP8266).
|
||||
// Would block: let queued WiFi RX reach lwip first so this read may
|
||||
// succeed; otherwise inbound segments can sit unprocessed for seconds
|
||||
// while the main loop polls
|
||||
if (this->waiting_for_data_()) {
|
||||
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
|
||||
yield_to_sys();
|
||||
}
|
||||
#endif
|
||||
// See waiting_for_data_() for safety of unlocked reads.
|
||||
if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
|
||||
this->wait_for_data_();
|
||||
@@ -636,12 +716,10 @@ int LWIPRawImpl::internal_output_() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
// Flushed: yield to SYS so the queued segments reach the WiFi driver
|
||||
// instead of waiting seconds for an unrelated SYS slot. Callers only get
|
||||
// here after a successful tcp_write, so idle paths never yield.
|
||||
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
|
||||
#endif
|
||||
yield_to_sys();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ class LWIPRawCommon {
|
||||
|
||||
protected:
|
||||
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);
|
||||
/// Convert a sockaddr of this socket's family to an lwip address and port.
|
||||
/// Returns false with errno set on a family or length mismatch.
|
||||
bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const;
|
||||
|
||||
// Member ordering optimized to minimize padding on 32-bit systems
|
||||
struct tcp_pcb *pcb_;
|
||||
@@ -58,7 +61,16 @@ class LWIPRawCommon {
|
||||
bool nodelay_ = false;
|
||||
sa_family_t family_ = 0;
|
||||
uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
|
||||
// State of a connect() started on this socket: 0 when none is pending (or
|
||||
// it completed), EINPROGRESS while the SYN is out, otherwise the errno the
|
||||
// lwip callbacks recorded for its failure. Fits the padding byte here.
|
||||
uint8_t connect_err_ = 0;
|
||||
static_assert(EINPROGRESS < 256 && ECONNREFUSED < 256 && ECONNRESET < 256 && ETIMEDOUT < 256,
|
||||
"connect_err_ stores errno values in a byte");
|
||||
};
|
||||
// The connect state must stay inside the padding: no socket, listening or
|
||||
// accepted, pays RAM for it
|
||||
static_assert(sizeof(LWIPRawCommon) == sizeof(struct tcp_pcb *) + 4, "LWIPRawCommon grew past one word of flags");
|
||||
|
||||
/// Connected socket implementation for LWIP raw TCP.
|
||||
/// No virtual methods — callers always use the concrete type.
|
||||
@@ -83,6 +95,13 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
errno = EOPNOTSUPP;
|
||||
return -1;
|
||||
}
|
||||
/// Start a non-blocking connect. Always returns -1 with errno EINPROGRESS
|
||||
/// when the SYN was queued; completion is reported by poll_connect().
|
||||
int connect(const struct sockaddr *addr, socklen_t addrlen);
|
||||
// Intentionally unlocked like ready(): reads one pointer and one byte that
|
||||
// the callbacks write in the order the checks depend on (error byte first,
|
||||
// then pcb_), so a torn read only costs one extra poll.
|
||||
ConnectPollResult poll_connect(int &err_out) const;
|
||||
ssize_t read(void *buf, size_t len);
|
||||
ssize_t readv(const struct iovec *iov, int iovcnt);
|
||||
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
|
||||
@@ -120,6 +139,7 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
|
||||
static void s_err_fn(void *arg, err_t err);
|
||||
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
|
||||
static err_t s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err);
|
||||
|
||||
protected:
|
||||
// True when the socket could receive data but none has arrived yet.
|
||||
@@ -137,6 +157,8 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
size_t rx_buf_offset_ = 0;
|
||||
bool rx_closed_ = false;
|
||||
};
|
||||
static_assert(sizeof(LWIPRawImpl) == sizeof(LWIPRawCommon) + sizeof(pbuf *) + sizeof(size_t) + sizeof(void *),
|
||||
"LWIPRawImpl layout changed");
|
||||
|
||||
/// Listening socket implementation for LWIP raw TCP.
|
||||
/// Separate from LWIPRawImpl — no virtual dispatch needed.
|
||||
|
||||
@@ -49,13 +49,15 @@ int LwIPSocketImpl::close() {
|
||||
|
||||
int LwIPSocketImpl::setblocking(bool blocking) {
|
||||
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
|
||||
if (fl < 0) {
|
||||
return fl;
|
||||
}
|
||||
if (blocking) {
|
||||
fl &= ~O_NONBLOCK;
|
||||
} else {
|
||||
fl |= O_NONBLOCK;
|
||||
}
|
||||
lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
return lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
}
|
||||
|
||||
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
#include <string>
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
@@ -165,7 +168,10 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
|
||||
#else
|
||||
// Use LWIP-specific functions
|
||||
ip6_addr_t ip6;
|
||||
inet6_aton(ip_address, &ip6);
|
||||
if (inet6_aton(ip_address, &ip6) == 0) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
||||
#endif
|
||||
return sizeof(sockaddr_in6);
|
||||
@@ -185,12 +191,58 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
server->sin_addr.s_addr = inet_addr(ip_address);
|
||||
// Unlike inet_addr(), inet_aton() can signal failure while still
|
||||
// accepting the broadcast address 255.255.255.255
|
||||
if (inet_aton(ip_address, &server->sin_addr) == 0) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
server->sin_port = htons(port);
|
||||
return sizeof(sockaddr_in);
|
||||
}
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
ConnectPollResult poll_connect(Socket &sock, int &err_out) {
|
||||
int fd = sock.get_fd();
|
||||
if (fd < 0 || fd >= FD_SETSIZE) {
|
||||
// FD_SET on either is undefined behavior
|
||||
err_out = EBADF;
|
||||
return ConnectPollResult::CONNECT_POLL_ERROR;
|
||||
}
|
||||
// Connect completion is a write event; the main loop only selects on reads
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
struct timeval tv = {0, 0};
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
// LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly
|
||||
int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
#else
|
||||
// Global-scope select: the entity namespace esphome::select shadows it here
|
||||
int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
#endif
|
||||
if (ret < 0) {
|
||||
err_out = errno;
|
||||
return ConnectPollResult::CONNECT_POLL_ERROR;
|
||||
}
|
||||
if (ret == 0) {
|
||||
return ConnectPollResult::CONNECT_POLL_PENDING;
|
||||
}
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) {
|
||||
err_out = errno;
|
||||
return ConnectPollResult::CONNECT_POLL_ERROR;
|
||||
}
|
||||
if (error != 0) {
|
||||
err_out = error;
|
||||
return ConnectPollResult::CONNECT_POLL_ERROR;
|
||||
}
|
||||
return ConnectPollResult::CONNECT_POLL_CONNECTED;
|
||||
}
|
||||
#endif
|
||||
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
|
||||
#if USE_NETWORK_IPV6
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
|
||||
@@ -145,6 +145,16 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
|
||||
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
|
||||
|
||||
/// Check a non-blocking connect() for completion without blocking. On
|
||||
/// CONNECT_POLL_ERROR, err_out holds the socket's SO_ERROR (or errno when the
|
||||
/// poll itself failed) on fd based implementations, and the failure recorded
|
||||
/// by the lwip callbacks on the raw lwip implementation.
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
inline ConnectPollResult poll_connect(Socket &sock, int &err_out) { return sock.poll_connect(err_out); }
|
||||
#else
|
||||
ConnectPollResult poll_connect(Socket &sock, int &err_out);
|
||||
#endif
|
||||
|
||||
/// Format sockaddr into caller-provided buffer, returns length written (excluding null)
|
||||
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf);
|
||||
|
||||
|
||||
@@ -13,7 +13,12 @@ void UDPComponent::setup() {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
for (const auto &address : this->addresses_) {
|
||||
struct sockaddr saddr {};
|
||||
socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_);
|
||||
if (socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_) == 0) {
|
||||
ESP_LOGW(TAG, "Invalid address %s", address);
|
||||
// A dropped address silently receives nothing; surface the misconfiguration
|
||||
this->status_set_warning(LOG_STR("invalid address"));
|
||||
continue;
|
||||
}
|
||||
this->sockaddrs_.push_back(saddr);
|
||||
}
|
||||
// set up broadcast socket
|
||||
@@ -94,7 +99,11 @@ void UDPComponent::setup() {
|
||||
// 8266 and RP2040 `Duino
|
||||
for (const auto &address : this->addresses_) {
|
||||
auto ipaddr = IPAddress();
|
||||
ipaddr.fromString(address);
|
||||
if (!ipaddr.fromString(address)) {
|
||||
ESP_LOGW(TAG, "Invalid address %s", address);
|
||||
this->status_set_warning(LOG_STR("invalid address"));
|
||||
continue;
|
||||
}
|
||||
this->ipaddrs_.push_back(ipaddr);
|
||||
}
|
||||
if (this->should_listen_)
|
||||
|
||||
@@ -434,11 +434,12 @@ void USBUartTypeCdcAcm::on_connected() {
|
||||
auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_,
|
||||
channel->cdc_dev_.interrupt_interface_number, 0);
|
||||
if (err_comm != ESP_OK) {
|
||||
// Continue anyway: the interface number stays valid for CDC request addressing
|
||||
ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number,
|
||||
esp_err_to_name(err_comm));
|
||||
channel->cdc_dev_.interrupt_interface_number = 0xFF; // Mark as unavailable, but continue anyway
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number);
|
||||
channel->cdc_dev_.interrupt_interface_claimed = true;
|
||||
}
|
||||
}
|
||||
auto err =
|
||||
@@ -465,14 +466,15 @@ void USBUartTypeCdcAcm::on_disconnected() {
|
||||
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
|
||||
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
|
||||
}
|
||||
if (channel->cdc_dev_.notify_ep != nullptr) {
|
||||
// Only tear down the notify pipe when we claimed its interface ourselves;
|
||||
// no transfer is ever submitted on it, so there is nothing else to cancel.
|
||||
if (channel->cdc_dev_.notify_ep != nullptr && channel->cdc_dev_.interrupt_interface_claimed) {
|
||||
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
|
||||
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
|
||||
}
|
||||
if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
|
||||
channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
|
||||
if (channel->cdc_dev_.interrupt_interface_claimed) {
|
||||
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number);
|
||||
channel->cdc_dev_.interrupt_interface_number = 0xFF;
|
||||
channel->cdc_dev_.interrupt_interface_claimed = false;
|
||||
}
|
||||
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
|
||||
// Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts
|
||||
|
||||
@@ -34,7 +34,10 @@ struct CdcEps {
|
||||
const usb_ep_desc_t *in_ep;
|
||||
const usb_ep_desc_t *out_ep;
|
||||
uint8_t bulk_interface_number;
|
||||
// Also the wIndex target for CDC class requests (SET_LINE_CODING etc.), so it
|
||||
// must remain valid even when the interface itself is not claimed.
|
||||
uint8_t interrupt_interface_number;
|
||||
bool interrupt_interface_claimed{false};
|
||||
};
|
||||
|
||||
enum CH34xChipType : uint8_t {
|
||||
|
||||
@@ -34,6 +34,10 @@ void WakeOnLanButton::press_action() {
|
||||
struct sockaddr_storage saddr {};
|
||||
auto addr_len =
|
||||
socket::set_sockaddr(reinterpret_cast<sockaddr *>(&saddr), sizeof(saddr), "255.255.255.255", this->port_);
|
||||
if (addr_len == 0) {
|
||||
ESP_LOGW(TAG, "Invalid broadcast address");
|
||||
return;
|
||||
}
|
||||
uint8_t buffer[6 + sizeof this->macaddr_ * 16];
|
||||
memcpy(buffer, PREFIX, sizeof(PREFIX));
|
||||
for (size_t i = 0; i != 16; i++) {
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ bleak==3.0.2
|
||||
smpclient==7.2.0
|
||||
requests==2.34.2
|
||||
py7zr==1.1.3
|
||||
platformdirs==4.11.5 # native esp-idf toolchain global cache dir
|
||||
platformdirs==4.11.7 # native esp-idf toolchain global cache dir
|
||||
ninja==1.13.2 # native esp8266 arduino toolchain build driver
|
||||
filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ pylint==4.0.8
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.16.5 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
prek==0.5.0 # also change in .github/workflows/ci.yml when updating
|
||||
prek==0.5.1 # also change in .github/workflows/ci.yml when updating
|
||||
|
||||
# Unit tests
|
||||
pytest==9.1.1
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# esp32_ble_server is only auto-loaded here, so it has no services of its own.
|
||||
esp32_improv:
|
||||
authorizer: none
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
esp32_ble_server:
|
||||
id: ble_server
|
||||
manufacturer_data: [0x72, 0x04, 0x00, 0x23]
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
esp32_ble_server:
|
||||
id: ble_server
|
||||
services:
|
||||
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d
|
||||
characteristics:
|
||||
- uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423
|
||||
read: true
|
||||
value: [1, 2, 3, 4]
|
||||
@@ -1,5 +1,10 @@
|
||||
"""Tests for esp32_ble_server configuration helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32_ble_server import (
|
||||
@@ -45,3 +50,26 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
|
||||
assert uuid_is(uuid16, uuid16)
|
||||
assert uuid_is(f"{uuid16:04X}", uuid16)
|
||||
assert uuid_is(f"{uuid16:08X}", uuid16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "required"),
|
||||
[
|
||||
# Auto-loaded by esp32_improv only: nothing to find until Improv asks for it
|
||||
("improv_only.yaml", False),
|
||||
# The configuration defines a service clients are meant to connect to
|
||||
("own_service.yaml", True),
|
||||
# Manufacturer data is only useful if it is actually broadcast
|
||||
("manufacturer_data_only.yaml", True),
|
||||
],
|
||||
)
|
||||
def test_advertising_required(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
required: bool,
|
||||
) -> None:
|
||||
"""The server only requests advertising when the configuration needs it."""
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
assert f"set_advertising_required({str(required).lower()})" in main_cpp
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
substitutions:
|
||||
network_enable_ipv6: "true"
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user