1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-06 13:22:19 +01:00

Merge remote-tracking branch 'upstream/dev' into esp32_ble_tracker_cleanup_code

This commit is contained in:
J. Nick Koston
2025-08-04 15:24:55 -10:00
18 changed files with 391 additions and 112 deletions

View File

@@ -56,6 +56,14 @@ class CustomAPIDevice {
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#else
template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) {
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
}
#endif #endif
/** Register a custom native API service that will show up in Home Assistant. /** Register a custom native API service that will show up in Home Assistant.
@@ -81,6 +89,12 @@ class CustomAPIDevice {
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#else
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
}
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
@@ -135,6 +149,22 @@ class CustomAPIDevice {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f); global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
} }
#else
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -222,6 +252,28 @@ class CustomAPIDevice {
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
#else
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void>
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void>
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
#endif #endif
}; };

View File

@@ -1,14 +1,20 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import BTLoggers from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_ID from esphome.const import CONF_ACTIVE, CONF_ID
from esphome.core import CORE
from esphome.log import AnsiFore, color
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
DEPENDENCIES = ["api", "esp32"] DEPENDENCIES = ["api", "esp32"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
_LOGGER = logging.getLogger(__name__)
CONF_CONNECTION_SLOTS = "connection_slots" CONF_CONNECTION_SLOTS = "connection_slots"
CONF_CACHE_SERVICES = "cache_services" CONF_CACHE_SERVICES = "cache_services"
CONF_CONNECTIONS = "connections" CONF_CONNECTIONS = "connections"
@@ -41,6 +47,27 @@ def validate_connections(config):
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
config config
) )
# Warn about connection slot waste when using Arduino framework
if CORE.using_arduino and connection_slots:
_LOGGER.warning(
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
"\n"
"To switch to ESP-IDF, add this to your YAML:\n"
" esp32:\n"
" framework:\n"
" type: esp-idf\n"
"\n"
"For detailed migration instructions, see:\n"
"%s",
color(
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
),
)
return { return {
**config, **config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)], CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],

View File

@@ -35,8 +35,8 @@ void BluetoothProxy::setup() {
// Don't pre-allocate pool - let it grow only if needed in busy environments // Don't pre-allocate pool - let it grow only if needed in busy environments
// Many devices in quiet areas will never need the overflow pool // Many devices in quiet areas will never need the overflow pool
this->connections_free_response_.limit = this->connections_.size(); this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS;
this->connections_free_response_.free = this->connections_.size(); this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) { if (this->api_connection_ != nullptr) {
@@ -134,12 +134,13 @@ void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Active: %s\n" " Active: %s\n"
" Connections: %d", " Connections: %d",
YESNO(this->active_), this->connections_.size()); YESNO(this->active_), this->connection_count_);
} }
void BluetoothProxy::loop() { void BluetoothProxy::loop() {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
for (auto *connection : this->connections_) { for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) { if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect(); connection->disconnect();
} }
@@ -162,7 +163,8 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
} }
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
for (auto *connection : this->connections_) { for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == address) if (connection->get_address() == address)
return connection; return connection;
} }
@@ -170,7 +172,8 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
if (!reserve) if (!reserve)
return nullptr; return nullptr;
for (auto *connection : this->connections_) { for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == 0) { if (connection->get_address() == 0) {
connection->send_service_ = DONE_SENDING_SERVICES; connection->send_service_ = DONE_SENDING_SERVICES;
connection->set_address(address); connection->set_address(address);

View File

@@ -2,6 +2,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <array>
#include <map> #include <map>
#include <vector> #include <vector>
@@ -63,9 +64,11 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) { void register_connection(BluetoothConnection *connection) {
this->connections_.push_back(connection); if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
this->connections_[this->connection_count_++] = connection;
connection->proxy_ = this; connection->proxy_ = this;
} }
}
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg); void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg);
@@ -138,8 +141,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
// Group 1: Pointers (4 bytes each, naturally aligned) // Group 1: Pointers (4 bytes each, naturally aligned)
api::APIConnection *api_connection_{nullptr}; api::APIConnection *api_connection_{nullptr};
// Group 2: Container types (typically 12 bytes on 32-bit) // Group 2: Fixed-size array of connection pointers
std::vector<BluetoothConnection *> connections_{}; std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
// BLE advertisement batching // BLE advertisement batching
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_; std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
@@ -154,7 +157,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
// Group 4: 1-byte types grouped together // Group 4: 1-byte types grouped together
bool active_; bool active_;
uint8_t advertisement_count_{0}; uint8_t advertisement_count_{0};
// 2 bytes used, 2 bytes padding uint8_t connection_count_{0};
// 3 bytes used, 1 byte padding
}; };
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -23,21 +23,14 @@
namespace esphome::esp32_ble { namespace esphome::esp32_ble {
// Maximum number of BLE scan results to buffer // Maximum size of the BLE event queue
// Sized to handle bursts of advertisements while allowing for processing delays // Increased to absorb the ring buffer capacity from esp32_ble_tracker
// With 16 advertisements per batch and some safety margin:
// - Without PSRAM: 24 entries (1.5× batch size)
// - With PSRAM: 36 entries (2.25× batch size)
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
#ifdef USE_PSRAM #ifdef USE_PSRAM
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM)
#else #else
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM)
#endif #endif
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
// NOLINTNEXTLINE(modernize-use-using) // NOLINTNEXTLINE(modernize-use-using)

View File

@@ -145,15 +145,7 @@ void BLEClientBase::connect() {
this->remote_addr_type_); this->remote_addr_type_);
this->paired_ = false; this->paired_ = false;
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); // Set preferred connection parameters before connecting
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
// Always set connection parameters to ensure stable operation
// Use FAST for all V3 connections (better latency and reliability) // Use FAST for all V3 connections (better latency and reliability)
// Use MEDIUM for V1/legacy connections (balanced performance) // Use MEDIUM for V1/legacy connections (balanced performance)
uint16_t min_interval, max_interval, timeout; uint16_t min_interval, max_interval, timeout;
@@ -181,6 +173,15 @@ void BLEClientBase::connect() {
} else { } else {
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
} }
// Now open the connection
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
} }
} }
@@ -255,6 +256,19 @@ void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
} }
void BLEClientBase::restore_medium_conn_params_() {
// Restore to medium connection parameters after initial connection phase
// This balances performance with bandwidth usage for normal operation
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
conn_params.latency = 0;
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
ESP_LOGD(TAG, "[%d] [%s] Restoring medium conn params", this->connection_index_, this->address_str_.c_str());
esp_ble_gap_update_conn_params(&conn_params);
}
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
@@ -283,7 +297,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (!this->check_addr(param->open.remote_bda)) if (!this->check_addr(param->open.remote_bda))
return false; return false;
this->log_event_("ESP_GATTC_OPEN_EVT"); this->log_event_("ESP_GATTC_OPEN_EVT");
this->conn_id_ = param->open.conn_id; // conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0; this->service_count_ = 0;
if (this->state_ != espbt::ClientState::CONNECTING) { if (this->state_ != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does // This should not happen but lets log it in case it does
@@ -317,15 +331,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->conn_id_ = UNSET_CONN_ID; this->conn_id_ = UNSET_CONN_ID;
break; break;
} }
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); // MTU negotiation already started in ESP_GATTC_CONNECT_EVT
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
this->address_str_.c_str(), ret);
}
this->set_state(espbt::ClientState::CONNECTED); this->set_state(espbt::ClientState::CONNECTED);
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str()); ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str());
// Restore to medium connection parameters for cached connections too
this->restore_medium_conn_params_();
// only set our state, subclients might have more stuff to do yet. // only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED; this->state_ = espbt::ClientState::ESTABLISHED;
break; break;
@@ -338,6 +352,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (!this->check_addr(param->connect.remote_bda)) if (!this->check_addr(param->connect.remote_bda))
return false; return false;
this->log_event_("ESP_GATTC_CONNECT_EVT"); this->log_event_("ESP_GATTC_CONNECT_EVT");
this->conn_id_ = param->connect.conn_id;
// Start MTU negotiation immediately as recommended by ESP-IDF examples
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
// ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
// This saves ~3ms in the connection process.
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
this->address_str_.c_str(), ret);
}
break; break;
} }
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_DISCONNECT_EVT: {
@@ -413,15 +437,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// This balances performance with bandwidth usage after the critical discovery phase // This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
esp_ble_conn_update_params_t conn_params = {{0}}; this->restore_medium_conn_params_();
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
conn_params.latency = 0;
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
ESP_LOGD(TAG, "[%d] [%s] Restored medium conn params after service discovery", this->connection_index_,
this->address_str_.c_str());
esp_ble_gap_update_conn_params(&conn_params);
} }
this->state_ = espbt::ClientState::ESTABLISHED; this->state_ = espbt::ClientState::ESTABLISHED;

View File

@@ -66,7 +66,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
(uint8_t) (this->address_ >> 0) & 0xff); (uint8_t) (this->address_ >> 0) & 0xff);
} }
} }
std::string address_str() const { return this->address_str_; } const std::string &address_str() const { return this->address_str_; }
BLEService *get_service(espbt::ESPBTUUID uuid); BLEService *get_service(espbt::ESPBTUUID uuid);
BLEService *get_service(uint16_t uuid); BLEService *get_service(uint16_t uuid);
@@ -127,6 +127,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// 6 bytes used, 2 bytes padding // 6 bytes used, 2 bytes padding
void log_event_(const char *name); void log_event_(const char *name);
void restore_medium_conn_params_();
}; };
} // namespace esp32_ble_client } // namespace esp32_ble_client

View File

@@ -49,7 +49,7 @@ class ESPNowPacket {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
// Constructor for sent data // Constructor for sent data
ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) { ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
this->init_sent_data(info->src_addr, status); this->init_sent_data_(info->src_addr, status);
} }
#else #else
// Constructor for sent data // Constructor for sent data

View File

@@ -20,12 +20,11 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
#define ERROR_CHECK(err) \ #define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \ if ((err) != i2c::ERROR_OK) { \
this->status_set_warning("Communication failure"); \ this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \
return; \ return; \
} }
void GT911Touchscreen::setup() { void GT911Touchscreen::setup() {
i2c::ErrorCode err;
if (this->reset_pin_ != nullptr) { if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup(); this->reset_pin_->setup();
this->reset_pin_->digital_write(false); this->reset_pin_->digital_write(false);
@@ -35,9 +34,14 @@ void GT911Touchscreen::setup() {
this->interrupt_pin_->digital_write(false); this->interrupt_pin_->digital_write(false);
} }
delay(2); delay(2);
this->reset_pin_->digital_write(true); this->reset_pin_->digital_write(true); // wait 50ms after reset
delay(50); // NOLINT this->set_timeout(50, [this] { this->setup_internal_(); });
return;
} }
this->setup_internal_();
}
void GT911Touchscreen::setup_internal_() {
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
// set pre-configured input mode // set pre-configured input mode
this->interrupt_pin_->setup(); this->interrupt_pin_->setup();
@@ -45,7 +49,7 @@ void GT911Touchscreen::setup() {
// check the configuration of the int line. // check the configuration of the int line.
uint8_t data[4]; uint8_t data[4];
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) { if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
this->address_ = SECONDARY_ADDRESS; this->address_ = SECONDARY_ADDRESS;
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
@@ -53,7 +57,7 @@ void GT911Touchscreen::setup() {
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
err = this->read(data, 1); err = this->read(data, 1);
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]); ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]);
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
this->attach_interrupt_(this->interrupt_pin_, this->attach_interrupt_(this->interrupt_pin_,
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
@@ -75,16 +79,24 @@ void GT911Touchscreen::setup() {
} }
} }
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
this->mark_failed("Failed to read calibration"); this->mark_failed("Calibration error");
return; return;
} }
} }
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
this->mark_failed("Failed to communicate"); this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return;
} }
this->setup_done_ = true;
} }
void GT911Touchscreen::update_touches() { void GT911Touchscreen::update_touches() {
this->skip_update_ = true; // skip send touch events by default, set to false after successful error checks
if (!this->setup_done_) {
return;
}
i2c::ErrorCode err; i2c::ErrorCode err;
uint8_t touch_state = 0; uint8_t touch_state = 0;
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
@@ -97,7 +109,6 @@ void GT911Touchscreen::update_touches() {
uint8_t num_of_touches = touch_state & 0x07; uint8_t num_of_touches = touch_state & 0x07;
if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) {
this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet.
return; return;
} }
@@ -107,6 +118,7 @@ void GT911Touchscreen::update_touches() {
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
ERROR_CHECK(err); ERROR_CHECK(err);
this->skip_update_ = false; // All error checks passed, send touch events
for (uint8_t i = 0; i != num_of_touches; i++) { for (uint8_t i = 0; i != num_of_touches; i++) {
uint16_t id = data[i][0]; uint16_t id = data[i][0];
uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t x = encode_uint16(data[i][2], data[i][1]);

View File

@@ -15,8 +15,20 @@ class GT911ButtonListener {
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public: public:
/// @brief Initialize the GT911 touchscreen.
///
/// If @ref reset_pin_ is set, the touchscreen will be hardware reset,
/// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout()
/// to allow the device to stabilize after reset.
///
/// If @ref interrupt_pin_ is set, it will be temporarily configured during reset
/// to control I2C address selection.
///
/// After the timeout, or immediately if no reset is performed, @ref setup_internal_()
/// is called to complete the initialization.
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
bool can_proceed() override { return this->setup_done_; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
@@ -25,8 +37,20 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
protected: protected:
void update_touches() override; void update_touches() override;
InternalGPIOPin *interrupt_pin_{}; /// @brief Perform the internal setup routine for the GT911 touchscreen.
GPIOPin *reset_pin_{}; ///
/// This function checks the I2C address, configures the interrupt pin (if available),
/// reads the touchscreen mode from the controller, and attempts to read calibration
/// data (maximum X and Y values) if not already set.
///
/// On success, sets @ref setup_done_ to true.
/// On failure, calls @ref mark_failed() with an appropriate error message.
void setup_internal_();
/// @brief True if the touchscreen setup has completed successfully.
bool setup_done_{false};
InternalGPIOPin *interrupt_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
std::vector<GT911ButtonListener *> button_listeners_; std::vector<GT911ButtonListener *> button_listeners_;
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
}; };

View File

@@ -24,9 +24,6 @@ static const uint32_t READ_DURATION_MS = 16;
static const size_t TASK_STACK_SIZE = 4096; static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 23; static const ssize_t TASK_PRIORITY = 23;
// Use an exponential moving average to correct a DC offset with weight factor 1/1000
static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
static const char *const TAG = "i2s_audio.microphone"; static const char *const TAG = "i2s_audio.microphone";
enum MicrophoneEventGroupBits : uint32_t { enum MicrophoneEventGroupBits : uint32_t {
@@ -381,26 +378,57 @@ void I2SAudioMicrophone::mic_task(void *params) {
} }
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) { void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
/**
* From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html:
*
* y(n) = x(n) - x(n-1) + R * y(n-1)
* R = 1 - (pi * 2 * frequency / samplerate)
*
* From https://en.wikipedia.org/wiki/Hearing_range:
* The human range is commonly given as 20Hz up.
*
* From https://en.wikipedia.org/wiki/High-resolution_audio:
* A reasonable upper bound for sample rate seems to be 96kHz.
*
* Calculate R value for 20Hz on a 96kHz sample rate:
* R = 1 - (pi * 2 * 20 / 96000)
* R = 0.9986910031
*
* Transform floating point to bit-shifting approximation:
* output = input - prev_input + R * prev_output
* output = input - prev_input + (prev_output - (prev_output >> S))
*
* Approximate bit-shift value S from R:
* R = 1 - (1 >> S)
* R = 1 - (1 / 2^S)
* R = 1 - 2^-S
* 0.9986910031 = 1 - 2^-S
* S = 9.57732 ~= 10
*
* Actual R from S:
* R = 1 - 2^-10 = 0.9990234375
*
* Confirm this has effect outside human hearing on 96000kHz sample:
* 0.9990234375 = 1 - (pi * 2 * f / 96000)
* f = 14.9208Hz
*
* Confirm this has effect outside human hearing on PDM 16kHz sample:
* 0.9990234375 = 1 - (pi * 2 * f / 16000)
* f = 2.4868Hz
*
*/
const uint8_t dc_filter_shift = 10;
const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1); const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size()); const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
if (total_samples == 0) {
return;
}
int64_t offset_accumulator = 0;
for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) { for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
const uint32_t byte_index = sample_index * bytes_per_sample; const uint32_t byte_index = sample_index * bytes_per_sample;
int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
offset_accumulator += sample; int32_t output = input - this->dc_offset_prev_input_ +
sample -= this->dc_offset_; (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift));
audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample); this->dc_offset_prev_input_ = input;
this->dc_offset_prev_output_ = output;
audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample);
} }
const int32_t new_offset = offset_accumulator / total_samples;
this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR +
(DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ /
DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR;
} }
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {

View File

@@ -82,7 +82,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
bool correct_dc_offset_; bool correct_dc_offset_;
bool locked_driver_{false}; bool locked_driver_{false};
int32_t dc_offset_{0}; int32_t dc_offset_prev_input_{0};
int32_t dc_offset_prev_output_{0};
}; };
} // namespace i2s_audio } // namespace i2s_audio

