mirror of
https://github.com/esphome/esphome.git
synced 2025-08-01 07:57:47 +00:00
Merge remote-tracking branch 'upstream/dev' into api_cleanups_2
This commit is contained in:
commit
f9744dabc1
@ -301,8 +301,10 @@ class APIConnection : public APIServerConnection {
|
|||||||
if (entity->has_own_name())
|
if (entity->has_own_name())
|
||||||
msg.name = entity->get_name();
|
msg.name = entity->get_name();
|
||||||
|
|
||||||
// Set common EntityBase properties
|
// Set common EntityBase properties
|
||||||
|
#ifdef USE_ENTITY_ICON
|
||||||
msg.icon = entity->get_icon();
|
msg.icon = entity->get_icon();
|
||||||
|
#endif
|
||||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
|
@ -292,9 +292,13 @@ class InfoResponseProtoMessage : public ProtoMessage {
|
|||||||
uint32_t key{0};
|
uint32_t key{0};
|
||||||
std::string name{};
|
std::string name{};
|
||||||
bool disabled_by_default{false};
|
bool disabled_by_default{false};
|
||||||
|
#ifdef USE_ENTITY_ICON
|
||||||
std::string icon{};
|
std::string icon{};
|
||||||
|
#endif
|
||||||
enums::EntityCategory entity_category{};
|
enums::EntityCategory entity_category{};
|
||||||
|
#ifdef USE_DEVICES
|
||||||
uint32_t device_id{0};
|
uint32_t device_id{0};
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
@ -303,7 +307,9 @@ class StateResponseProtoMessage : public ProtoMessage {
|
|||||||
public:
|
public:
|
||||||
~StateResponseProtoMessage() override = default;
|
~StateResponseProtoMessage() override = default;
|
||||||
uint32_t key{0};
|
uint32_t key{0};
|
||||||
|
#ifdef USE_DEVICES
|
||||||
uint32_t device_id{0};
|
uint32_t device_id{0};
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
@ -312,7 +318,9 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
|||||||
public:
|
public:
|
||||||
~CommandProtoMessage() override = default;
|
~CommandProtoMessage() override = default;
|
||||||
uint32_t key{0};
|
uint32_t key{0};
|
||||||
|
#ifdef USE_DEVICES
|
||||||
uint32_t device_id{0};
|
uint32_t device_id{0};
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
|
@ -13,11 +13,180 @@ namespace bluetooth_proxy {
|
|||||||
|
|
||||||
static const char *const TAG = "bluetooth_proxy.connection";
|
static const char *const TAG = "bluetooth_proxy.connection";
|
||||||
|
|
||||||
|
static std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
|
||||||
|
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
|
||||||
|
return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]),
|
||||||
|
((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
|
||||||
|
}
|
||||||
|
|
||||||
void BluetoothConnection::dump_config() {
|
void BluetoothConnection::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
||||||
BLEClientBase::dump_config();
|
BLEClientBase::dump_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BluetoothConnection::loop() {
|
||||||
|
BLEClientBase::loop();
|
||||||
|
|
||||||
|
// Early return if no active connection or not in service discovery phase
|
||||||
|
if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle service discovery
|
||||||
|
this->send_service_for_discovery_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||||
|
// Send disconnection notification
|
||||||
|
this->proxy_->send_device_connection(this->address_, false, 0, reason);
|
||||||
|
|
||||||
|
// Important: If we were in the middle of sending services, we do NOT send
|
||||||
|
// send_gatt_services_done() here. This ensures the client knows that
|
||||||
|
// the service discovery was interrupted and can retry. The client
|
||||||
|
// (aioesphomeapi) implements a 30-second timeout (DEFAULT_BLE_TIMEOUT)
|
||||||
|
// to detect incomplete service discovery rather than relying on us to
|
||||||
|
// tell them about a partial list.
|
||||||
|
this->set_address(0);
|
||||||
|
this->send_service_ = DONE_SENDING_SERVICES;
|
||||||
|
this->proxy_->send_connections_free();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothConnection::send_service_for_discovery_() {
|
||||||
|
if (this->send_service_ == this->service_count_) {
|
||||||
|
this->send_service_ = DONE_SENDING_SERVICES;
|
||||||
|
this->proxy_->send_gatt_services_done(this->address_);
|
||||||
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||||
|
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||||
|
this->release_services();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early return if no API connection
|
||||||
|
auto *api_conn = this->proxy_->get_api_connection();
|
||||||
|
if (api_conn == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send next service
|
||||||
|
esp_gattc_service_elem_t service_result;
|
||||||
|
uint16_t service_count = 1;
|
||||||
|
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||||
|
&service_result, &service_count, this->send_service_);
|
||||||
|
this->send_service_++;
|
||||||
|
|
||||||
|
if (service_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), this->send_service_ - 1, service_status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service_count == 0) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), service_count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api::BluetoothGATTGetServicesResponse resp;
|
||||||
|
resp.address = this->address_;
|
||||||
|
resp.services.reserve(1); // Always one service per response in this implementation
|
||||||
|
api::BluetoothGATTService service_resp;
|
||||||
|
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
|
||||||
|
service_resp.handle = service_result.start_handle;
|
||||||
|
|
||||||
|
// Get the number of characteristics directly with one call
|
||||||
|
uint16_t total_char_count = 0;
|
||||||
|
esp_gatt_status_t char_count_status =
|
||||||
|
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||||
|
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||||
|
|
||||||
|
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
|
||||||
|
// Only reserve if we successfully got a count
|
||||||
|
service_resp.characteristics.reserve(total_char_count);
|
||||||
|
} else if (char_count_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), char_count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process characteristics
|
||||||
|
uint16_t char_offset = 0;
|
||||||
|
esp_gattc_char_elem_t char_result;
|
||||||
|
while (true) { // characteristics
|
||||||
|
uint16_t char_count = 1;
|
||||||
|
esp_gatt_status_t char_status =
|
||||||
|
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||||
|
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||||
|
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (char_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), char_status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (char_count == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
api::BluetoothGATTCharacteristic characteristic_resp;
|
||||||
|
characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid);
|
||||||
|
characteristic_resp.handle = char_result.char_handle;
|
||||||
|
characteristic_resp.properties = char_result.properties;
|
||||||
|
char_offset++;
|
||||||
|
|
||||||
|
// Get the number of descriptors directly with one call
|
||||||
|
uint16_t total_desc_count = 0;
|
||||||
|
esp_gatt_status_t desc_count_status =
|
||||||
|
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle,
|
||||||
|
service_result.end_handle, 0, &total_desc_count);
|
||||||
|
|
||||||
|
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
|
||||||
|
// Only reserve if we successfully got a count
|
||||||
|
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||||
|
} else if (desc_count_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), char_result.char_handle, desc_count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process descriptors
|
||||||
|
uint16_t desc_offset = 0;
|
||||||
|
esp_gattc_descr_elem_t desc_result;
|
||||||
|
while (true) { // descriptors
|
||||||
|
uint16_t desc_count = 1;
|
||||||
|
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||||
|
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||||
|
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (desc_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), desc_status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (desc_count == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
api::BluetoothGATTDescriptor descriptor_resp;
|
||||||
|
descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid);
|
||||||
|
descriptor_resp.handle = desc_result.handle;
|
||||||
|
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
|
||||||
|
desc_offset++;
|
||||||
|
}
|
||||||
|
service_resp.characteristics.push_back(std::move(characteristic_resp));
|
||||||
|
}
|
||||||
|
resp.services.push_back(std::move(service_resp));
|
||||||
|
|
||||||
|
// Send the message (we already checked api_conn is not null at the beginning)
|
||||||
|
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) {
|
esp_ble_gattc_cb_param_t *param) {
|
||||||
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
||||||
@ -25,22 +194,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
|||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ESP_GATTC_DISCONNECT_EVT: {
|
case ESP_GATTC_DISCONNECT_EVT: {
|
||||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
this->reset_connection_(param->disconnect.reason);
|
||||||
this->set_address(0);
|
|
||||||
this->proxy_->send_connections_free();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ESP_GATTC_CLOSE_EVT: {
|
case ESP_GATTC_CLOSE_EVT: {
|
||||||
this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason);
|
this->reset_connection_(param->close.reason);
|
||||||
this->set_address(0);
|
|
||||||
this->proxy_->send_connections_free();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ESP_GATTC_OPEN_EVT: {
|
case ESP_GATTC_OPEN_EVT: {
|
||||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||||
this->proxy_->send_device_connection(this->address_, false, 0, param->open.status);
|
this->reset_connection_(param->open.status);
|
||||||
this->set_address(0);
|
|
||||||
this->proxy_->send_connections_free();
|
|
||||||
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
|
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
|
||||||
this->proxy_->send_connections_free();
|
this->proxy_->send_connections_free();
|
||||||
|
@ -12,6 +12,7 @@ class BluetoothProxy;
|
|||||||
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||||
public:
|
public:
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
void loop() override;
|
||||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) override;
|
esp_ble_gattc_cb_param_t *param) override;
|
||||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||||
@ -27,6 +28,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
|||||||
protected:
|
protected:
|
||||||
friend class BluetoothProxy;
|
friend class BluetoothProxy;
|
||||||
|
|
||||||
|
void send_service_for_discovery_();
|
||||||
|
void reset_connection_(esp_err_t reason);
|
||||||
|
|
||||||
// Memory optimized layout for 32-bit systems
|
// Memory optimized layout for 32-bit systems
|
||||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||||
BluetoothProxy *proxy_;
|
BluetoothProxy *proxy_;
|
||||||
|
@ -11,19 +11,6 @@ namespace esphome {
|
|||||||
namespace bluetooth_proxy {
|
namespace bluetooth_proxy {
|
||||||
|
|
||||||
static const char *const TAG = "bluetooth_proxy";
|
static const char *const TAG = "bluetooth_proxy";
|
||||||
static const int DONE_SENDING_SERVICES = -2;
|
|
||||||
|
|
||||||
std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
|
|
||||||
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
|
|
||||||
return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]),
|
|
||||||
((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch size for BLE advertisements to maximize WiFi efficiency
|
// Batch size for BLE advertisements to maximize WiFi efficiency
|
||||||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
||||||
@ -213,130 +200,12 @@ void BluetoothProxy::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Flush any pending BLE advertisements that have been accumulated but not yet sent
|
// Flush any pending BLE advertisements that have been accumulated but not yet sent
|
||||||
static uint32_t last_flush_time = 0;
|
|
||||||
uint32_t now = App.get_loop_component_start_time();
|
uint32_t now = App.get_loop_component_start_time();
|
||||||
|
|
||||||
// Flush accumulated advertisements every 100ms
|
// Flush accumulated advertisements every 100ms
|
||||||
if (now - last_flush_time >= 100) {
|
if (now - this->last_advertisement_flush_time_ >= 100) {
|
||||||
this->flush_pending_advertisements();
|
this->flush_pending_advertisements();
|
||||||
last_flush_time = now;
|
this->last_advertisement_flush_time_ = now;
|
||||||
}
|
|
||||||
for (auto *connection : this->connections_) {
|
|
||||||
if (connection->send_service_ == connection->service_count_) {
|
|
||||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
|
||||||
this->send_gatt_services_done(connection->get_address());
|
|
||||||
if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
|
||||||
connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
|
||||||
connection->release_services();
|
|
||||||
}
|
|
||||||
} else if (connection->send_service_ >= 0) {
|
|
||||||
esp_gattc_service_elem_t service_result;
|
|
||||||
uint16_t service_count = 1;
|
|
||||||
esp_gatt_status_t service_status =
|
|
||||||
esp_ble_gattc_get_service(connection->get_gattc_if(), connection->get_conn_id(), nullptr, &service_result,
|
|
||||||
&service_count, connection->send_service_);
|
|
||||||
connection->send_service_++;
|
|
||||||
if (service_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d",
|
|
||||||
connection->get_connection_index(), connection->address_str().c_str(), connection->send_service_ - 1,
|
|
||||||
service_status);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (service_count == 0) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d",
|
|
||||||
connection->get_connection_index(), connection->address_str().c_str(), service_count);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
api::BluetoothGATTGetServicesResponse resp;
|
|
||||||
resp.address = connection->get_address();
|
|
||||||
resp.services.reserve(1); // Always one service per response in this implementation
|
|
||||||
api::BluetoothGATTService service_resp;
|
|
||||||
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
|
|
||||||
service_resp.handle = service_result.start_handle;
|
|
||||||
uint16_t char_offset = 0;
|
|
||||||
esp_gattc_char_elem_t char_result;
|
|
||||||
// Get the number of characteristics directly with one call
|
|
||||||
uint16_t total_char_count = 0;
|
|
||||||
esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
|
|
||||||
connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
|
|
||||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
|
||||||
|
|
||||||
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
|
|
||||||
// Only reserve if we successfully got a count
|
|
||||||
service_resp.characteristics.reserve(total_char_count);
|
|
||||||
} else if (char_count_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
|
|
||||||
connection->address_str().c_str(), char_count_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now process characteristics
|
|
||||||
while (true) { // characteristics
|
|
||||||
uint16_t char_count = 1;
|
|
||||||
esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
|
|
||||||
connection->get_gattc_if(), connection->get_conn_id(), service_result.start_handle,
|
|
||||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
|
||||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (char_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", connection->get_connection_index(),
|
|
||||||
connection->address_str().c_str(), char_status);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (char_count == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
api::BluetoothGATTCharacteristic characteristic_resp;
|
|
||||||
characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid);
|
|
||||||
characteristic_resp.handle = char_result.char_handle;
|
|
||||||
characteristic_resp.properties = char_result.properties;
|
|
||||||
char_offset++;
|
|
||||||
|
|
||||||
// Get the number of descriptors directly with one call
|
|
||||||
uint16_t total_desc_count = 0;
|
|
||||||
esp_gatt_status_t desc_count_status =
|
|
||||||
esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
|
|
||||||
char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
|
|
||||||
|
|
||||||
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
|
|
||||||
// Only reserve if we successfully got a count
|
|
||||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
|
||||||
} else if (desc_count_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
|
|
||||||
connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
|
|
||||||
desc_count_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now process descriptors
|
|
||||||
uint16_t desc_offset = 0;
|
|
||||||
esp_gattc_descr_elem_t desc_result;
|
|
||||||
while (true) { // descriptors
|
|
||||||
uint16_t desc_count = 1;
|
|
||||||
esp_gatt_status_t desc_status =
|
|
||||||
esp_ble_gattc_get_all_descr(connection->get_gattc_if(), connection->get_conn_id(),
|
|
||||||
char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
|
||||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (desc_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", connection->get_connection_index(),
|
|
||||||
connection->address_str().c_str(), desc_status);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (desc_count == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
api::BluetoothGATTDescriptor descriptor_resp;
|
|
||||||
descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid);
|
|
||||||
descriptor_resp.handle = desc_result.handle;
|
|
||||||
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
|
|
||||||
desc_offset++;
|
|
||||||
}
|
|
||||||
service_resp.characteristics.push_back(std::move(characteristic_resp));
|
|
||||||
}
|
|
||||||
resp.services.push_back(std::move(service_resp));
|
|
||||||
this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ namespace esphome {
|
|||||||
namespace bluetooth_proxy {
|
namespace bluetooth_proxy {
|
||||||
|
|
||||||
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
||||||
|
static const int DONE_SENDING_SERVICES = -2;
|
||||||
|
|
||||||
using namespace esp32_ble_client;
|
using namespace esp32_ble_client;
|
||||||
|
|
||||||
@ -149,7 +150,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
|||||||
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
||||||
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
|
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
|
||||||
|
|
||||||
// Group 3: 1-byte types grouped together
|
// Group 3: 4-byte types
|
||||||
|
uint32_t last_advertisement_flush_time_{0};
|
||||||
|
|
||||||
|
// 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
|
// 2 bytes used, 2 bytes padding
|
||||||
|
@ -31,6 +31,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
|
CoreModel,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, HexInt, TimePeriod
|
from esphome.core import CORE, HexInt, TimePeriod
|
||||||
@ -713,6 +714,7 @@ async def to_code(config):
|
|||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
|
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
|
||||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
|
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
|
||||||
|
cg.add_define(CoreModel.MULTI_ATOMICS)
|
||||||
|
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
@ -128,46 +128,53 @@ void ESP32BLETracker::loop() {
|
|||||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
|
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
while (read_idx != write_idx) {
|
while (read_idx != write_idx) {
|
||||||
// Process one result at a time directly from ring buffer
|
// Calculate how many contiguous results we can process in one batch
|
||||||
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx];
|
// If write > read: process all results from read to write
|
||||||
|
// If write <= read (wraparound): process from read to end of buffer first
|
||||||
|
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
|
||||||
|
|
||||||
|
// Process the batch for raw advertisements
|
||||||
if (this->raw_advertisements_) {
|
if (this->raw_advertisements_) {
|
||||||
for (auto *listener : this->listeners_) {
|
for (auto *listener : this->listeners_) {
|
||||||
listener->parse_devices(&scan_result, 1);
|
listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||||
}
|
}
|
||||||
for (auto *client : this->clients_) {
|
for (auto *client : this->clients_) {
|
||||||
client->parse_devices(&scan_result, 1);
|
client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process individual results for parsed advertisements
|
||||||
if (this->parse_advertisements_) {
|
if (this->parse_advertisements_) {
|
||||||
#ifdef USE_ESP32_BLE_DEVICE
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
ESPBTDevice device;
|
for (size_t i = 0; i < batch_size; i++) {
|
||||||
device.parse_scan_rst(scan_result);
|
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
|
||||||
|
ESPBTDevice device;
|
||||||
|
device.parse_scan_rst(scan_result);
|
||||||
|
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (auto *listener : this->listeners_) {
|
for (auto *listener : this->listeners_) {
|
||||||
if (listener->parse_device(device))
|
if (listener->parse_device(device))
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto *client : this->clients_) {
|
for (auto *client : this->clients_) {
|
||||||
if (client->parse_device(device)) {
|
if (client->parse_device(device)) {
|
||||||
found = true;
|
found = true;
|
||||||
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
||||||
promote_to_connecting = true;
|
promote_to_connecting = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!found && !this->scan_continuous_) {
|
if (!found && !this->scan_continuous_) {
|
||||||
this->print_bt_device_info(device);
|
this->print_bt_device_info(device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif // USE_ESP32_BLE_DEVICE
|
#endif // USE_ESP32_BLE_DEVICE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to next entry in ring buffer
|
// Update read index for entire batch
|
||||||
read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
|
read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
|
||||||
|
|
||||||
// Store with release to ensure reads complete before index update
|
// Store with release to ensure reads complete before index update
|
||||||
this->ring_read_index_.store(read_idx, std::memory_order_release);
|
this->ring_read_index_.store(read_idx, std::memory_order_release);
|
||||||
|
@ -15,6 +15,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
|
CoreModel,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.helpers import copy_file_if_changed
|
from esphome.helpers import copy_file_if_changed
|
||||||
@ -187,6 +188,7 @@ async def to_code(config):
|
|||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||||
|
cg.add_define(CoreModel.SINGLE)
|
||||||
|
|
||||||
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_HOST,
|
PLATFORM_HOST,
|
||||||
|
CoreModel,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ async def to_code(config):
|
|||||||
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
|
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
|
||||||
cg.add_build_flag("-std=gnu++20")
|
cg.add_build_flag("-std=gnu++20")
|
||||||
cg.add_define("ESPHOME_BOARD", "host")
|
cg.add_define("ESPHOME_BOARD", "host")
|
||||||
|
cg.add_define(CoreModel.MULTI_ATOMICS)
|
||||||
cg.add_platformio_option("platform", "platformio/native")
|
cg.add_platformio_option("platform", "platformio/native")
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
@ -20,6 +20,7 @@ from esphome.const import (
|
|||||||
KEY_FRAMEWORK_VERSION,
|
KEY_FRAMEWORK_VERSION,
|
||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
|
CoreModel,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
@ -260,6 +261,7 @@ async def component_to_code(config):
|
|||||||
cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}")
|
cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}")
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]])
|
cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]])
|
||||||
|
cg.add_define(CoreModel.MULTI_NO_ATOMICS)
|
||||||
|
|
||||||
# force using arduino framework
|
# force using arduino framework
|
||||||
cg.add_platformio_option("framework", "arduino")
|
cg.add_platformio_option("framework", "arduino")
|
||||||
|
@ -3,6 +3,7 @@ import esphome.codegen as cg
|
|||||||
from esphome.components import display, spi
|
from esphome.components import display, spi
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_FLIP_X,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_INTENSITY,
|
CONF_INTENSITY,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
@ -14,7 +15,6 @@ CODEOWNERS = ["@rspaargaren"]
|
|||||||
DEPENDENCIES = ["spi"]
|
DEPENDENCIES = ["spi"]
|
||||||
|
|
||||||
CONF_ROTATE_CHIP = "rotate_chip"
|
CONF_ROTATE_CHIP = "rotate_chip"
|
||||||
CONF_FLIP_X = "flip_x"
|
|
||||||
CONF_SCROLL_SPEED = "scroll_speed"
|
CONF_SCROLL_SPEED = "scroll_speed"
|
||||||
CONF_SCROLL_DWELL = "scroll_dwell"
|
CONF_SCROLL_DWELL = "scroll_dwell"
|
||||||
CONF_SCROLL_DELAY = "scroll_delay"
|
CONF_SCROLL_DELAY = "scroll_delay"
|
||||||
|
@ -16,6 +16,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
|
CoreModel,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, EsphomeError, coroutine_with_priority
|
from esphome.core import CORE, EsphomeError, coroutine_with_priority
|
||||||
from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file
|
from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file
|
||||||
@ -171,6 +172,7 @@ async def to_code(config):
|
|||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_define("ESPHOME_VARIANT", "RP2040")
|
cg.add_define("ESPHOME_VARIANT", "RP2040")
|
||||||
|
cg.add_define(CoreModel.SINGLE)
|
||||||
|
|
||||||
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ from esphome.const import (
|
|||||||
CONF_BRIGHTNESS,
|
CONF_BRIGHTNESS,
|
||||||
CONF_CONTRAST,
|
CONF_CONTRAST,
|
||||||
CONF_EXTERNAL_VCC,
|
CONF_EXTERNAL_VCC,
|
||||||
|
CONF_FLIP_X,
|
||||||
|
CONF_FLIP_Y,
|
||||||
CONF_INVERT,
|
CONF_INVERT,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
@ -18,9 +20,6 @@ ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base")
|
|||||||
SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer)
|
SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer)
|
||||||
SSD1306Model = ssd1306_base_ns.enum("SSD1306Model")
|
SSD1306Model = ssd1306_base_ns.enum("SSD1306Model")
|
||||||
|
|
||||||
CONF_FLIP_X = "flip_x"
|
|
||||||
CONF_FLIP_Y = "flip_y"
|
|
||||||
|
|
||||||
MODELS = {
|
MODELS = {
|
||||||
"SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32,
|
"SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32,
|
||||||
"SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64,
|
"SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64,
|
||||||
|
@ -35,6 +35,14 @@ class Framework(StrEnum):
|
|||||||
ZEPHYR = "zephyr"
|
ZEPHYR = "zephyr"
|
||||||
|
|
||||||
|
|
||||||
|
class CoreModel(StrEnum):
|
||||||
|
"""Core model identifiers for ESPHome scheduler."""
|
||||||
|
|
||||||
|
SINGLE = "ESPHOME_CORES_SINGLE"
|
||||||
|
MULTI_NO_ATOMICS = "ESPHOME_CORES_MULTI_NO_ATOMICS"
|
||||||
|
MULTI_ATOMICS = "ESPHOME_CORES_MULTI_ATOMICS"
|
||||||
|
|
||||||
|
|
||||||
class PlatformFramework(Enum):
|
class PlatformFramework(Enum):
|
||||||
"""Combined platform-framework identifiers with tuple values."""
|
"""Combined platform-framework identifiers with tuple values."""
|
||||||
|
|
||||||
@ -375,6 +383,8 @@ CONF_FINGER_ID = "finger_id"
|
|||||||
CONF_FINGERPRINT_COUNT = "fingerprint_count"
|
CONF_FINGERPRINT_COUNT = "fingerprint_count"
|
||||||
CONF_FLASH_LENGTH = "flash_length"
|
CONF_FLASH_LENGTH = "flash_length"
|
||||||
CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
|
CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
|
||||||
|
CONF_FLIP_X = "flip_x"
|
||||||
|
CONF_FLIP_Y = "flip_y"
|
||||||
CONF_FLOW = "flow"
|
CONF_FLOW = "flow"
|
||||||
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
||||||
CONF_FONT = "font"
|
CONF_FONT = "font"
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
#define ESPHOME_VARIANT "ESP32"
|
#define ESPHOME_VARIANT "ESP32"
|
||||||
#define ESPHOME_DEBUG_SCHEDULER
|
#define ESPHOME_DEBUG_SCHEDULER
|
||||||
|
|
||||||
|
// Default threading model for static analysis (ESP32 is multi-core with atomics)
|
||||||
|
#define ESPHOME_CORES_MULTI_ATOMICS
|
||||||
|
|
||||||
// logger
|
// logger
|
||||||
#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ static void validate_static_string(const char *name) {
|
|||||||
ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str);
|
ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
// A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to
|
// A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to
|
||||||
// them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task,
|
// them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task,
|
||||||
@ -82,9 +82,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
item->callback = std::move(func);
|
item->callback = std::move(func);
|
||||||
item->remove = false;
|
item->remove = false;
|
||||||
|
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// Special handling for defer() (delay = 0, type = TIMEOUT)
|
// Special handling for defer() (delay = 0, type = TIMEOUT)
|
||||||
// ESP8266 and RP2040 are excluded because they don't need thread-safe defer handling
|
// Single-core platforms don't need thread-safe defer handling
|
||||||
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
||||||
// Put in defer queue for guaranteed FIFO execution
|
// Put in defer queue for guaranteed FIFO execution
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
@ -92,7 +92,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
this->defer_queue_.push_back(std::move(item));
|
this->defer_queue_.push_back(std::move(item));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* not ESPHOME_CORES_SINGLE */
|
||||||
|
|
||||||
// Get fresh timestamp for new timer/interval - ensures accurate scheduling
|
// Get fresh timestamp for new timer/interval - ensures accurate scheduling
|
||||||
const auto now = this->millis_64_(millis()); // Fresh millis() call
|
const auto now = this->millis_64_(millis()); // Fresh millis() call
|
||||||
@ -123,7 +123,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(),
|
ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(),
|
||||||
name_cstr ? name_cstr : "(null)", type_str, delay, static_cast<uint32_t>(item->next_execution_ - now));
|
name_cstr ? name_cstr : "(null)", type_str, delay, static_cast<uint32_t>(item->next_execution_ - now));
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
// If name is provided, do atomic cancel-and-add
|
// If name is provided, do atomic cancel-and-add
|
||||||
@ -231,7 +231,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
|||||||
return item->next_execution_ - now_64;
|
return item->next_execution_ - now_64;
|
||||||
}
|
}
|
||||||
void HOT Scheduler::call(uint32_t now) {
|
void HOT Scheduler::call(uint32_t now) {
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// Process defer queue first to guarantee FIFO execution order for deferred items.
|
// Process defer queue first to guarantee FIFO execution order for deferred items.
|
||||||
// Previously, defer() used the heap which gave undefined order for equal timestamps,
|
// Previously, defer() used the heap which gave undefined order for equal timestamps,
|
||||||
// causing race conditions on multi-core systems (ESP32, BK7200).
|
// causing race conditions on multi-core systems (ESP32, BK7200).
|
||||||
@ -239,8 +239,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
// - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_
|
// - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_
|
||||||
// - Items execute in exact order they were deferred (FIFO guarantee)
|
// - Items execute in exact order they were deferred (FIFO guarantee)
|
||||||
// - No deferred items exist in to_add_, so processing order doesn't affect correctness
|
// - No deferred items exist in to_add_, so processing order doesn't affect correctness
|
||||||
// ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach
|
// Single-core platforms don't use this queue and fall back to the heap-based approach.
|
||||||
// (ESP8266: single-core, RP2040: empty mutex implementation).
|
|
||||||
//
|
//
|
||||||
// Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
|
// Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
|
||||||
// processed here. They are removed from the queue normally via pop_front() but skipped
|
// processed here. They are removed from the queue normally via pop_front() but skipped
|
||||||
@ -262,7 +261,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
this->execute_item_(item.get(), now);
|
this->execute_item_(item.get(), now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* not ESPHOME_CORES_SINGLE */
|
||||||
|
|
||||||
// Convert the fresh timestamp from main loop to 64-bit for scheduler operations
|
// Convert the fresh timestamp from main loop to 64-bit for scheduler operations
|
||||||
const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop()
|
const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop()
|
||||||
@ -274,13 +273,15 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
if (now_64 - last_print > 2000) {
|
if (now_64 - last_print > 2000) {
|
||||||
last_print = now_64;
|
last_print = now_64;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
|
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
|
||||||
this->millis_major_, this->last_millis_.load(std::memory_order_relaxed));
|
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
|
||||||
#else
|
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
|
||||||
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
|
major_dbg, last_dbg);
|
||||||
|
#else /* not ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
|
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
|
||||||
this->millis_major_, this->last_millis_);
|
this->millis_major_, this->last_millis_);
|
||||||
#endif
|
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
while (!this->empty_()) {
|
while (!this->empty_()) {
|
||||||
std::unique_ptr<SchedulerItem> item;
|
std::unique_ptr<SchedulerItem> item;
|
||||||
{
|
{
|
||||||
@ -305,7 +306,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif // ESPHOME_DEBUG_SCHEDULER
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
// If we have too many items to remove
|
// If we have too many items to remove
|
||||||
if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
|
if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
|
||||||
@ -352,7 +353,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
||||||
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
|
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
|
||||||
item->next_execution_, now_64);
|
item->next_execution_, now_64);
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
// Warning: During callback(), a lot of stuff can happen, including:
|
// Warning: During callback(), a lot of stuff can happen, including:
|
||||||
// - timeouts/intervals get added, potentially invalidating vector pointers
|
// - timeouts/intervals get added, potentially invalidating vector pointers
|
||||||
@ -460,7 +461,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
|||||||
size_t total_cancelled = 0;
|
size_t total_cancelled = 0;
|
||||||
|
|
||||||
// Check all containers for matching items
|
// Check all containers for matching items
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// Only check defer queue for timeouts (intervals never go there)
|
// Only check defer queue for timeouts (intervals never go there)
|
||||||
if (type == SchedulerItem::TIMEOUT) {
|
if (type == SchedulerItem::TIMEOUT) {
|
||||||
for (auto &item : this->defer_queue_) {
|
for (auto &item : this->defer_queue_) {
|
||||||
@ -470,7 +471,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* not ESPHOME_CORES_SINGLE */
|
||||||
|
|
||||||
// Cancel items in the main heap
|
// Cancel items in the main heap
|
||||||
for (auto &item : this->items_) {
|
for (auto &item : this->items_) {
|
||||||
@ -495,24 +496,53 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
|||||||
|
|
||||||
uint64_t Scheduler::millis_64_(uint32_t now) {
|
uint64_t Scheduler::millis_64_(uint32_t now) {
|
||||||
// THREAD SAFETY NOTE:
|
// THREAD SAFETY NOTE:
|
||||||
// This function can be called from multiple threads simultaneously on ESP32/LibreTiny.
|
// This function has three implementations, based on the precompiler flags
|
||||||
// On single-threaded platforms (ESP8266, RP2040), atomics are not needed.
|
// - ESPHOME_CORES_SINGLE - Runs on single-core platforms (ESP8266, RP2040, etc.)
|
||||||
|
// - ESPHOME_CORES_MULTI_NO_ATOMICS - Runs on multi-core platforms without atomics (LibreTiny)
|
||||||
|
// - ESPHOME_CORES_MULTI_ATOMICS - Runs on multi-core platforms with atomics (ESP32, HOST, etc.)
|
||||||
|
//
|
||||||
|
// Make sure all changes are synchronized if you edit this function.
|
||||||
//
|
//
|
||||||
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
|
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
|
||||||
// handles out-of-order timestamps between threads, but minimizing time differences
|
// handles out-of-order timestamps between threads, but minimizing time differences
|
||||||
// helps maintain accuracy.
|
// helps maintain accuracy.
|
||||||
//
|
//
|
||||||
// The implementation handles the 32-bit rollover (every 49.7 days) by:
|
|
||||||
// 1. Using a lock when detecting rollover to ensure atomic update
|
|
||||||
// 2. Restricting normal updates to forward movement within the same epoch
|
|
||||||
// This prevents race conditions at the rollover boundary without requiring
|
|
||||||
// 64-bit atomics or locking on every call.
|
|
||||||
|
|
||||||
#ifdef USE_LIBRETINY
|
#ifdef ESPHOME_CORES_SINGLE
|
||||||
// LibreTiny: Multi-threaded but lacks atomic operation support
|
// This is the single core implementation.
|
||||||
// TODO: If LibreTiny ever adds atomic support, remove this entire block and
|
//
|
||||||
// let it fall through to the atomic-based implementation below
|
// Single-core platforms have no concurrency, so this is a simple implementation
|
||||||
// We need to use a lock when near the rollover boundary to prevent races
|
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
|
||||||
|
|
||||||
|
uint16_t major = this->millis_major_;
|
||||||
|
uint32_t last = this->last_millis_;
|
||||||
|
|
||||||
|
// Check for rollover
|
||||||
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
|
this->millis_major_++;
|
||||||
|
major++;
|
||||||
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
|
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||||
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if time moved forward
|
||||||
|
if (now > last) {
|
||||||
|
this->last_millis_ = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
||||||
|
return now + (static_cast<uint64_t>(major) << 32);
|
||||||
|
|
||||||
|
#elif defined(ESPHOME_CORES_MULTI_NO_ATOMICS)
|
||||||
|
// This is the multi core no atomics implementation.
|
||||||
|
//
|
||||||
|
// Without atomics, this implementation uses locks more aggressively:
|
||||||
|
// 1. Always locks when near the rollover boundary (within 10 seconds)
|
||||||
|
// 2. Always locks when detecting a large backwards jump
|
||||||
|
// 3. Updates without lock in normal forward progression (accepting minor races)
|
||||||
|
// This is less efficient but necessary without atomic operations.
|
||||||
|
uint16_t major = this->millis_major_;
|
||||||
uint32_t last = this->last_millis_;
|
uint32_t last = this->last_millis_;
|
||||||
|
|
||||||
// Define a safe window around the rollover point (10 seconds)
|
// Define a safe window around the rollover point (10 seconds)
|
||||||
@ -531,9 +561,10 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
|||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
// True rollover detected (happens every ~49.7 days)
|
// True rollover detected (happens every ~49.7 days)
|
||||||
this->millis_major_++;
|
this->millis_major_++;
|
||||||
|
major++;
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
}
|
}
|
||||||
// Update last_millis_ while holding lock
|
// Update last_millis_ while holding lock
|
||||||
this->last_millis_ = now;
|
this->last_millis_ = now;
|
||||||
@ -549,58 +580,76 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
|||||||
// If now <= last and we're not near rollover, don't update
|
// If now <= last and we're not near rollover, don't update
|
||||||
// This minimizes backwards time movement
|
// This minimizes backwards time movement
|
||||||
|
|
||||||
#elif !defined(USE_ESP8266) && !defined(USE_RP2040)
|
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
||||||
// Multi-threaded platforms with atomic support (ESP32)
|
return now + (static_cast<uint64_t>(major) << 32);
|
||||||
uint32_t last = this->last_millis_.load(std::memory_order_relaxed);
|
|
||||||
|
|
||||||
// If we might be near a rollover (large backwards jump), take the lock for the entire operation
|
#elif defined(ESPHOME_CORES_MULTI_ATOMICS)
|
||||||
// This ensures rollover detection and last_millis_ update are atomic together
|
// This is the multi core with atomics implementation.
|
||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
//
|
||||||
// Potential rollover - need lock for atomic rollover detection + update
|
// Uses atomic operations with acquire/release semantics to ensure coherent
|
||||||
LockGuard guard{this->lock_};
|
// reads of millis_major_ and last_millis_ across cores. Features:
|
||||||
// Re-read with lock held
|
// 1. Epoch-coherency retry loop to handle concurrent updates
|
||||||
last = this->last_millis_.load(std::memory_order_relaxed);
|
// 2. Lock only taken for actual rollover detection and update
|
||||||
|
// 3. Lock-free CAS updates for normal forward time progression
|
||||||
|
// 4. Memory ordering ensures cores see consistent time values
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
uint16_t major = this->millis_major_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Acquire so that if we later decide **not** to take the lock we still
|
||||||
|
* observe a `millis_major_` value coherent with the loaded `last_millis_`.
|
||||||
|
* The acquire load ensures any later read of `millis_major_` sees its
|
||||||
|
* corresponding increment.
|
||||||
|
*/
|
||||||
|
uint32_t last = this->last_millis_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
|
// If we might be near a rollover (large backwards jump), take the lock for the entire operation
|
||||||
|
// This ensures rollover detection and last_millis_ update are atomic together
|
||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
// True rollover detected (happens every ~49.7 days)
|
// Potential rollover - need lock for atomic rollover detection + update
|
||||||
this->millis_major_++;
|
LockGuard guard{this->lock_};
|
||||||
|
// Re-read with lock held; mutex already provides ordering
|
||||||
|
last = this->last_millis_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
|
// True rollover detected (happens every ~49.7 days)
|
||||||
|
this->millis_major_.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
major++;
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
}
|
}
|
||||||
// Update last_millis_ while holding lock to prevent races
|
/*
|
||||||
this->last_millis_.store(now, std::memory_order_relaxed);
|
* Update last_millis_ while holding the lock to prevent races
|
||||||
} else {
|
* Publish the new low-word *after* bumping `millis_major_` (done above)
|
||||||
// Normal case: Try lock-free update, but only allow forward movement within same epoch
|
* so readers never see a mismatched pair.
|
||||||
// This prevents accidentally moving backwards across a rollover boundary
|
*/
|
||||||
while (now > last && (now - last) < HALF_MAX_UINT32) {
|
this->last_millis_.store(now, std::memory_order_release);
|
||||||
if (this->last_millis_.compare_exchange_weak(last, now, std::memory_order_relaxed)) {
|
} else {
|
||||||
break;
|
// Normal case: Try lock-free update, but only allow forward movement within same epoch
|
||||||
|
// This prevents accidentally moving backwards across a rollover boundary
|
||||||
|
while (now > last && (now - last) < HALF_MAX_UINT32) {
|
||||||
|
if (this->last_millis_.compare_exchange_weak(last, now,
|
||||||
|
std::memory_order_release, // success
|
||||||
|
std::memory_order_relaxed)) { // failure
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// CAS failure means no data was published; relaxed is fine
|
||||||
|
// last is automatically updated by compare_exchange_weak if it fails
|
||||||
}
|
}
|
||||||
// last is automatically updated by compare_exchange_weak if it fails
|
|
||||||
}
|
}
|
||||||
|
uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed);
|
||||||
|
if (major_end == major)
|
||||||
|
return now + (static_cast<uint64_t>(major) << 32);
|
||||||
}
|
}
|
||||||
|
// Unreachable - the loop always returns when major_end == major
|
||||||
|
__builtin_unreachable();
|
||||||
|
|
||||||
#else
|
#else
|
||||||
// Single-threaded platforms (ESP8266, RP2040): No atomics needed
|
#error \
|
||||||
uint32_t last = this->last_millis_;
|
"No platform threading model defined. One of ESPHOME_CORES_SINGLE, ESPHOME_CORES_MULTI_NO_ATOMICS, or ESPHOME_CORES_MULTI_ATOMICS must be defined."
|
||||||
|
|
||||||
// Check for rollover
|
|
||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
|
||||||
this->millis_major_++;
|
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
|
||||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
|
|
||||||
// Only update if time moved forward
|
|
||||||
if (now > last) {
|
|
||||||
this->last_millis_ = now;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
|
||||||
return now + (static_cast<uint64_t>(this->millis_major_) << 32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
|
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -204,23 +205,40 @@ class Scheduler {
|
|||||||
Mutex lock_;
|
Mutex lock_;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> items_;
|
std::vector<std::unique_ptr<SchedulerItem>> items_;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
|
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// ESP8266 and RP2040 don't need the defer queue because:
|
// Single-core platforms don't need the defer queue and save 40 bytes of RAM
|
||||||
// ESP8266: Single-core with no preemptive multitasking
|
|
||||||
// RP2040: Currently has empty mutex implementation in ESPHome
|
|
||||||
// Both platforms save 40 bytes of RAM by excluding this
|
|
||||||
std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
|
std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
|
||||||
#endif
|
#endif /* ESPHOME_CORES_SINGLE */
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
|
uint32_t to_remove_{0};
|
||||||
// Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
|
|
||||||
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
|
/*
|
||||||
|
* Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
|
||||||
|
*
|
||||||
|
* MEMORY-ORDERING NOTE
|
||||||
|
* --------------------
|
||||||
|
* `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half.
|
||||||
|
* Writers publish `last_millis_` with memory_order_release and readers use
|
||||||
|
* memory_order_acquire. This ensures that once a reader sees the new low word,
|
||||||
|
* it also observes the corresponding increment of `millis_major_`.
|
||||||
|
*/
|
||||||
std::atomic<uint32_t> last_millis_{0};
|
std::atomic<uint32_t> last_millis_{0};
|
||||||
#else
|
#else /* not ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
// Platforms without atomic support or single-threaded platforms
|
// Platforms without atomic support or single-threaded platforms
|
||||||
uint32_t last_millis_{0};
|
uint32_t last_millis_{0};
|
||||||
#endif
|
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
// millis_major_ is protected by lock when incrementing
|
|
||||||
|
/*
|
||||||
|
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding
|
||||||
|
* `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race.
|
||||||
|
* Ordering relative to `last_millis_` is provided by its release store and the
|
||||||
|
* corresponding acquire loads.
|
||||||
|
*/
|
||||||
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
|
std::atomic<uint16_t> millis_major_{0};
|
||||||
|
#else /* not ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
uint16_t millis_major_{0};
|
uint16_t millis_major_{0};
|
||||||
uint32_t to_remove_{0};
|
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
|||||||
esptool==4.9.0
|
esptool==4.9.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20250514.0
|
esphome-dashboard==20250514.0
|
||||||
aioesphomeapi==37.0.1
|
aioesphomeapi==37.0.2
|
||||||
zeroconf==0.147.0
|
zeroconf==0.147.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.18.14 # dashboard_import
|
ruamel.yaml==0.18.14 # dashboard_import
|
||||||
|
@ -1491,6 +1491,28 @@ def find_common_fields(
|
|||||||
return common_fields
|
return common_fields
|
||||||
|
|
||||||
|
|
||||||
|
def get_common_field_ifdef(
|
||||||
|
field_name: str, messages: list[descriptor.DescriptorProto]
|
||||||
|
) -> str | None:
|
||||||
|
"""Get the field_ifdef option if it's consistent across all messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Name of the field to check
|
||||||
|
messages: List of messages that contain this field
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The field_ifdef string if all messages have the same value, None otherwise
|
||||||
|
"""
|
||||||
|
field_ifdefs = {
|
||||||
|
get_field_opt(field, pb.field_ifdef)
|
||||||
|
for msg in messages
|
||||||
|
if (field := next((f for f in msg.field if f.name == field_name), None))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return the ifdef only if all messages agree on the same value
|
||||||
|
return field_ifdefs.pop() if len(field_ifdefs) == 1 else None
|
||||||
|
|
||||||
|
|
||||||
def build_base_class(
|
def build_base_class(
|
||||||
base_class_name: str,
|
base_class_name: str,
|
||||||
common_fields: list[descriptor.FieldDescriptorProto],
|
common_fields: list[descriptor.FieldDescriptorProto],
|
||||||
@ -1506,9 +1528,14 @@ def build_base_class(
|
|||||||
for field in common_fields:
|
for field in common_fields:
|
||||||
ti = create_field_type_info(field)
|
ti = create_field_type_info(field)
|
||||||
|
|
||||||
|
# Get field_ifdef if it's consistent across all messages
|
||||||
|
field_ifdef = get_common_field_ifdef(field.name, messages)
|
||||||
|
|
||||||
# Only add field declarations, not encode/decode logic
|
# Only add field declarations, not encode/decode logic
|
||||||
protected_content.extend(ti.protected_content)
|
if ti.protected_content:
|
||||||
public_content.extend(ti.public_content)
|
protected_content.extend(wrap_with_ifdef(ti.protected_content, field_ifdef))
|
||||||
|
if ti.public_content:
|
||||||
|
public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef))
|
||||||
|
|
||||||
# Determine if any message using this base class needs decoding
|
# Determine if any message using this base class needs decoding
|
||||||
needs_decode = any(
|
needs_decode = any(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user