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

This commit is contained in:
J. Nick Koston 2025-07-20 13:00:45 -10:00
commit 72fcb29fd2
No known key found for this signature in database
27 changed files with 794 additions and 905 deletions

View File

@ -178,6 +178,51 @@ jobs:
reviewedUsers.add(review.user.login);
});
// Check for previous comments from this workflow to avoid duplicate pings
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: pr_number
});
const previouslyPingedUsers = new Set();
const previouslyPingedTeams = new Set();
// Look for comments from github-actions bot that contain codeowner pings
const workflowComments = comments.filter(comment =>
comment.user.type === 'Bot' &&
comment.user.login === 'github-actions[bot]' &&
comment.body.includes("I've automatically requested reviews from codeowners")
);
// Extract previously mentioned users and teams from workflow comments
for (const comment of workflowComments) {
// Match @username patterns (not team mentions)
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
userMentions.forEach(mention => {
const username = mention.slice(1); // remove @
previouslyPingedUsers.add(username);
});
// Match @org/team patterns
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || [];
teamMentions.forEach(mention => {
const teamName = mention.split('/')[1];
previouslyPingedTeams.add(teamName);
});
}
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`);
// Remove users who have already been pinged in previous workflow comments
previouslyPingedUsers.forEach(user => {
matchedOwners.delete(user);
});
previouslyPingedTeams.forEach(team => {
matchedTeams.delete(team);
});
// Remove only users who have already submitted reviews (not just requested reviewers)
reviewedUsers.forEach(reviewer => {
matchedOwners.delete(reviewer);
@ -192,7 +237,7 @@ jobs:
const teamsList = Array.from(matchedTeams);
if (reviewersList.length === 0 && teamsList.length === 0) {
console.log('No eligible reviewers found (all may already be requested or reviewed)');
console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)');
return;
}
@ -227,31 +272,41 @@ jobs:
console.log('All codeowners are already requested reviewers or have reviewed');
}
// Add a comment to the PR mentioning what happened (include all matched codeowners)
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
// Only add a comment if there are new codeowners to mention (not previously pinged)
if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
} else {
console.log('No new codeowners to mention in comment (all previously pinged)');
}
} catch (error) {
if (error.status === 422) {
console.log('Some reviewers may already be requested or unavailable:', error.message);
// Try to add a comment even if review request failed
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
// Only try to add a comment if there are new codeowners to mention
if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
} catch (commentError) {
console.log('Failed to add comment:', commentError.message);
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
} catch (commentError) {
console.log('Failed to add comment:', commentError.message);
}
} else {
console.log('No new codeowners to mention in fallback comment');
}
} else {
throw error;

View File

@ -92,10 +92,49 @@ jobs:
mention !== `@${issueAuthor}`
);
const allMentions = [...filteredUserOwners, ...teamOwners];
// Check for previous comments from this workflow to avoid duplicate pings
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issue_number
});
const previouslyPingedUsers = new Set();
const previouslyPingedTeams = new Set();
// Look for comments from github-actions bot that contain codeowner pings for this component
const workflowComments = comments.filter(comment =>
comment.user.type === 'Bot' &&
comment.user.login === 'github-actions[bot]' &&
comment.body.includes(`component: ${componentName}`) &&
comment.body.includes("you've been identified as a codeowner")
);
// Extract previously mentioned users and teams from workflow comments
for (const comment of workflowComments) {
// Match @username patterns (not team mentions)
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
userMentions.forEach(mention => {
previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison
});
// Match @org/team patterns
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || [];
teamMentions.forEach(mention => {
previouslyPingedTeams.add(mention);
});
}
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`);
// Remove previously pinged users and teams
const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention));
const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention));
const allMentions = [...newUserOwners, ...newTeamOwners];
if (allMentions.length === 0) {
console.log('No codeowners to notify (issue author is the only codeowner)');
console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)');
return;
}
@ -111,7 +150,7 @@ jobs:
body: commentBody
});
console.log(`Successfully notified codeowners: ${mentionString}`);
console.log(`Successfully notified new codeowners: ${mentionString}`);
} catch (error) {
console.log('Failed to process codeowner notifications:', error.message);

View File

@ -230,14 +230,16 @@ message DeviceInfoResponse {
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
// Deprecated in API version 1.9
uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
string manufacturer = 12;
string friendly_name = 13;
uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
// Deprecated in API version 1.10
uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"];
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
@ -337,7 +339,9 @@ message ListEntitiesCoverResponse {
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
}
// Deprecated in API version 1.1
enum LegacyCoverState {
option deprecated = true;
LEGACY_COVER_STATE_OPEN = 0;
LEGACY_COVER_STATE_CLOSED = 1;
}
@ -356,7 +360,8 @@ message CoverStateResponse {
fixed32 key = 1;
// legacy: state has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change
LegacyCoverState legacy_state = 2;
// Deprecated in API version 1.1
LegacyCoverState legacy_state = 2 [deprecated=true];
float position = 3;
float tilt = 4;
@ -364,7 +369,9 @@ message CoverStateResponse {
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
}
// Deprecated in API version 1.1
enum LegacyCoverCommand {
option deprecated = true;
LEGACY_COVER_COMMAND_OPEN = 0;
LEGACY_COVER_COMMAND_CLOSE = 1;
LEGACY_COVER_COMMAND_STOP = 2;
@ -380,8 +387,10 @@ message CoverCommandRequest {
// legacy: command has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change
bool has_legacy_command = 2;
LegacyCoverCommand legacy_command = 3;
// Deprecated in API version 1.1
bool has_legacy_command = 2 [deprecated=true];
// Deprecated in API version 1.1
LegacyCoverCommand legacy_command = 3 [deprecated=true];
bool has_position = 4;
float position = 5;
@ -413,7 +422,9 @@ message ListEntitiesFanResponse {
repeated string supported_preset_modes = 12;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
}
// Deprecated in API version 1.6 - only used in deprecated fields
enum FanSpeed {
option deprecated = true;
FAN_SPEED_LOW = 0;
FAN_SPEED_MEDIUM = 1;
FAN_SPEED_HIGH = 2;
@ -432,7 +443,8 @@ message FanStateResponse {
fixed32 key = 1;
bool state = 2;
bool oscillating = 3;
FanSpeed speed = 4 [deprecated = true];
// Deprecated in API version 1.6
FanSpeed speed = 4 [deprecated=true];
FanDirection direction = 5;
int32 speed_level = 6;
string preset_mode = 7;
@ -448,8 +460,10 @@ message FanCommandRequest {
fixed32 key = 1;
bool has_state = 2;
bool state = 3;
bool has_speed = 4 [deprecated = true];
FanSpeed speed = 5 [deprecated = true];
// Deprecated in API version 1.6
bool has_speed = 4 [deprecated=true];
// Deprecated in API version 1.6
FanSpeed speed = 5 [deprecated=true];
bool has_oscillating = 6;
bool oscillating = 7;
bool has_direction = 8;
@ -488,9 +502,13 @@ message ListEntitiesLightResponse {
repeated ColorMode supported_color_modes = 12;
// next four supports_* are for legacy clients, newer clients should use color modes
// Deprecated in API version 1.6
bool legacy_supports_brightness = 5 [deprecated=true];
// Deprecated in API version 1.6
bool legacy_supports_rgb = 6 [deprecated=true];
// Deprecated in API version 1.6
bool legacy_supports_white_value = 7 [deprecated=true];
// Deprecated in API version 1.6
bool legacy_supports_color_temperature = 8 [deprecated=true];
float min_mireds = 9;
float max_mireds = 10;
@ -567,7 +585,9 @@ enum SensorStateClass {
STATE_CLASS_TOTAL = 3;
}
// Deprecated in API version 1.5
enum SensorLastResetType {
option deprecated = true;
LAST_RESET_NONE = 0;
LAST_RESET_NEVER = 1;
LAST_RESET_AUTO = 2;
@ -591,7 +611,8 @@ message ListEntitiesSensorResponse {
string device_class = 9;
SensorStateClass state_class = 10;
// Last reset type removed in 2021.9.0
SensorLastResetType legacy_last_reset_type = 11;
// Deprecated in API version 1.5
SensorLastResetType legacy_last_reset_type = 11 [deprecated=true];
bool disabled_by_default = 12;
EntityCategory entity_category = 13;
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
@ -947,7 +968,8 @@ message ListEntitiesClimateResponse {
float visual_target_temperature_step = 10;
// for older peer versions - in new system this
// is if CLIMATE_PRESET_AWAY exists is supported_presets
bool legacy_supports_away = 11;
// Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12;
repeated ClimateFanMode supported_fan_modes = 13;
repeated ClimateSwingMode supported_swing_modes = 14;
@ -978,7 +1000,8 @@ message ClimateStateResponse {
float target_temperature_low = 5;
float target_temperature_high = 6;
// For older peers, equal to preset == CLIMATE_PRESET_AWAY
bool unused_legacy_away = 7;
// Deprecated in API version 1.5
bool unused_legacy_away = 7 [deprecated=true];
ClimateAction action = 8;
ClimateFanMode fan_mode = 9;
ClimateSwingMode swing_mode = 10;
@ -1006,8 +1029,10 @@ message ClimateCommandRequest {
bool has_target_temperature_high = 8;
float target_temperature_high = 9;
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
bool unused_has_legacy_away = 10;
bool unused_legacy_away = 11;
// Deprecated in API version 1.5
bool unused_has_legacy_away = 10 [deprecated=true];
// Deprecated in API version 1.5
bool unused_legacy_away = 11 [deprecated=true];
bool has_fan_mode = 12;
ClimateFanMode fan_mode = 13;
bool has_swing_mode = 14;
@ -1354,12 +1379,17 @@ message SubscribeBluetoothLEAdvertisementsRequest {
uint32 flags = 1;
}
// Deprecated - only used by deprecated BluetoothLEAdvertisementResponse
message BluetoothServiceData {
option deprecated = true;
string uuid = 1;
repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7
// Deprecated in API version 1.7
repeated uint32 legacy_data = 2 [deprecated=true]; // Removed in api version 1.7
bytes data = 3; // Added in api version 1.7
}
// Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead
message BluetoothLEAdvertisementResponse {
option deprecated = true;
option (id) = 67;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY";

View File

@ -363,8 +363,6 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
auto *cover = static_cast<cover::Cover *>(entity);
CoverStateResponse msg;
auto traits = cover->get_traits();
msg.legacy_state =
(cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED;
msg.position = cover->position;
if (traits.get_supports_tilt())
msg.tilt = cover->tilt;
@ -386,19 +384,6 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
}
void APIConnection::cover_command(const CoverCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
if (msg.has_legacy_command) {
switch (msg.legacy_command) {
case enums::LEGACY_COVER_COMMAND_OPEN:
call.set_command_open();
break;
case enums::LEGACY_COVER_COMMAND_CLOSE:
call.set_command_close();
break;
case enums::LEGACY_COVER_COMMAND_STOP:
call.set_command_stop();
break;
}
}
if (msg.has_position)
call.set_position(msg.position);
if (msg.has_tilt)
@ -496,14 +481,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto traits = light->get_traits();
for (auto mode : traits.get_supported_color_modes())
msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS);
msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB);
msg.legacy_supports_white_value =
msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE));
msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE);
if (msg.legacy_supports_color_temperature) {
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds();
msg.max_mireds = traits.get_max_mireds();
}
@ -693,7 +672,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
msg.supports_action = traits.get_supports_action();
for (auto fan_mode : traits.get_supported_fan_modes())
msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
@ -1114,21 +1092,6 @@ void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoo
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
}
bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) {
if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) {
BluetoothLEAdvertisementResponse resp = msg;
for (auto &service : resp.service_data) {
service.legacy_data.assign(service.data.begin(), service.data.end());
service.data.clear();
}
for (auto &manufacturer_data : resp.manufacturer_data) {
manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end());
manufacturer_data.data.clear();
}
return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
}
return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
}
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
}
@ -1499,12 +1462,10 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.webserver_port = USE_WEBSERVER_PORT;
#endif
#ifdef USE_BLUETOOTH_PROXY
resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version();
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
#endif
#ifdef USE_VOICE_ASSISTANT
resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version();
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
#endif
#ifdef USE_API_NOISE
@ -1671,6 +1632,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) {
}
void APIConnection::process_batch_() {
// Ensure PacketInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<PacketInfo>::value,
"PacketInfo must remain trivially destructible with this placement-new approach");
if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false;
return;
@ -1708,9 +1673,12 @@ void APIConnection::process_batch_() {
return;
}
// Pre-allocate storage for packet info
std::vector<PacketInfo> packet_info;
packet_info.reserve(num_items);
size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH);
// Stack-allocated array for packet info
alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)];
PacketInfo *packet_info = reinterpret_cast<PacketInfo *>(packet_info_storage);
size_t packet_count = 0;
// Cache these values to avoid repeated virtual calls
const uint8_t header_padding = this->helper_->frame_header_padding();
@ -1742,8 +1710,8 @@ void APIConnection::process_batch_() {
// The actual message data follows after the header padding
uint32_t current_offset = 0;
// Process items and encode directly to buffer
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
// Process items and encode directly to buffer (up to our limit)
for (size_t i = 0; i < packets_to_process; i++) {
const auto &item = this->deferred_batch_[i];
// Try to encode message
// The creator will calculate overhead to determine if the message fits
@ -1757,7 +1725,11 @@ void APIConnection::process_batch_() {
// Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
packet_info.emplace_back(item.message_type, current_offset, proto_payload_size);
// Use placement new to construct PacketInfo in pre-allocated stack array
// This avoids default-constructing all MAX_PACKETS_PER_BATCH elements
// Explicit destruction is not needed because PacketInfo is trivially destructible,
// as ensured by the static_assert in its definition.
new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size);
// Update tracking variables
items_processed++;
@ -1783,8 +1755,8 @@ void APIConnection::process_batch_() {
}
// Send all collected packets
APIError err =
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info);
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()},
std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),

View File

@ -33,7 +33,17 @@ struct ClientInfo {
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending
static constexpr size_t MAX_INITIAL_PER_BATCH = 20;
// This was increased from 20 to 24 after removing the unique_id field from entity info messages,
// which reduced message sizes allowing more entities per batch without exceeding packet limits
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
// Maximum number of packets to process in a single batch (platform-dependent)
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
#if defined(USE_ESP32) || defined(USE_HOST)
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
#else
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
#endif
class APIConnection : public APIServerConnection {
public:
@ -130,7 +140,6 @@ class APIConnection : public APIServerConnection {
#ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg);
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;

View File

@ -79,33 +79,56 @@ APIError APIFrameHelper::loop() {
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
}
// Common socket write error handling
APIError APIFrameHelper::handle_socket_write_error_() {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
HELPER_LOG("Socket write failed with errno %d", errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) {
SendBuffer buffer;
buffer.data.reserve(total_write_len);
buffer.size = total_write_len - offset;
buffer.data = std::make_unique<uint8_t[]>(buffer.size);
uint16_t to_skip = offset;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base);
buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len);
if (to_skip >= iov[i].iov_len) {
// Skip this entire segment
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
// Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer.data.get() + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
this->tx_buf_.push_back(std::move(buffer));
}
// This method writes data to socket or buffers it
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
uint16_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += static_cast<uint16_t>(iov[i].iov_len);
}
#endif
// Try to send any existing buffered data first if there is any
if (!this->tx_buf_.empty()) {
@ -118,7 +141,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
// If there is still data in the buffer, we can't send, buffer
// the new data and return
if (!this->tx_buf_.empty()) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
}
@ -127,37 +150,16 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
ssize_t sent = this->socket_->writev(iov, iovcnt);
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
APIError err = this->handle_socket_write_error_();
if (err == APIError::WOULD_BLOCK) {
// Socket would block, buffer the data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
// Socket error
HELPER_LOG("Socket write failed with errno %d", errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
return err; // Socket write failed
} else if (static_cast<uint16_t>(sent) < total_write_len) {
// Partially sent, buffer the remaining data
SendBuffer buffer;
uint16_t to_consume = static_cast<uint16_t>(sent);
uint16_t remaining = total_write_len - static_cast<uint16_t>(sent);
buffer.data.reserve(remaining);
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
// This segment was fully sent
to_consume -= static_cast<uint16_t>(iov[i].iov_len);
} else {
// This segment was partially sent or not sent at all
const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_consume;
buffer.data.insert(buffer.data.end(), data, data + len);
to_consume = 0;
}
}
this->tx_buf_.push_back(std::move(buffer));
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
}
return APIError::OK; // Success, all data sent or buffered
@ -176,14 +178,7 @@ APIError APIFrameHelper::try_send_tx_buf_() {
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
if (sent == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
// Real socket error (not just would block)
HELPER_LOG("Socket write failed with errno %d", errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// Socket would block, we'll try again later
return APIError::WOULD_BLOCK;
return this->handle_socket_write_error_();
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
@ -299,6 +294,26 @@ APIError APINoiseFrameHelper::init() {
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
// Helper for handling handshake frame errors
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
} else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
}
return aerr;
}
// Helper for handling noise library errors
APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) {
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str());
return api_err;
}
return APIError::OK;
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
// During handshake phase, process as many actions as possible until we can't progress
@ -306,12 +321,12 @@ APIError APINoiseFrameHelper::loop() {
// WOULD_BLOCK when no more data is available to read
while (state_ != State::DATA && this->socket_->ready()) {
APIError err = state_action_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
if (err == APIError::WOULD_BLOCK) {
break;
}
if (err != APIError::OK) {
return err;
}
}
// Use base class implementation for buffer sending
@ -332,7 +347,7 @@ APIError APINoiseFrameHelper::loop() {
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/
APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
@ -395,7 +410,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str());
#endif
frame->msg = std::move(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
@ -421,24 +436,17 @@ APIError APINoiseFrameHelper::state_action_() {
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
ParsedFrame frame;
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
return aerr;
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
return aerr;
}
if (aerr != APIError::OK)
return aerr;
// ignore contents, may be used in future for flags
// Reserve space for: existing prologue + 2 size bytes + frame data
prologue_.reserve(prologue_.size() + 2 + frame.msg.size());
prologue_.push_back((uint8_t) (frame.msg.size() >> 8));
prologue_.push_back((uint8_t) frame.msg.size());
prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
prologue_.reserve(prologue_.size() + 2 + frame.size());
prologue_.push_back((uint8_t) (frame.size() >> 8));
prologue_.push_back((uint8_t) frame.size());
prologue_.insert(prologue_.end(), frame.begin(), frame.end());
state_ = State::SERVER_HELLO;
}
@ -476,41 +484,29 @@ APIError APINoiseFrameHelper::state_action_() {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
ParsedFrame frame;
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
return aerr;
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
return aerr;
}
if (aerr != APIError::OK)
return aerr;
if (frame.msg.empty()) {
if (frame.empty()) {
send_explicit_handshake_reject_("Empty handshake message");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame.msg[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]);
} else if (frame[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
send_explicit_handshake_reject_("Bad handshake error byte");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1);
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str());
if (err == NOISE_ERROR_MAC_FAILURE) {
send_explicit_handshake_reject_("Handshake MAC failure");
} else {
send_explicit_handshake_reject_("Handshake error");
}
return APIError::HANDSHAKESTATE_READ_FAILED;
// Special handling for MAC failure
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error");
return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED);
}
aerr = check_handshake_finished_();
@ -523,11 +519,10 @@ APIError APINoiseFrameHelper::state_action_() {
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_WRITE_FAILED;
}
APIError aerr_write =
handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr_write != APIError::OK)
return aerr_write;
buffer[0] = 0x00; // success
aerr = write_frame_(buffer, mbuf.size + 1);
@ -576,23 +571,21 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::WOULD_BLOCK;
}
ParsedFrame frame;
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
return APIError::CIPHERSTATE_DECRYPT_FAILED;
}
APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED);
if (decrypt_err != APIError::OK)
return decrypt_err;
uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.msg.data();
uint8_t *msg_data = frame.data();
if (msg_size < 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size);
@ -607,7 +600,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET;
}
buffer->container = std::move(frame.msg);
buffer->container = std::move(frame);
buffer->data_offset = 4;
buffer->data_len = data_len;
buffer->type = type;
@ -640,6 +633,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
// We need to encrypt each packet in place
for (const auto &packet : packets) {
@ -668,23 +662,22 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
4 + packet.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str());
return APIError::CIPHERSTATE_ENCRYPT_FAILED;
}
APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED);
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted packet
this->reusable_iovs_.push_back(
{buf_start, static_cast<size_t>(3 + mbuf.size)}); // indicator + size + encrypted data
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
this->reusable_iovs_.push_back({buf_start, packet_len});
total_write_len += packet_len;
}
// Send all encrypted packets in one writev call
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size());
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@ -697,12 +690,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return this->write_raw_(iov, 1);
return this->write_raw_(iov, 1, 3); // Just header
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_(iov, 2);
return this->write_raw_(iov, 2, 3 + len); // Header + data
}
/** Initiate the data structures for the handshake.
@ -723,35 +716,27 @@ APIError APINoiseFrameHelper::init_handshake_() {
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_ = {};
err = noise_handshakestate_start(handshake_);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
return APIError::OK;
}
@ -767,11 +752,9 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SPLIT_FAILED;
}
APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK)
return aerr;
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
@ -838,7 +821,7 @@ APIError APIPlaintextFrameHelper::loop() {
*
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/
APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
@ -956,7 +939,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str());
#endif
frame->msg = std::move(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
@ -971,7 +954,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::WOULD_BLOCK;
}
ParsedFrame frame;
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) {
@ -991,12 +974,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte";
iov[0].iov_base = (void *) msg;
iov[0].iov_len = 19;
this->write_raw_(iov, 1);
this->write_raw_(iov, 1, 19);
}
return aerr;
}
buffer->container = std::move(frame.msg);
buffer->container = std::move(frame);
buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_;
@ -1021,6 +1004,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
for (const auto &packet : packets) {
// Calculate varint sizes for header layout
@ -1065,12 +1049,13 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Add iovec for this packet (header + payload)
this->reusable_iovs_.push_back(
{buf_start + header_offset, static_cast<size_t>(total_header_len + packet.payload_size)});
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
total_write_len += packet_len;
}
// Send all packets in one writev call
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size());
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
#endif // USE_API_PLAINTEXT

View File

@ -111,29 +111,28 @@ class APIFrameHelper {
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
protected:
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
// Buffer containing data to be sent
struct SendBuffer {
std::vector<uint8_t> data;
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
std::unique_ptr<uint8_t[]> data;
uint16_t size{0}; // Total size of the buffer
uint16_t offset{0}; // Current offset within the buffer
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
const uint8_t *current_data() const { return data.data() + offset; }
uint16_t remaining() const { return size - offset; }
const uint8_t *current_data() const { return data.get() + offset; }
};
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt);
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
// Common socket write error handling
APIError handle_socket_write_error_();
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
@ -210,11 +209,13 @@ class APINoiseFrameHelper : public APIFrameHelper {
protected:
APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame);
APIError try_read_frame_(std::vector<uint8_t> *frame);
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
APIError handle_handshake_frame_error_(APIError aerr);
APIError handle_noise_error_(int err, const char *func_name, APIError api_err);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
@ -263,7 +264,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError try_read_frame_(ParsedFrame *frame);
APIError try_read_frame_(std::vector<uint8_t> *frame);
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;

View File

@ -94,17 +94,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_WEBSERVER
buffer.encode_uint32(10, this->webserver_port);
#endif
#ifdef USE_BLUETOOTH_PROXY
buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version);
#endif
#ifdef USE_BLUETOOTH_PROXY
buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags);
#endif
buffer.encode_string(12, this->manufacturer);
buffer.encode_string(13, this->friendly_name);
#ifdef USE_VOICE_ASSISTANT
buffer.encode_uint32(14, this->legacy_voice_assistant_version);
#endif
#ifdef USE_VOICE_ASSISTANT
buffer.encode_uint32(17, this->voice_assistant_feature_flags);
#endif
@ -150,17 +144,11 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const {
#ifdef USE_WEBSERVER
ProtoSize::add_uint32_field(total_size, 1, this->webserver_port);
#endif
#ifdef USE_BLUETOOTH_PROXY
ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version);
#endif
#ifdef USE_BLUETOOTH_PROXY
ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags);
#endif
ProtoSize::add_string_field(total_size, 1, this->manufacturer);
ProtoSize::add_string_field(total_size, 1, this->friendly_name);
#ifdef USE_VOICE_ASSISTANT
ProtoSize::add_uint32_field(total_size, 1, this->legacy_voice_assistant_version);
#endif
#ifdef USE_VOICE_ASSISTANT
ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags);
#endif
@ -270,7 +258,6 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const {
}
void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_uint32(2, static_cast<uint32_t>(this->legacy_state));
buffer.encode_float(3, this->position);
buffer.encode_float(4, this->tilt);
buffer.encode_uint32(5, static_cast<uint32_t>(this->current_operation));
@ -280,7 +267,6 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
}
void CoverStateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed32_field(total_size, 1, this->key);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->legacy_state));
ProtoSize::add_float_field(total_size, 1, this->position);
ProtoSize::add_float_field(total_size, 1, this->tilt);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->current_operation));
@ -290,12 +276,6 @@ void CoverStateResponse::calculate_size(uint32_t &total_size) const {
}
bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2:
this->has_legacy_command = value.as_bool();
break;
case 3:
this->legacy_command = static_cast<enums::LegacyCoverCommand>(value.as_uint32());
break;
case 4:
this->has_position = value.as_bool();
break;
@ -379,7 +359,6 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->state);
buffer.encode_bool(3, this->oscillating);
buffer.encode_uint32(4, static_cast<uint32_t>(this->speed));
buffer.encode_uint32(5, static_cast<uint32_t>(this->direction));
buffer.encode_int32(6, this->speed_level);
buffer.encode_string(7, this->preset_mode);
@ -391,7 +370,6 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed32_field(total_size, 1, this->key);
ProtoSize::add_bool_field(total_size, 1, this->state);
ProtoSize::add_bool_field(total_size, 1, this->oscillating);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->speed));
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->direction));
ProtoSize::add_int32_field(total_size, 1, this->speed_level);
ProtoSize::add_string_field(total_size, 1, this->preset_mode);
@ -407,12 +385,6 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
case 3:
this->state = value.as_bool();
break;
case 4:
this->has_speed = value.as_bool();
break;
case 5:
this->speed = static_cast<enums::FanSpeed>(value.as_uint32());
break;
case 6:
this->has_oscillating = value.as_bool();
break;
@ -473,10 +445,6 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->supported_color_modes) {
buffer.encode_uint32(12, static_cast<uint32_t>(it), true);
}
buffer.encode_bool(5, this->legacy_supports_brightness);
buffer.encode_bool(6, this->legacy_supports_rgb);
buffer.encode_bool(7, this->legacy_supports_white_value);
buffer.encode_bool(8, this->legacy_supports_color_temperature);
buffer.encode_float(9, this->min_mireds);
buffer.encode_float(10, this->max_mireds);
for (auto &it : this->effects) {
@ -500,10 +468,6 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_enum_field_repeated(total_size, 1, static_cast<uint32_t>(it));
}
}
ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_brightness);
ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb);
ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value);
ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature);
ProtoSize::add_float_field(total_size, 1, this->min_mireds);
ProtoSize::add_float_field(total_size, 1, this->max_mireds);
if (!this->effects.empty()) {
@ -677,7 +641,6 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(8, this->force_update);
buffer.encode_string(9, this->device_class);
buffer.encode_uint32(10, static_cast<uint32_t>(this->state_class));
buffer.encode_uint32(11, static_cast<uint32_t>(this->legacy_last_reset_type));
buffer.encode_bool(12, this->disabled_by_default);
buffer.encode_uint32(13, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
@ -696,7 +659,6 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->force_update);
ProtoSize::add_string_field(total_size, 1, this->device_class);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->state_class));
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->legacy_last_reset_type));
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
@ -1105,7 +1067,6 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_float(8, this->visual_min_temperature);
buffer.encode_float(9, this->visual_max_temperature);
buffer.encode_float(10, this->visual_target_temperature_step);
buffer.encode_bool(11, this->legacy_supports_away);
buffer.encode_bool(12, this->supports_action);
for (auto &it : this->supported_fan_modes) {
buffer.encode_uint32(13, static_cast<uint32_t>(it), true);
@ -1150,7 +1111,6 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_float_field(total_size, 1, this->visual_min_temperature);
ProtoSize::add_float_field(total_size, 1, this->visual_max_temperature);
ProtoSize::add_float_field(total_size, 1, this->visual_target_temperature_step);
ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away);
ProtoSize::add_bool_field(total_size, 1, this->supports_action);
if (!this->supported_fan_modes.empty()) {
for (const auto &it : this->supported_fan_modes) {
@ -1198,7 +1158,6 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_float(4, this->target_temperature);
buffer.encode_float(5, this->target_temperature_low);
buffer.encode_float(6, this->target_temperature_high);
buffer.encode_bool(7, this->unused_legacy_away);
buffer.encode_uint32(8, static_cast<uint32_t>(this->action));
buffer.encode_uint32(9, static_cast<uint32_t>(this->fan_mode));
buffer.encode_uint32(10, static_cast<uint32_t>(this->swing_mode));
@ -1218,7 +1177,6 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_float_field(total_size, 1, this->target_temperature);
ProtoSize::add_float_field(total_size, 1, this->target_temperature_low);
ProtoSize::add_float_field(total_size, 1, this->target_temperature_high);
ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->action));
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->fan_mode));
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->swing_mode));
@ -1248,12 +1206,6 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
case 8:
this->has_target_temperature_high = value.as_bool();
break;
case 10:
this->unused_has_legacy_away = value.as_bool();
break;
case 11:
this->unused_legacy_away = value.as_bool();
break;
case 12:
this->has_fan_mode = value.as_bool();
break;
@ -1869,50 +1821,6 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
}
return true;
}
void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->uuid);
for (auto &it : this->legacy_data) {
buffer.encode_uint32(2, it, true);
}
buffer.encode_bytes(3, reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size());
}
void BluetoothServiceData::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->uuid);
if (!this->legacy_data.empty()) {
for (const auto &it : this->legacy_data) {
ProtoSize::add_uint32_field_repeated(total_size, 1, it);
}
}
ProtoSize::add_string_field(total_size, 1, this->data);
}
void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address);
buffer.encode_bytes(2, reinterpret_cast<const uint8_t *>(this->name.data()), this->name.size());
buffer.encode_sint32(3, this->rssi);
for (auto &it : this->service_uuids) {
buffer.encode_string(4, it, true);
}
for (auto &it : this->service_data) {
buffer.encode_message(5, it, true);
}
for (auto &it : this->manufacturer_data) {
buffer.encode_message(6, it, true);
}
buffer.encode_uint32(7, this->address_type);
}
void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_uint64_field(total_size, 1, this->address);
ProtoSize::add_string_field(total_size, 1, this->name);
ProtoSize::add_sint32_field(total_size, 1, this->rssi);
if (!this->service_uuids.empty()) {
for (const auto &it : this->service_uuids) {
ProtoSize::add_string_field_repeated(total_size, 1, it);
}
}
ProtoSize::add_repeated_message(total_size, 1, this->service_data);
ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data);
ProtoSize::add_uint32_field(total_size, 1, this->address_type);
}
void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address);
buffer.encode_sint32(2, this->rssi);

View File

@ -17,27 +17,13 @@ enum EntityCategory : uint32_t {
ENTITY_CATEGORY_DIAGNOSTIC = 2,
};
#ifdef USE_COVER
enum LegacyCoverState : uint32_t {
LEGACY_COVER_STATE_OPEN = 0,
LEGACY_COVER_STATE_CLOSED = 1,
};
enum CoverOperation : uint32_t {
COVER_OPERATION_IDLE = 0,
COVER_OPERATION_IS_OPENING = 1,
COVER_OPERATION_IS_CLOSING = 2,
};
enum LegacyCoverCommand : uint32_t {
LEGACY_COVER_COMMAND_OPEN = 0,
LEGACY_COVER_COMMAND_CLOSE = 1,
LEGACY_COVER_COMMAND_STOP = 2,
};
#endif
#ifdef USE_FAN
enum FanSpeed : uint32_t {
FAN_SPEED_LOW = 0,
FAN_SPEED_MEDIUM = 1,
FAN_SPEED_HIGH = 2,
};
enum FanDirection : uint32_t {
FAN_DIRECTION_FORWARD = 0,
FAN_DIRECTION_REVERSE = 1,
@ -65,11 +51,6 @@ enum SensorStateClass : uint32_t {
STATE_CLASS_TOTAL_INCREASING = 2,
STATE_CLASS_TOTAL = 3,
};
enum SensorLastResetType : uint32_t {
LAST_RESET_NONE = 0,
LAST_RESET_NEVER = 1,
LAST_RESET_AUTO = 2,
};
#endif
enum LogLevel : uint32_t {
LOG_LEVEL_NONE = 0,
@ -485,7 +466,7 @@ class DeviceInfo : public ProtoMessage {
class DeviceInfoResponse : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint8_t ESTIMATED_SIZE = 219;
static constexpr uint8_t ESTIMATED_SIZE = 211;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; }
#endif
@ -507,17 +488,11 @@ class DeviceInfoResponse : public ProtoMessage {
#ifdef USE_WEBSERVER
uint32_t webserver_port{0};
#endif
#ifdef USE_BLUETOOTH_PROXY
uint32_t legacy_bluetooth_proxy_version{0};
#endif
#ifdef USE_BLUETOOTH_PROXY
uint32_t bluetooth_proxy_feature_flags{0};
#endif
std::string manufacturer{};
std::string friendly_name{};
#ifdef USE_VOICE_ASSISTANT
uint32_t legacy_voice_assistant_version{0};
#endif
#ifdef USE_VOICE_ASSISTANT
uint32_t voice_assistant_feature_flags{0};
#endif
@ -646,11 +621,10 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
class CoverStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 22;
static constexpr uint8_t ESTIMATED_SIZE = 23;
static constexpr uint8_t ESTIMATED_SIZE = 21;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "cover_state_response"; }
#endif
enums::LegacyCoverState legacy_state{};
float position{0.0f};
float tilt{0.0f};
enums::CoverOperation current_operation{};
@ -665,12 +639,10 @@ class CoverStateResponse : public StateResponseProtoMessage {
class CoverCommandRequest : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 30;
static constexpr uint8_t ESTIMATED_SIZE = 29;
static constexpr uint8_t ESTIMATED_SIZE = 25;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "cover_command_request"; }
#endif
bool has_legacy_command{false};
enums::LegacyCoverCommand legacy_command{};
bool has_position{false};
float position{0.0f};
bool has_tilt{false};
@ -709,13 +681,12 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
class FanStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 23;
static constexpr uint8_t ESTIMATED_SIZE = 30;
static constexpr uint8_t ESTIMATED_SIZE = 28;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_state_response"; }
#endif
bool state{false};
bool oscillating{false};
enums::FanSpeed speed{};
enums::FanDirection direction{};
int32_t speed_level{0};
std::string preset_mode{};
@ -730,14 +701,12 @@ class FanStateResponse : public StateResponseProtoMessage {
class FanCommandRequest : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 42;
static constexpr uint8_t ESTIMATED_SIZE = 38;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; }
#endif
bool has_state{false};
bool state{false};
bool has_speed{false};
enums::FanSpeed speed{};
bool has_oscillating{false};
bool oscillating{false};
bool has_direction{false};
@ -760,15 +729,11 @@ class FanCommandRequest : public CommandProtoMessage {
class ListEntitiesLightResponse : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 15;
static constexpr uint8_t ESTIMATED_SIZE = 81;
static constexpr uint8_t ESTIMATED_SIZE = 73;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; }
#endif
std::vector<enums::ColorMode> supported_color_modes{};
bool legacy_supports_brightness{false};
bool legacy_supports_rgb{false};
bool legacy_supports_white_value{false};
bool legacy_supports_color_temperature{false};
float min_mireds{0.0f};
float max_mireds{0.0f};
std::vector<std::string> effects{};
@ -854,7 +819,7 @@ class LightCommandRequest : public CommandProtoMessage {
class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 16;
static constexpr uint8_t ESTIMATED_SIZE = 68;
static constexpr uint8_t ESTIMATED_SIZE = 66;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_sensor_response"; }
#endif
@ -863,7 +828,6 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
bool force_update{false};
std::string device_class{};
enums::SensorStateClass state_class{};
enums::SensorLastResetType legacy_last_reset_type{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@ -1289,7 +1253,7 @@ class CameraImageRequest : public ProtoDecodableMessage {
class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 147;
static constexpr uint8_t ESTIMATED_SIZE = 145;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_climate_response"; }
#endif
@ -1299,7 +1263,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f};
bool legacy_supports_away{false};
bool supports_action{false};
std::vector<enums::ClimateFanMode> supported_fan_modes{};
std::vector<enums::ClimateSwingMode> supported_swing_modes{};
@ -1322,7 +1285,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
class ClimateStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 47;
static constexpr uint8_t ESTIMATED_SIZE = 70;
static constexpr uint8_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_state_response"; }
#endif
@ -1331,7 +1294,6 @@ class ClimateStateResponse : public StateResponseProtoMessage {
float target_temperature{0.0f};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
bool unused_legacy_away{false};
enums::ClimateAction action{};
enums::ClimateFanMode fan_mode{};
enums::ClimateSwingMode swing_mode{};
@ -1351,7 +1313,7 @@ class ClimateStateResponse : public StateResponseProtoMessage {
class ClimateCommandRequest : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 88;
static constexpr uint8_t ESTIMATED_SIZE = 84;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; }
#endif
@ -1363,8 +1325,6 @@ class ClimateCommandRequest : public CommandProtoMessage {
float target_temperature_low{0.0f};
bool has_target_temperature_high{false};
float target_temperature_high{0.0f};
bool unused_has_legacy_away{false};
bool unused_legacy_away{false};
bool has_fan_mode{false};
enums::ClimateFanMode fan_mode{};
bool has_swing_mode{false};
@ -1736,41 +1696,6 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class BluetoothServiceData : public ProtoMessage {
public:
std::string uuid{};
std::vector<uint32_t> legacy_data{};
std::string data{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class BluetoothLEAdvertisementResponse : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 67;
static constexpr uint8_t ESTIMATED_SIZE = 107;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_le_advertisement_response"; }
#endif
uint64_t address{0};
std::string name{};
int32_t rssi{0};
std::vector<std::string> service_uuids{};
std::vector<BluetoothServiceData> service_data{};
std::vector<BluetoothServiceData> manufacturer_data{};
uint32_t address_type{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class BluetoothLERawAdvertisement : public ProtoMessage {
public:
uint64_t address{0};

View File

@ -23,16 +23,6 @@ template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::Entity
}
}
#ifdef USE_COVER
template<> const char *proto_enum_to_string<enums::LegacyCoverState>(enums::LegacyCoverState value) {
switch (value) {
case enums::LEGACY_COVER_STATE_OPEN:
return "LEGACY_COVER_STATE_OPEN";
case enums::LEGACY_COVER_STATE_CLOSED:
return "LEGACY_COVER_STATE_CLOSED";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverOperation value) {
switch (value) {
case enums::COVER_OPERATION_IDLE:
@ -45,32 +35,8 @@ template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverO
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::LegacyCoverCommand>(enums::LegacyCoverCommand value) {
switch (value) {
case enums::LEGACY_COVER_COMMAND_OPEN:
return "LEGACY_COVER_COMMAND_OPEN";
case enums::LEGACY_COVER_COMMAND_CLOSE:
return "LEGACY_COVER_COMMAND_CLOSE";
case enums::LEGACY_COVER_COMMAND_STOP:
return "LEGACY_COVER_COMMAND_STOP";
default:
return "UNKNOWN";
}
}
#endif
#ifdef USE_FAN
template<> const char *proto_enum_to_string<enums::FanSpeed>(enums::FanSpeed value) {
switch (value) {
case enums::FAN_SPEED_LOW:
return "FAN_SPEED_LOW";
case enums::FAN_SPEED_MEDIUM:
return "FAN_SPEED_MEDIUM";
case enums::FAN_SPEED_HIGH:
return "FAN_SPEED_HIGH";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirection value) {
switch (value) {
case enums::FAN_DIRECTION_FORWARD:
@ -127,18 +93,6 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::SensorLastResetType>(enums::SensorLastResetType value) {
switch (value) {
case enums::LAST_RESET_NONE:
return "LAST_RESET_NONE";
case enums::LAST_RESET_NEVER:
return "LAST_RESET_NEVER";
case enums::LAST_RESET_AUTO:
return "LAST_RESET_AUTO";
default:
return "UNKNOWN";
}
}
#endif
template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) {
switch (value) {
@ -737,13 +691,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
out.append(buffer);
out.append("\n");
#endif
#ifdef USE_BLUETOOTH_PROXY
out.append(" legacy_bluetooth_proxy_version: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version);
out.append(buffer);
out.append("\n");
#endif
#ifdef USE_BLUETOOTH_PROXY
out.append(" bluetooth_proxy_feature_flags: ");
@ -760,13 +707,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
out.append("'").append(this->friendly_name).append("'");
out.append("\n");
#ifdef USE_VOICE_ASSISTANT
out.append(" legacy_voice_assistant_version: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version);
out.append(buffer);
out.append("\n");
#endif
#ifdef USE_VOICE_ASSISTANT
out.append(" voice_assistant_feature_flags: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags);
@ -961,10 +901,6 @@ void CoverStateResponse::dump_to(std::string &out) const {
out.append(buffer);
out.append("\n");
out.append(" legacy_state: ");
out.append(proto_enum_to_string<enums::LegacyCoverState>(this->legacy_state));
out.append("\n");
out.append(" position: ");
snprintf(buffer, sizeof(buffer), "%g", this->position);
out.append(buffer);
@ -996,14 +932,6 @@ void CoverCommandRequest::dump_to(std::string &out) const {
out.append(buffer);
out.append("\n");
out.append(" has_legacy_command: ");
out.append(YESNO(this->has_legacy_command));
out.append("\n");
out.append(" legacy_command: ");
out.append(proto_enum_to_string<enums::LegacyCoverCommand>(this->legacy_command));
out.append("\n");
out.append(" has_position: ");
out.append(YESNO(this->has_position));
out.append("\n");
@ -1115,10 +1043,6 @@ void FanStateResponse::dump_to(std::string &out) const {
out.append(YESNO(this->oscillating));
out.append("\n");
out.append(" speed: ");
out.append(proto_enum_to_string<enums::FanSpeed>(this->speed));
out.append("\n");
out.append(" direction: ");
out.append(proto_enum_to_string<enums::FanDirection>(this->direction));
out.append("\n");
@ -1157,14 +1081,6 @@ void FanCommandRequest::dump_to(std::string &out) const {
out.append(YESNO(this->state));
out.append("\n");
out.append(" has_speed: ");
out.append(YESNO(this->has_speed));
out.append("\n");
out.append(" speed: ");
out.append(proto_enum_to_string<enums::FanSpeed>(this->speed));
out.append("\n");
out.append(" has_oscillating: ");
out.append(YESNO(this->has_oscillating));
out.append("\n");
@ -1231,22 +1147,6 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
out.append("\n");
}
out.append(" legacy_supports_brightness: ");
out.append(YESNO(this->legacy_supports_brightness));
out.append("\n");
out.append(" legacy_supports_rgb: ");
out.append(YESNO(this->legacy_supports_rgb));
out.append("\n");
out.append(" legacy_supports_white_value: ");
out.append(YESNO(this->legacy_supports_white_value));
out.append("\n");
out.append(" legacy_supports_color_temperature: ");
out.append(YESNO(this->legacy_supports_color_temperature));
out.append("\n");
out.append(" min_mireds: ");
snprintf(buffer, sizeof(buffer), "%g", this->min_mireds);
out.append(buffer);
@ -1537,10 +1437,6 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
out.append("\n");
out.append(" legacy_last_reset_type: ");
out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type));
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
@ -2107,10 +2003,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
out.append(buffer);
out.append("\n");
out.append(" legacy_supports_away: ");
out.append(YESNO(this->legacy_supports_away));
out.append("\n");
out.append(" supports_action: ");
out.append(YESNO(this->supports_action));
out.append("\n");
@ -2223,10 +2115,6 @@ void ClimateStateResponse::dump_to(std::string &out) const {
out.append(buffer);
out.append("\n");
out.append(" unused_legacy_away: ");
out.append(YESNO(this->unused_legacy_away));
out.append("\n");
out.append(" action: ");
out.append(proto_enum_to_string<enums::ClimateAction>(this->action));
out.append("\n");
@ -2313,14 +2201,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
out.append(buffer);
out.append("\n");
out.append(" unused_has_legacy_away: ");
out.append(YESNO(this->unused_has_legacy_away));
out.append("\n");
out.append(" unused_legacy_away: ");
out.append(YESNO(this->unused_legacy_away));
out.append("\n");
out.append(" has_fan_mode: ");
out.append(YESNO(this->has_fan_mode));
out.append("\n");
@ -3053,66 +2933,6 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const
out.append("\n");
out.append("}");
}
void BluetoothServiceData::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("BluetoothServiceData {\n");
out.append(" uuid: ");
out.append("'").append(this->uuid).append("'");
out.append("\n");
for (const auto &it : this->legacy_data) {
out.append(" legacy_data: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, it);
out.append(buffer);
out.append("\n");
}
out.append(" data: ");
out.append(format_hex_pretty(this->data));
out.append("\n");
out.append("}");
}
void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("BluetoothLEAdvertisementResponse {\n");
out.append(" address: ");
snprintf(buffer, sizeof(buffer), "%llu", this->address);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append(format_hex_pretty(this->name));
out.append("\n");
out.append(" rssi: ");
snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi);
out.append(buffer);
out.append("\n");
for (const auto &it : this->service_uuids) {
out.append(" service_uuids: ");
out.append("'").append(it).append("'");
out.append("\n");
}
for (const auto &it : this->service_data) {
out.append(" service_data: ");
it.dump_to(out);
out.append("\n");
}
for (const auto &it : this->manufacturer_data) {
out.append(" manufacturer_data: ");
it.dump_to(out);
out.append("\n");
}
out.append(" address_type: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type);
out.append(buffer);
out.append("\n");
out.append("}");
}
void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("BluetoothLERawAdvertisement {\n");

View File

@ -13,11 +13,180 @@ namespace bluetooth_proxy {
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() {
ESP_LOGCONFIG(TAG, "BLE Connection:");
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,
esp_ble_gattc_cb_param_t *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) {
case ESP_GATTC_DISCONNECT_EVT: {
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
this->set_address(0);
this->proxy_->send_connections_free();
this->reset_connection_(param->disconnect.reason);
break;
}
case ESP_GATTC_CLOSE_EVT: {
this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason);
this->set_address(0);
this->proxy_->send_connections_free();
this->reset_connection_(param->close.reason);
break;
}
case ESP_GATTC_OPEN_EVT: {
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->set_address(0);
this->proxy_->send_connections_free();
this->reset_connection_(param->open.status);
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
this->proxy_->send_connections_free();

View File

@ -12,6 +12,7 @@ class BluetoothProxy;
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
public:
void dump_config() override;
void loop() override;
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
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;
@ -27,6 +28,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
protected:
friend class BluetoothProxy;
void send_service_for_discovery_();
void reset_connection_(esp_err_t reason);
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
BluetoothProxy *proxy_;

View File

@ -11,19 +11,6 @@ namespace esphome {
namespace 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
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
@ -140,46 +127,6 @@ void BluetoothProxy::flush_pending_advertisements() {
this->advertisement_count_ = 0;
}
#ifdef USE_ESP32_BLE_DEVICE
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
api::BluetoothLEAdvertisementResponse resp;
resp.address = device.address_uint64();
resp.address_type = device.get_address_type();
if (!device.get_name().empty())
resp.name = device.get_name();
resp.rssi = device.get_rssi();
// Pre-allocate vectors based on known sizes
auto service_uuids = device.get_service_uuids();
resp.service_uuids.reserve(service_uuids.size());
for (auto &uuid : service_uuids) {
resp.service_uuids.emplace_back(uuid.to_string());
}
// Pre-allocate service data vector
auto service_datas = device.get_service_datas();
resp.service_data.reserve(service_datas.size());
for (auto &data : service_datas) {
resp.service_data.emplace_back();
auto &service_data = resp.service_data.back();
service_data.uuid = data.uuid.to_string();
service_data.data.assign(data.data.begin(), data.data.end());
}
// Pre-allocate manufacturer data vector
auto manufacturer_datas = device.get_manufacturer_datas();
resp.manufacturer_data.reserve(manufacturer_datas.size());
for (auto &data : manufacturer_datas) {
resp.manufacturer_data.emplace_back();
auto &manufacturer_data = resp.manufacturer_data.back();
manufacturer_data.uuid = data.uuid.to_string();
manufacturer_data.data.assign(data.data.begin(), data.data.end());
}
this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
}
#endif // USE_ESP32_BLE_DEVICE
void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
ESP_LOGCONFIG(TAG,
@ -213,130 +160,12 @@ void BluetoothProxy::loop() {
}
// 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();
// Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) {
if (now - this->last_advertisement_flush_time_ >= 100) {
this->flush_pending_advertisements();
last_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);
}
this->last_advertisement_flush_time_ = now;
}
}

View File

@ -22,6 +22,7 @@ namespace esphome {
namespace bluetooth_proxy {
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
static const int DONE_SENDING_SERVICES = -2;
using namespace esp32_ble_client;
@ -131,9 +132,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
}
protected:
#ifdef USE_ESP32_BLE_DEVICE
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
#endif
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
@ -149,7 +147,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
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_;
uint8_t advertisement_count_{0};
// 2 bytes used, 2 bytes padding

View File

@ -31,6 +31,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
CoreModel,
__version__,
)
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_build_flag(f"-DUSE_ESP32_VARIANT_{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_compat_mode", "strict")

View File

@ -128,46 +128,53 @@ void ESP32BLETracker::loop() {
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
while (read_idx != write_idx) {
// Process one result at a time directly from ring buffer
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx];
// Calculate how many contiguous results we can process in one batch
// 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_) {
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_) {
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_) {
#ifdef USE_ESP32_BLE_DEVICE
ESPBTDevice device;
device.parse_scan_rst(scan_result);
for (size_t i = 0; i < batch_size; i++) {
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
ESPBTDevice device;
device.parse_scan_rst(scan_result);
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
if (!connecting && client->state() == ClientState::DISCOVERED) {
promote_to_connecting = true;
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
if (!connecting && client->state() == ClientState::DISCOVERED) {
promote_to_connecting = true;
}
}
}
}
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
}
}
#endif // USE_ESP32_BLE_DEVICE
}
// Move to next entry in ring buffer
read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
// Update read index for entire batch
read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
// Store with release to ensure reads complete before index update
this->ring_read_index_.store(read_idx, std::memory_order_release);

View File

@ -15,6 +15,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_ESP8266,
CoreModel,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.helpers import copy_file_if_changed
@ -187,6 +188,7 @@ async def to_code(config):
cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(CoreModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])

View File

@ -7,6 +7,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_HOST,
CoreModel,
)
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_build_flag("-std=gnu++20")
cg.add_define("ESPHOME_BOARD", "host")
cg.add_define(CoreModel.MULTI_ATOMICS)
cg.add_platformio_option("platform", "platformio/native")
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")

View File

@ -20,6 +20,7 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
CoreModel,
__version__,
)
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_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]])
cg.add_define(CoreModel.MULTI_NO_ATOMICS)
# force using arduino framework
cg.add_platformio_option("framework", "arduino")

View File

@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import display, spi
import esphome.config_validation as cv
from esphome.const import (
CONF_FLIP_X,
CONF_ID,
CONF_INTENSITY,
CONF_LAMBDA,
@ -14,7 +15,6 @@ CODEOWNERS = ["@rspaargaren"]
DEPENDENCIES = ["spi"]
CONF_ROTATE_CHIP = "rotate_chip"
CONF_FLIP_X = "flip_x"
CONF_SCROLL_SPEED = "scroll_speed"
CONF_SCROLL_DWELL = "scroll_dwell"
CONF_SCROLL_DELAY = "scroll_delay"

View File

@ -16,6 +16,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_RP2040,
CoreModel,
)
from esphome.core import CORE, EsphomeError, coroutine_with_priority
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.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
cg.add_define(CoreModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])

View File

@ -6,6 +6,8 @@ from esphome.const import (
CONF_BRIGHTNESS,
CONF_CONTRAST,
CONF_EXTERNAL_VCC,
CONF_FLIP_X,
CONF_FLIP_Y,
CONF_INVERT,
CONF_LAMBDA,
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)
SSD1306Model = ssd1306_base_ns.enum("SSD1306Model")
CONF_FLIP_X = "flip_x"
CONF_FLIP_Y = "flip_y"
MODELS = {
"SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32,
"SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64,

View File

@ -35,6 +35,14 @@ class Framework(StrEnum):
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):
"""Combined platform-framework identifiers with tuple values."""
@ -375,6 +383,8 @@ CONF_FINGER_ID = "finger_id"
CONF_FINGERPRINT_COUNT = "fingerprint_count"
CONF_FLASH_LENGTH = "flash_length"
CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
CONF_FLIP_X = "flip_x"
CONF_FLIP_Y = "flip_y"
CONF_FLOW = "flow"
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
CONF_FONT = "font"

View File

@ -15,6 +15,9 @@
#define ESPHOME_VARIANT "ESP32"
#define ESPHOME_DEBUG_SCHEDULER
// Default threading model for static analysis (ESP32 is multi-core with atomics)
#define ESPHOME_CORES_MULTI_ATOMICS
// logger
#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE

View File

@ -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);
}
}
#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
// 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->remove = false;
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
#ifndef ESPHOME_CORES_SINGLE
// 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) {
// Put in defer queue for guaranteed FIFO execution
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));
return;
}
#endif
#endif /* not ESPHOME_CORES_SINGLE */
// Get fresh timestamp for new timer/interval - ensures accurate scheduling
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(),
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_};
// 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;
}
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.
// Previously, defer() used the heap which gave undefined order for equal timestamps,
// 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_
// - Items execute in exact order they were deferred (FIFO guarantee)
// - 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
// (ESP8266: single-core, RP2040: empty mutex implementation).
// Single-core platforms don't use this queue and fall back to the heap-based approach.
//
// 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
@ -262,7 +261,7 @@ void HOT Scheduler::call(uint32_t 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
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) {
last_print = now_64;
std::vector<std::unique_ptr<SchedulerItem>> old_items;
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
this->millis_major_, this->last_millis_.load(std::memory_order_relaxed));
#else
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
#ifdef ESPHOME_CORES_MULTI_ATOMICS
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" 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_);
#endif
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
while (!this->empty_()) {
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);
}
}
#endif // ESPHOME_DEBUG_SCHEDULER
#endif /* ESPHOME_DEBUG_SCHEDULER */
// If we have too many items to remove
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 ")",
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
item->next_execution_, now_64);
#endif
#endif /* ESPHOME_DEBUG_SCHEDULER */
// Warning: During callback(), a lot of stuff can happen, including:
// - 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;
// 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)
if (type == SchedulerItem::TIMEOUT) {
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
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) {
// THREAD SAFETY NOTE:
// This function can be called from multiple threads simultaneously on ESP32/LibreTiny.
// On single-threaded platforms (ESP8266, RP2040), atomics are not needed.
// This function has three implementations, based on the precompiler flags
// - 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
// handles out-of-order timestamps between threads, but minimizing time differences
// 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
// LibreTiny: Multi-threaded but lacks atomic operation support
// TODO: If LibreTiny ever adds atomic support, remove this entire block and
// let it fall through to the atomic-based implementation below
// We need to use a lock when near the rollover boundary to prevent races
#ifdef ESPHOME_CORES_SINGLE
// This is the single core implementation.
//
// Single-core platforms have no concurrency, so this is a simple implementation
// 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_;
// 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) {
// True rollover detected (happens every ~49.7 days)
this->millis_major_++;
major++;
#ifdef ESPHOME_DEBUG_SCHEDULER
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
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
// This minimizes backwards time movement
#elif !defined(USE_ESP8266) && !defined(USE_RP2040)
// Multi-threaded platforms with atomic support (ESP32)
uint32_t last = this->last_millis_.load(std::memory_order_relaxed);
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
// 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) {
// Potential rollover - need lock for atomic rollover detection + update
LockGuard guard{this->lock_};
// Re-read with lock held
last = this->last_millis_.load(std::memory_order_relaxed);
#elif defined(ESPHOME_CORES_MULTI_ATOMICS)
// This is the multi core with atomics implementation.
//
// Uses atomic operations with acquire/release semantics to ensure coherent
// reads of millis_major_ and last_millis_ across cores. Features:
// 1. Epoch-coherency retry loop to handle concurrent updates
// 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) {
// True rollover detected (happens every ~49.7 days)
this->millis_major_++;
// Potential rollover - need lock for atomic rollover detection + update
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
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif
}
// Update last_millis_ while holding lock to prevent races
this->last_millis_.store(now, std::memory_order_relaxed);
} else {
// 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_relaxed)) {
break;
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
}
/*
* Update last_millis_ while holding the lock to prevent races
* Publish the new low-word *after* bumping `millis_major_` (done above)
* so readers never see a mismatched pair.
*/
this->last_millis_.store(now, std::memory_order_release);
} else {
// 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
// Single-threaded platforms (ESP8266, RP2040): No atomics needed
uint32_t last = this->last_millis_;
// 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);
#error \
"No platform threading model defined. One of ESPHOME_CORES_SINGLE, ESPHOME_CORES_MULTI_NO_ATOMICS, or ESPHOME_CORES_MULTI_ATOMICS must be defined."
#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,

View File

@ -1,10 +1,11 @@
#pragma once
#include "esphome/core/defines.h"
#include <vector>
#include <memory>
#include <cstring>
#include <deque>
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
#ifdef ESPHOME_CORES_MULTI_ATOMICS
#include <atomic>
#endif
@ -204,23 +205,40 @@ class Scheduler {
Mutex lock_;
std::vector<std::unique_ptr<SchedulerItem>> items_;
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
// ESP8266 and RP2040 don't need the defer queue because:
// 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
#ifndef ESPHOME_CORES_SINGLE
// Single-core platforms don't need the defer queue and save 40 bytes of RAM
std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
#endif
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
// Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
#endif /* ESPHOME_CORES_SINGLE */
uint32_t to_remove_{0};
#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};
#else
#else /* not ESPHOME_CORES_MULTI_ATOMICS */
// Platforms without atomic support or single-threaded platforms
uint32_t last_millis_{0};
#endif
// millis_major_ is protected by lock when incrementing
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
/*
* 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};
uint32_t to_remove_{0};
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
};
} // namespace esphome

View File

@ -971,11 +971,11 @@ class RepeatedTypeInfo(TypeInfo):
def build_type_usage_map(
file_desc: descriptor.FileDescriptorProto,
) -> tuple[dict[str, str | None], dict[str, str | None], dict[str, int]]:
) -> tuple[dict[str, str | None], dict[str, str | None], dict[str, int], set[str]]:
"""Build mappings for both enums and messages to their ifdefs based on usage.
Returns:
tuple: (enum_ifdef_map, message_ifdef_map, message_source_map)
tuple: (enum_ifdef_map, message_ifdef_map, message_source_map, used_messages)
"""
enum_ifdef_map: dict[str, str | None] = {}
message_ifdef_map: dict[str, str | None] = {}
@ -988,6 +988,7 @@ def build_type_usage_map(
message_usage: dict[
str, set[str]
] = {} # message_name -> set of message names that use it
used_messages: set[str] = set() # Track which messages are actually used
# Build message name to ifdef mapping for quick lookup
message_to_ifdef: dict[str, str | None] = {
@ -996,17 +997,26 @@ def build_type_usage_map(
# Analyze field usage
for message in file_desc.message_type:
# Skip deprecated messages entirely
if message.options.deprecated:
continue
for field in message.field:
# Skip deprecated fields when tracking enum usage
if field.options.deprecated:
continue
type_name = field.type_name.split(".")[-1] if field.type_name else None
if not type_name:
continue
# Track enum usage
# Track enum usage (only from non-deprecated fields)
if field.type == 14: # TYPE_ENUM
enum_usage.setdefault(type_name, set()).add(message.name)
# Track message usage
elif field.type == 11: # TYPE_MESSAGE
message_usage.setdefault(type_name, set()).add(message.name)
used_messages.add(type_name)
# Helper to get unique ifdef from a set of messages
def get_unique_ifdef(message_names: set[str]) -> str | None:
@ -1069,12 +1079,18 @@ def build_type_usage_map(
# Build message source map
# First pass: Get explicit sources for messages with source option or id
for msg in file_desc.message_type:
# Skip deprecated messages
if msg.options.deprecated:
continue
if msg.options.HasExtension(pb.source):
# Explicit source option takes precedence
message_source_map[msg.name] = get_opt(msg, pb.source, SOURCE_BOTH)
elif msg.options.HasExtension(pb.id):
# Service messages (with id) default to SOURCE_BOTH
message_source_map[msg.name] = SOURCE_BOTH
# Service messages are always used
used_messages.add(msg.name)
# Second pass: Determine sources for embedded messages based on their usage
for msg in file_desc.message_type:
@ -1103,7 +1119,12 @@ def build_type_usage_map(
# Not used by any message and no explicit source - default to encode-only
message_source_map[msg.name] = SOURCE_SERVER
return enum_ifdef_map, message_ifdef_map, message_source_map
return (
enum_ifdef_map,
message_ifdef_map,
message_source_map,
used_messages,
)
def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]:
@ -1145,6 +1166,10 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
total_size = 0
for field in desc.field:
# Skip deprecated fields
if field.options.deprecated:
continue
ti = create_field_type_info(field)
# Add estimated size for this field
@ -1213,6 +1238,10 @@ def build_message_type(
public_content.append("#endif")
for field in desc.field:
# Skip deprecated fields completely
if field.options.deprecated:
continue
ti = create_field_type_info(field)
# Skip field declarations for fields that are in the base class
@ -1459,8 +1488,10 @@ def find_common_fields(
if not messages:
return []
# Start with fields from the first message
first_msg_fields = {field.name: field for field in messages[0].field}
# Start with fields from the first message (excluding deprecated fields)
first_msg_fields = {
field.name: field for field in messages[0].field if not field.options.deprecated
}
common_fields = []
# Check each field to see if it exists in all messages with same type
@ -1471,6 +1502,9 @@ def find_common_fields(
for msg in messages[1:]:
found = False
for other_field in msg.field:
# Skip deprecated fields
if other_field.options.deprecated:
continue
if (
other_field.name == field_name
and other_field.type == field.type
@ -1599,6 +1633,10 @@ def build_service_message_type(
message_source_map: dict[str, int],
) -> tuple[str, str] | None:
"""Builds the service message type."""
# Skip deprecated messages
if mt.options.deprecated:
return None
snake = camel_to_snake(mt.name)
id_: int | None = get_opt(mt, pb.id)
if id_ is None:
@ -1700,12 +1738,18 @@ namespace api {
content += "namespace enums {\n\n"
# Build dynamic ifdef mappings for both enums and messages
enum_ifdef_map, message_ifdef_map, message_source_map = build_type_usage_map(file)
enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = (
build_type_usage_map(file)
)
# Simple grouping of enums by ifdef
current_ifdef = None
for enum in file.enum_type:
# Skip deprecated enums
if enum.options.deprecated:
continue
s, c, dc = build_enum_type(enum, enum_ifdef_map)
enum_ifdef = enum_ifdef_map.get(enum.name)
@ -1756,6 +1800,14 @@ namespace api {
current_ifdef = None
for m in mt:
# Skip deprecated messages
if m.options.deprecated:
continue
# Skip messages that aren't used (unless they have an ID/service message)
if m.name not in used_messages and not m.options.HasExtension(pb.id):
continue
s, c, dc = build_message_type(m, base_class_fields, message_source_map)
msg_ifdef = message_ifdef_map.get(m.name)