View File

@@ -77,6 +77,7 @@ BRIGHTNESS = 0x51
WRDISBV = 0x51 WRDISBV = 0x51
RDDISBV = 0x52 RDDISBV = 0x52
WRCTRLD = 0x53 WRCTRLD = 0x53
WCE = 0x58
SWIRE1 = 0x5A SWIRE1 = 0x5A
SWIRE2 = 0x5B SWIRE2 = 0x5B
IFMODE = 0xB0 IFMODE = 0xB0
@@ -91,6 +92,7 @@ PWCTR2 = 0xC1
PWCTR3 = 0xC2 PWCTR3 = 0xC2
PWCTR4 = 0xC3 PWCTR4 = 0xC3
PWCTR5 = 0xC4 PWCTR5 = 0xC4
SPIMODESEL = 0xC4
VMCTR1 = 0xC5 VMCTR1 = 0xC5
IFCTR = 0xC6 IFCTR = 0xC6
VMCTR2 = 0xC7 VMCTR2 = 0xC7

View File

@@ -5,10 +5,13 @@ from esphome.components.mipi import (
PAGESEL, PAGESEL,
PIXFMT, PIXFMT,
SLPOUT, SLPOUT,
SPIMODESEL,
SWIRE1, SWIRE1,
SWIRE2, SWIRE2,
TEON, TEON,
WCE,
WRAM, WRAM,
WRCTRLD,
DriverChip, DriverChip,
delay, delay,
) )
@@ -87,4 +90,19 @@ T4_S3_AMOLED = RM690B0.extend(
bus_mode=TYPE_QUAD, bus_mode=TYPE_QUAD,
) )
CO5300 = DriverChip(
"CO5300",
brightness=0xD0,
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
initsequence=(
(SLPOUT,), # Requires early SLPOUT
(PAGESEL, 0x00),
(SPIMODESEL, 0x80),
(WRCTRLD, 0x20),
(WCE, 0x00),
),
)
models = {} models = {}

View File

@@ -1,6 +1,7 @@
from esphome.components.mipi import DriverChip from esphome.components.mipi import DriverChip
import esphome.config_validation as cv import esphome.config_validation as cv
from .amoled import CO5300
from .ili import ILI9488_A from .ili import ILI9488_A
DriverChip( DriverChip(
@@ -140,3 +141,14 @@ ILI9488_A.extend(
data_rate="20MHz", data_rate="20MHz",
invert_colors=True, invert_colors=True,
) )
CO5300.extend(
"WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75",
width=466,
height=466,
pixel_mode="16bit",
offset_height=0,
offset_width=6,
cs_pin=12,
reset_pin=39,
)

View File

@@ -116,7 +116,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
} }
// Handle regular form data // Handle regular form data
if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) {
ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL; return ESP_FAIL;

View File

@@ -373,3 +373,20 @@ button:
name: "Test Button" name: "Test Button"
on_press: on_press:
- logger.log: "Button pressed" - logger.log: "Button pressed"
# Date, Time, and DateTime entities
datetime:
- platform: template
type: date
name: "Test Date"
initial_value: "2023-05-13"
optimistic: true
- platform: template
type: time
name: "Test Time"
initial_value: "12:30:00"
optimistic: true
- platform: template
type: datetime
name: "Test DateTime"
optimistic: true

View File

@@ -4,7 +4,17 @@ from __future__ import annotations
import asyncio import asyncio
from aioesphomeapi import ClimateInfo, EntityState, SensorState from aioesphomeapi import (
ClimateInfo,
DateInfo,
DateState,
DateTimeInfo,
DateTimeState,
EntityState,
SensorState,
TimeInfo,
TimeState,
)
import pytest import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -22,34 +32,56 @@ async def test_host_mode_many_entities(
async with run_compiled(yaml_config), api_client_connected() as client: async with run_compiled(yaml_config), api_client_connected() as client:
# Subscribe to state changes # Subscribe to state changes
states: dict[int, EntityState] = {} states: dict[int, EntityState] = {}
sensor_count_future: asyncio.Future[int] = loop.create_future() minimum_states_future: asyncio.Future[None] = loop.create_future()
def on_state(state: EntityState) -> None: def on_state(state: EntityState) -> None:
states[state.key] = state states[state.key] = state
# Count sensor states specifically # Check if we have received minimum expected states
sensor_states = [ sensor_states = [
s s
for s in states.values() for s in states.values()
if isinstance(s, SensorState) and isinstance(s.state, float) if isinstance(s, SensorState) and isinstance(s.state, float)
] ]
# When we have received states from at least 50 sensors, resolve the future date_states = [s for s in states.values() if isinstance(s, DateState)]
if len(sensor_states) >= 50 and not sensor_count_future.done(): time_states = [s for s in states.values() if isinstance(s, TimeState)]
sensor_count_future.set_result(len(sensor_states)) datetime_states = [
s for s in states.values() if isinstance(s, DateTimeState)
]
# We expect at least 50 sensors and 1 of each datetime entity type
if (
len(sensor_states) >= 50
and len(date_states) >= 1
and len(time_states) >= 1
and len(datetime_states) >= 1
and not minimum_states_future.done()
):
minimum_states_future.set_result(None)
client.subscribe_states(on_state) client.subscribe_states(on_state)
# Wait for states from at least 50 sensors with timeout # Wait for minimum states with timeout
try: try:
sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0) await asyncio.wait_for(minimum_states_future, timeout=10.0)
except TimeoutError: except TimeoutError:
sensor_states = [ sensor_states = [
s s
for s in states.values() for s in states.values()
if isinstance(s, SensorState) and isinstance(s.state, float) if isinstance(s, SensorState) and isinstance(s.state, float)
] ]
date_states = [s for s in states.values() if isinstance(s, DateState)]
time_states = [s for s in states.values() if isinstance(s, TimeState)]
datetime_states = [
s for s in states.values() if isinstance(s, DateTimeState)
]
pytest.fail( pytest.fail(
f"Did not receive states from at least 50 sensors within 10 seconds. " f"Did not receive expected states within 10 seconds. "
f"Received {len(sensor_states)} sensor states out of {len(states)} total states" f"Received: {len(sensor_states)} sensor states (expected >=50), "
f"{len(date_states)} date states (expected >=1), "
f"{len(time_states)} time states (expected >=1), "
f"{len(datetime_states)} datetime states (expected >=1). "
f"Total states: {len(states)}"
) )
# Verify we received a good number of entity states # Verify we received a good number of entity states
@@ -64,13 +96,25 @@ async def test_host_mode_many_entities(
if isinstance(s, SensorState) and isinstance(s.state, float) if isinstance(s, SensorState) and isinstance(s.state, float)
] ]
assert sensor_count >= 50, (
f"Expected at least 50 sensor states, got {sensor_count}"
)
assert len(sensor_states) >= 50, ( assert len(sensor_states) >= 50, (
f"Expected at least 50 sensor states, got {len(sensor_states)}" f"Expected at least 50 sensor states, got {len(sensor_states)}"
) )
# Verify we received datetime entity states
date_states = [s for s in states.values() if isinstance(s, DateState)]
time_states = [s for s in states.values() if isinstance(s, TimeState)]
datetime_states = [s for s in states.values() if isinstance(s, DateTimeState)]
assert len(date_states) >= 1, (
f"Expected at least 1 date state, got {len(date_states)}"
)
assert len(time_states) >= 1, (
f"Expected at least 1 time state, got {len(time_states)}"
)
assert len(datetime_states) >= 1, (
f"Expected at least 1 datetime state, got {len(datetime_states)}"
)
# Get entity info to verify climate entity details # Get entity info to verify climate entity details
entities = await client.list_entities_services() entities = await client.list_entities_services()
climate_infos = [e for e in entities[0] if isinstance(e, ClimateInfo)] climate_infos = [e for e in entities[0] if isinstance(e, ClimateInfo)]
@@ -89,3 +133,28 @@ async def test_host_mode_many_entities(
assert "HOME" in preset_names, f"Expected 'HOME' preset, got {preset_names}" assert "HOME" in preset_names, f"Expected 'HOME' preset, got {preset_names}"
assert "AWAY" in preset_names, f"Expected 'AWAY' preset, got {preset_names}" assert "AWAY" in preset_names, f"Expected 'AWAY' preset, got {preset_names}"
assert "SLEEP" in preset_names, f"Expected 'SLEEP' preset, got {preset_names}" assert "SLEEP" in preset_names, f"Expected 'SLEEP' preset, got {preset_names}"
# Verify datetime entities exist
date_infos = [e for e in entities[0] if isinstance(e, DateInfo)]
time_infos = [e for e in entities[0] if isinstance(e, TimeInfo)]
datetime_infos = [e for e in entities[0] if isinstance(e, DateTimeInfo)]
assert len(date_infos) >= 1, "Expected at least 1 date entity"
assert len(time_infos) >= 1, "Expected at least 1 time entity"
assert len(datetime_infos) >= 1, "Expected at least 1 datetime entity"
# Verify the entity names
date_info = date_infos[0]
assert date_info.name == "Test Date", (
f"Expected date entity name 'Test Date', got {date_info.name}"
)
time_info = time_infos[0]
assert time_info.name == "Test Time", (
f"Expected time entity name 'Test Time', got {time_info.name}"
)
datetime_info = datetime_infos[0]
assert datetime_info.name == "Test DateTime", (
f"Expected datetime entity name 'Test DateTime', got {datetime_info.name}"
)