mirror of
https://github.com/esphome/esphome.git
synced 2025-08-01 07:57:47 +00:00
Merge remote-tracking branch 'upstream/dev' into frame_helper_dupe_name_storage
This commit is contained in:
commit
72fcb29fd2
95
.github/workflows/codeowner-review-request.yml
vendored
95
.github/workflows/codeowner-review-request.yml
vendored
@ -178,6 +178,51 @@ jobs:
|
|||||||
reviewedUsers.add(review.user.login);
|
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)
|
// Remove only users who have already submitted reviews (not just requested reviewers)
|
||||||
reviewedUsers.forEach(reviewer => {
|
reviewedUsers.forEach(reviewer => {
|
||||||
matchedOwners.delete(reviewer);
|
matchedOwners.delete(reviewer);
|
||||||
@ -192,7 +237,7 @@ jobs:
|
|||||||
const teamsList = Array.from(matchedTeams);
|
const teamsList = Array.from(matchedTeams);
|
||||||
|
|
||||||
if (reviewersList.length === 0 && teamsList.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,31 +272,41 @@ jobs:
|
|||||||
console.log('All codeowners are already requested reviewers or have reviewed');
|
console.log('All codeowners are already requested reviewers or have reviewed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a comment to the PR mentioning what happened (include all matched codeowners)
|
// Only add a comment if there are new codeowners to mention (not previously pinged)
|
||||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
|
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||||
|
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: pr_number,
|
issue_number: pr_number,
|
||||||
body: commentBody
|
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) {
|
} catch (error) {
|
||||||
if (error.status === 422) {
|
if (error.status === 422) {
|
||||||
console.log('Some reviewers may already be requested or unavailable:', error.message);
|
console.log('Some reviewers may already be requested or unavailable:', error.message);
|
||||||
|
|
||||||
// Try to add a comment even if review request failed
|
// Only try to add a comment if there are new codeowners to mention
|
||||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
|
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||||
|
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: pr_number,
|
issue_number: pr_number,
|
||||||
body: commentBody
|
body: commentBody
|
||||||
});
|
});
|
||||||
} catch (commentError) {
|
console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
|
||||||
console.log('Failed to add comment:', commentError.message);
|
} catch (commentError) {
|
||||||
|
console.log('Failed to add comment:', commentError.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No new codeowners to mention in fallback comment');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
|
45
.github/workflows/issue-codeowner-notify.yml
vendored
45
.github/workflows/issue-codeowner-notify.yml
vendored
@ -92,10 +92,49 @@ jobs:
|
|||||||
mention !== `@${issueAuthor}`
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +150,7 @@ jobs:
|
|||||||
body: commentBody
|
body: commentBody
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Successfully notified codeowners: ${mentionString}`);
|
console.log(`Successfully notified new codeowners: ${mentionString}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to process codeowner notifications:', error.message);
|
console.log('Failed to process codeowner notifications:', error.message);
|
||||||
|
@ -230,14 +230,16 @@ message DeviceInfoResponse {
|
|||||||
|
|
||||||
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
|
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"];
|
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||||
|
|
||||||
string manufacturer = 12;
|
string manufacturer = 12;
|
||||||
|
|
||||||
string friendly_name = 13;
|
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"];
|
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||||
|
|
||||||
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
|
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
|
||||||
@ -337,7 +339,9 @@ message ListEntitiesCoverResponse {
|
|||||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated in API version 1.1
|
||||||
enum LegacyCoverState {
|
enum LegacyCoverState {
|
||||||
|
option deprecated = true;
|
||||||
LEGACY_COVER_STATE_OPEN = 0;
|
LEGACY_COVER_STATE_OPEN = 0;
|
||||||
LEGACY_COVER_STATE_CLOSED = 1;
|
LEGACY_COVER_STATE_CLOSED = 1;
|
||||||
}
|
}
|
||||||
@ -356,7 +360,8 @@ message CoverStateResponse {
|
|||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
// legacy: state has been removed in 1.13
|
// legacy: state has been removed in 1.13
|
||||||
// clients/servers must still send/accept it until the next protocol change
|
// 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 position = 3;
|
||||||
float tilt = 4;
|
float tilt = 4;
|
||||||
@ -364,7 +369,9 @@ message CoverStateResponse {
|
|||||||
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated in API version 1.1
|
||||||
enum LegacyCoverCommand {
|
enum LegacyCoverCommand {
|
||||||
|
option deprecated = true;
|
||||||
LEGACY_COVER_COMMAND_OPEN = 0;
|
LEGACY_COVER_COMMAND_OPEN = 0;
|
||||||
LEGACY_COVER_COMMAND_CLOSE = 1;
|
LEGACY_COVER_COMMAND_CLOSE = 1;
|
||||||
LEGACY_COVER_COMMAND_STOP = 2;
|
LEGACY_COVER_COMMAND_STOP = 2;
|
||||||
@ -380,8 +387,10 @@ message CoverCommandRequest {
|
|||||||
|
|
||||||
// legacy: command has been removed in 1.13
|
// legacy: command has been removed in 1.13
|
||||||
// clients/servers must still send/accept it until the next protocol change
|
// clients/servers must still send/accept it until the next protocol change
|
||||||
bool has_legacy_command = 2;
|
// Deprecated in API version 1.1
|
||||||
LegacyCoverCommand legacy_command = 3;
|
bool has_legacy_command = 2 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.1
|
||||||
|
LegacyCoverCommand legacy_command = 3 [deprecated=true];
|
||||||
|
|
||||||
bool has_position = 4;
|
bool has_position = 4;
|
||||||
float position = 5;
|
float position = 5;
|
||||||
@ -413,7 +422,9 @@ message ListEntitiesFanResponse {
|
|||||||
repeated string supported_preset_modes = 12;
|
repeated string supported_preset_modes = 12;
|
||||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||||
}
|
}
|
||||||
|
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||||
enum FanSpeed {
|
enum FanSpeed {
|
||||||
|
option deprecated = true;
|
||||||
FAN_SPEED_LOW = 0;
|
FAN_SPEED_LOW = 0;
|
||||||
FAN_SPEED_MEDIUM = 1;
|
FAN_SPEED_MEDIUM = 1;
|
||||||
FAN_SPEED_HIGH = 2;
|
FAN_SPEED_HIGH = 2;
|
||||||
@ -432,7 +443,8 @@ message FanStateResponse {
|
|||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool state = 2;
|
bool state = 2;
|
||||||
bool oscillating = 3;
|
bool oscillating = 3;
|
||||||
FanSpeed speed = 4 [deprecated = true];
|
// Deprecated in API version 1.6
|
||||||
|
FanSpeed speed = 4 [deprecated=true];
|
||||||
FanDirection direction = 5;
|
FanDirection direction = 5;
|
||||||
int32 speed_level = 6;
|
int32 speed_level = 6;
|
||||||
string preset_mode = 7;
|
string preset_mode = 7;
|
||||||
@ -448,8 +460,10 @@ message FanCommandRequest {
|
|||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_state = 2;
|
bool has_state = 2;
|
||||||
bool state = 3;
|
bool state = 3;
|
||||||
bool has_speed = 4 [deprecated = true];
|
// Deprecated in API version 1.6
|
||||||
FanSpeed speed = 5 [deprecated = true];
|
bool has_speed = 4 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
|
FanSpeed speed = 5 [deprecated=true];
|
||||||
bool has_oscillating = 6;
|
bool has_oscillating = 6;
|
||||||
bool oscillating = 7;
|
bool oscillating = 7;
|
||||||
bool has_direction = 8;
|
bool has_direction = 8;
|
||||||
@ -488,9 +502,13 @@ message ListEntitiesLightResponse {
|
|||||||
|
|
||||||
repeated ColorMode supported_color_modes = 12;
|
repeated ColorMode supported_color_modes = 12;
|
||||||
// next four supports_* are for legacy clients, newer clients should use color modes
|
// 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];
|
bool legacy_supports_brightness = 5 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_rgb = 6 [deprecated=true];
|
bool legacy_supports_rgb = 6 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_white_value = 7 [deprecated=true];
|
bool legacy_supports_white_value = 7 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
||||||
float min_mireds = 9;
|
float min_mireds = 9;
|
||||||
float max_mireds = 10;
|
float max_mireds = 10;
|
||||||
@ -567,7 +585,9 @@ enum SensorStateClass {
|
|||||||
STATE_CLASS_TOTAL = 3;
|
STATE_CLASS_TOTAL = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated in API version 1.5
|
||||||
enum SensorLastResetType {
|
enum SensorLastResetType {
|
||||||
|
option deprecated = true;
|
||||||
LAST_RESET_NONE = 0;
|
LAST_RESET_NONE = 0;
|
||||||
LAST_RESET_NEVER = 1;
|
LAST_RESET_NEVER = 1;
|
||||||
LAST_RESET_AUTO = 2;
|
LAST_RESET_AUTO = 2;
|
||||||
@ -591,7 +611,8 @@ message ListEntitiesSensorResponse {
|
|||||||
string device_class = 9;
|
string device_class = 9;
|
||||||
SensorStateClass state_class = 10;
|
SensorStateClass state_class = 10;
|
||||||
// Last reset type removed in 2021.9.0
|
// 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;
|
bool disabled_by_default = 12;
|
||||||
EntityCategory entity_category = 13;
|
EntityCategory entity_category = 13;
|
||||||
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
||||||
@ -947,7 +968,8 @@ message ListEntitiesClimateResponse {
|
|||||||
float visual_target_temperature_step = 10;
|
float visual_target_temperature_step = 10;
|
||||||
// for older peer versions - in new system this
|
// for older peer versions - in new system this
|
||||||
// is if CLIMATE_PRESET_AWAY exists is supported_presets
|
// 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;
|
bool supports_action = 12;
|
||||||
repeated ClimateFanMode supported_fan_modes = 13;
|
repeated ClimateFanMode supported_fan_modes = 13;
|
||||||
repeated ClimateSwingMode supported_swing_modes = 14;
|
repeated ClimateSwingMode supported_swing_modes = 14;
|
||||||
@ -978,7 +1000,8 @@ message ClimateStateResponse {
|
|||||||
float target_temperature_low = 5;
|
float target_temperature_low = 5;
|
||||||
float target_temperature_high = 6;
|
float target_temperature_high = 6;
|
||||||
// For older peers, equal to preset == CLIMATE_PRESET_AWAY
|
// 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;
|
ClimateAction action = 8;
|
||||||
ClimateFanMode fan_mode = 9;
|
ClimateFanMode fan_mode = 9;
|
||||||
ClimateSwingMode swing_mode = 10;
|
ClimateSwingMode swing_mode = 10;
|
||||||
@ -1006,8 +1029,10 @@ message ClimateCommandRequest {
|
|||||||
bool has_target_temperature_high = 8;
|
bool has_target_temperature_high = 8;
|
||||||
float target_temperature_high = 9;
|
float target_temperature_high = 9;
|
||||||
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
|
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
|
||||||
bool unused_has_legacy_away = 10;
|
// Deprecated in API version 1.5
|
||||||
bool unused_legacy_away = 11;
|
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;
|
bool has_fan_mode = 12;
|
||||||
ClimateFanMode fan_mode = 13;
|
ClimateFanMode fan_mode = 13;
|
||||||
bool has_swing_mode = 14;
|
bool has_swing_mode = 14;
|
||||||
@ -1354,12 +1379,17 @@ message SubscribeBluetoothLEAdvertisementsRequest {
|
|||||||
uint32 flags = 1;
|
uint32 flags = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated - only used by deprecated BluetoothLEAdvertisementResponse
|
||||||
message BluetoothServiceData {
|
message BluetoothServiceData {
|
||||||
|
option deprecated = true;
|
||||||
string uuid = 1;
|
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
|
bytes data = 3; // Added in api version 1.7
|
||||||
}
|
}
|
||||||
|
// Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead
|
||||||
message BluetoothLEAdvertisementResponse {
|
message BluetoothLEAdvertisementResponse {
|
||||||
|
option deprecated = true;
|
||||||
option (id) = 67;
|
option (id) = 67;
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||||
|
@ -363,8 +363,6 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
|
|||||||
auto *cover = static_cast<cover::Cover *>(entity);
|
auto *cover = static_cast<cover::Cover *>(entity);
|
||||||
CoverStateResponse msg;
|
CoverStateResponse msg;
|
||||||
auto traits = cover->get_traits();
|
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;
|
msg.position = cover->position;
|
||||||
if (traits.get_supports_tilt())
|
if (traits.get_supports_tilt())
|
||||||
msg.tilt = cover->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) {
|
void APIConnection::cover_command(const CoverCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
|
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)
|
if (msg.has_position)
|
||||||
call.set_position(msg.position);
|
call.set_position(msg.position);
|
||||||
if (msg.has_tilt)
|
if (msg.has_tilt)
|
||||||
@ -496,14 +481,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
|||||||
auto traits = light->get_traits();
|
auto traits = light->get_traits();
|
||||||
for (auto mode : traits.get_supported_color_modes())
|
for (auto mode : traits.get_supported_color_modes())
|
||||||
msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
|
msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
|
||||||
msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS);
|
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||||
msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB);
|
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||||
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) {
|
|
||||||
msg.min_mireds = traits.get_min_mireds();
|
msg.min_mireds = traits.get_min_mireds();
|
||||||
msg.max_mireds = traits.get_max_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_current_temperature_step = traits.get_visual_current_temperature_step();
|
||||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||||
msg.visual_max_humidity = traits.get_visual_max_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();
|
msg.supports_action = traits.get_supports_action();
|
||||||
for (auto fan_mode : traits.get_supported_fan_modes())
|
for (auto fan_mode : traits.get_supported_fan_modes())
|
||||||
msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
|
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) {
|
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
|
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) {
|
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(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;
|
resp.webserver_port = USE_WEBSERVER_PORT;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#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_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();
|
resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#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();
|
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
@ -1671,6 +1632,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::process_batch_() {
|
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()) {
|
if (this->deferred_batch_.empty()) {
|
||||||
this->flags_.batch_scheduled = false;
|
this->flags_.batch_scheduled = false;
|
||||||
return;
|
return;
|
||||||
@ -1708,9 +1673,12 @@ void APIConnection::process_batch_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-allocate storage for packet info
|
size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH);
|
||||||
std::vector<PacketInfo> packet_info;
|
|
||||||
packet_info.reserve(num_items);
|
// 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
|
// Cache these values to avoid repeated virtual calls
|
||||||
const uint8_t header_padding = this->helper_->frame_header_padding();
|
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
|
// The actual message data follows after the header padding
|
||||||
uint32_t current_offset = 0;
|
uint32_t current_offset = 0;
|
||||||
|
|
||||||
// Process items and encode directly to buffer
|
// Process items and encode directly to buffer (up to our limit)
|
||||||
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
for (size_t i = 0; i < packets_to_process; i++) {
|
||||||
const auto &item = this->deferred_batch_[i];
|
const auto &item = this->deferred_batch_[i];
|
||||||
// Try to encode message
|
// Try to encode message
|
||||||
// The creator will calculate overhead to determine if the message fits
|
// The creator will calculate overhead to determine if the message fits
|
||||||
@ -1757,7 +1725,11 @@ void APIConnection::process_batch_() {
|
|||||||
// Message was encoded successfully
|
// Message was encoded successfully
|
||||||
// payload_size is header_padding + actual payload size + footer_size
|
// payload_size is header_padding + actual payload size + footer_size
|
||||||
uint16_t proto_payload_size = payload_size - header_padding - 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
|
// Update tracking variables
|
||||||
items_processed++;
|
items_processed++;
|
||||||
@ -1783,8 +1755,8 @@ void APIConnection::process_batch_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send all collected packets
|
// Send all collected packets
|
||||||
APIError err =
|
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()},
|
||||||
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info);
|
std::span<const PacketInfo>(packet_info, packet_count));
|
||||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||||
on_fatal_error();
|
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),
|
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||||
|
@ -33,7 +33,17 @@ struct ClientInfo {
|
|||||||
// Keepalive timeout in milliseconds
|
// Keepalive timeout in milliseconds
|
||||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
||||||
// Maximum number of entities to process in a single batch during initial state/info sending
|
// 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 {
|
class APIConnection : public APIServerConnection {
|
||||||
public:
|
public:
|
||||||
@ -130,7 +140,6 @@ class APIConnection : public APIServerConnection {
|
|||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &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_device_request(const BluetoothDeviceRequest &msg) override;
|
||||||
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
||||||
|
@ -79,33 +79,56 @@ APIError APIFrameHelper::loop() {
|
|||||||
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
|
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
|
// 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;
|
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++) {
|
for (int i = 0; i < iovcnt; i++) {
|
||||||
const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base);
|
if (to_skip >= iov[i].iov_len) {
|
||||||
buffer.data.insert(buffer.data.end(), data, data + 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->tx_buf_.push_back(std::move(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method writes data to socket or buffers it
|
// 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::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
|
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
|
||||||
|
|
||||||
if (iovcnt == 0)
|
if (iovcnt == 0)
|
||||||
return APIError::OK; // Nothing to do, success
|
return APIError::OK; // Nothing to do, success
|
||||||
|
|
||||||
uint16_t total_write_len = 0;
|
|
||||||
for (int i = 0; i < iovcnt; i++) {
|
|
||||||
#ifdef HELPER_LOG_PACKETS
|
#ifdef HELPER_LOG_PACKETS
|
||||||
|
for (int i = 0; i < iovcnt; i++) {
|
||||||
ESP_LOGVV(TAG, "Sending raw: %s",
|
ESP_LOGVV(TAG, "Sending raw: %s",
|
||||||
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
|
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
|
// Try to send any existing buffered data first if there is any
|
||||||
if (!this->tx_buf_.empty()) {
|
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
|
// If there is still data in the buffer, we can't send, buffer
|
||||||
// the new data and return
|
// the new data and return
|
||||||
if (!this->tx_buf_.empty()) {
|
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
|
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);
|
ssize_t sent = this->socket_->writev(iov, iovcnt);
|
||||||
|
|
||||||
if (sent == -1) {
|
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
|
// 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
|
return APIError::OK; // Success, data buffered
|
||||||
}
|
}
|
||||||
// Socket error
|
return err; // Socket write failed
|
||||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
|
||||||
this->state_ = State::FAILED;
|
|
||||||
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
|
|
||||||
} else if (static_cast<uint16_t>(sent) < total_write_len) {
|
} else if (static_cast<uint16_t>(sent) < total_write_len) {
|
||||||
// Partially sent, buffer the remaining data
|
// Partially sent, buffer the remaining data
|
||||||
SendBuffer buffer;
|
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return APIError::OK; // Success, all data sent or buffered
|
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());
|
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
|
||||||
|
|
||||||
if (sent == -1) {
|
if (sent == -1) {
|
||||||
if (errno != EWOULDBLOCK && errno != EAGAIN) {
|
return this->handle_socket_write_error_();
|
||||||
// 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;
|
|
||||||
} else if (sent == 0) {
|
} else if (sent == 0) {
|
||||||
// Nothing sent but not an error
|
// Nothing sent but not an error
|
||||||
return APIError::WOULD_BLOCK;
|
return APIError::WOULD_BLOCK;
|
||||||
@ -299,6 +294,26 @@ APIError APINoiseFrameHelper::init() {
|
|||||||
state_ = State::CLIENT_HELLO;
|
state_ = State::CLIENT_HELLO;
|
||||||
return APIError::OK;
|
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)
|
/// Run through handshake messages (if in that phase)
|
||||||
APIError APINoiseFrameHelper::loop() {
|
APIError APINoiseFrameHelper::loop() {
|
||||||
// During handshake phase, process as many actions as possible until we can't progress
|
// 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
|
// WOULD_BLOCK when no more data is available to read
|
||||||
while (state_ != State::DATA && this->socket_->ready()) {
|
while (state_ != State::DATA && this->socket_->ready()) {
|
||||||
APIError err = state_action_();
|
APIError err = state_action_();
|
||||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
if (err == APIError::WOULD_BLOCK) {
|
if (err == APIError::WOULD_BLOCK) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (err != APIError::OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use base class implementation for buffer sending
|
// 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_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||||
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
|
* 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) {
|
if (frame == nullptr) {
|
||||||
HELPER_LOG("Bad argument for try_read_frame_");
|
HELPER_LOG("Bad argument for try_read_frame_");
|
||||||
return APIError::BAD_ARG;
|
return APIError::BAD_ARG;
|
||||||
@ -395,7 +410,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
|||||||
#ifdef HELPER_LOG_PACKETS
|
#ifdef HELPER_LOG_PACKETS
|
||||||
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str());
|
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str());
|
||||||
#endif
|
#endif
|
||||||
frame->msg = std::move(rx_buf_);
|
*frame = std::move(rx_buf_);
|
||||||
// consume msg
|
// consume msg
|
||||||
rx_buf_ = {};
|
rx_buf_ = {};
|
||||||
rx_buf_len_ = 0;
|
rx_buf_len_ = 0;
|
||||||
@ -421,24 +436,17 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
}
|
}
|
||||||
if (state_ == State::CLIENT_HELLO) {
|
if (state_ == State::CLIENT_HELLO) {
|
||||||
// waiting for client hello
|
// waiting for client hello
|
||||||
ParsedFrame frame;
|
std::vector<uint8_t> frame;
|
||||||
aerr = try_read_frame_(&frame);
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr == APIError::BAD_INDICATOR) {
|
if (aerr != APIError::OK) {
|
||||||
send_explicit_handshake_reject_("Bad indicator byte");
|
return handle_handshake_frame_error_(aerr);
|
||||||
return 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
|
// ignore contents, may be used in future for flags
|
||||||
// Reserve space for: existing prologue + 2 size bytes + frame data
|
// Reserve space for: existing prologue + 2 size bytes + frame data
|
||||||
prologue_.reserve(prologue_.size() + 2 + frame.msg.size());
|
prologue_.reserve(prologue_.size() + 2 + frame.size());
|
||||||
prologue_.push_back((uint8_t) (frame.msg.size() >> 8));
|
prologue_.push_back((uint8_t) (frame.size() >> 8));
|
||||||
prologue_.push_back((uint8_t) frame.msg.size());
|
prologue_.push_back((uint8_t) frame.size());
|
||||||
prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
|
prologue_.insert(prologue_.end(), frame.begin(), frame.end());
|
||||||
|
|
||||||
state_ = State::SERVER_HELLO;
|
state_ = State::SERVER_HELLO;
|
||||||
}
|
}
|
||||||
@ -476,41 +484,29 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
int action = noise_handshakestate_get_action(handshake_);
|
int action = noise_handshakestate_get_action(handshake_);
|
||||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||||
// waiting for handshake msg
|
// waiting for handshake msg
|
||||||
ParsedFrame frame;
|
std::vector<uint8_t> frame;
|
||||||
aerr = try_read_frame_(&frame);
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr == APIError::BAD_INDICATOR) {
|
if (aerr != APIError::OK) {
|
||||||
send_explicit_handshake_reject_("Bad indicator byte");
|
return handle_handshake_frame_error_(aerr);
|
||||||
return 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");
|
send_explicit_handshake_reject_("Empty handshake message");
|
||||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||||
} else if (frame.msg[0] != 0x00) {
|
} else if (frame[0] != 0x00) {
|
||||||
HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]);
|
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
|
||||||
send_explicit_handshake_reject_("Bad handshake error byte");
|
send_explicit_handshake_reject_("Bad handshake error byte");
|
||||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
NoiseBuffer mbuf;
|
NoiseBuffer mbuf;
|
||||||
noise_buffer_init(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);
|
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
state_ = State::FAILED;
|
// Special handling for MAC failure
|
||||||
HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str());
|
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error");
|
||||||
if (err == NOISE_ERROR_MAC_FAILURE) {
|
return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED);
|
||||||
send_explicit_handshake_reject_("Handshake MAC failure");
|
|
||||||
} else {
|
|
||||||
send_explicit_handshake_reject_("Handshake error");
|
|
||||||
}
|
|
||||||
return APIError::HANDSHAKESTATE_READ_FAILED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aerr = check_handshake_finished_();
|
aerr = check_handshake_finished_();
|
||||||
@ -523,11 +519,10 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||||
|
|
||||||
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
|
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
|
||||||
if (err != 0) {
|
APIError aerr_write =
|
||||||
state_ = State::FAILED;
|
handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED);
|
||||||
HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str());
|
if (aerr_write != APIError::OK)
|
||||||
return APIError::HANDSHAKESTATE_WRITE_FAILED;
|
return aerr_write;
|
||||||
}
|
|
||||||
buffer[0] = 0x00; // success
|
buffer[0] = 0x00; // success
|
||||||
|
|
||||||
aerr = write_frame_(buffer, mbuf.size + 1);
|
aerr = write_frame_(buffer, mbuf.size + 1);
|
||||||
@ -576,23 +571,21 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
return APIError::WOULD_BLOCK;
|
return APIError::WOULD_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedFrame frame;
|
std::vector<uint8_t> frame;
|
||||||
aerr = try_read_frame_(&frame);
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|
||||||
NoiseBuffer mbuf;
|
NoiseBuffer mbuf;
|
||||||
noise_buffer_init(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);
|
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
|
||||||
if (err != 0) {
|
APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED);
|
||||||
state_ = State::FAILED;
|
if (decrypt_err != APIError::OK)
|
||||||
HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
|
return decrypt_err;
|
||||||
return APIError::CIPHERSTATE_DECRYPT_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t msg_size = mbuf.size;
|
uint16_t msg_size = mbuf.size;
|
||||||
uint8_t *msg_data = frame.msg.data();
|
uint8_t *msg_data = frame.data();
|
||||||
if (msg_size < 4) {
|
if (msg_size < 4) {
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad data packet: size %d too short", msg_size);
|
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;
|
return APIError::BAD_DATA_PACKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer->container = std::move(frame.msg);
|
buffer->container = std::move(frame);
|
||||||
buffer->data_offset = 4;
|
buffer->data_offset = 4;
|
||||||
buffer->data_len = data_len;
|
buffer->data_len = data_len;
|
||||||
buffer->type = type;
|
buffer->type = type;
|
||||||
@ -640,6 +633,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
|||||||
|
|
||||||
this->reusable_iovs_.clear();
|
this->reusable_iovs_.clear();
|
||||||
this->reusable_iovs_.reserve(packets.size());
|
this->reusable_iovs_.reserve(packets.size());
|
||||||
|
uint16_t total_write_len = 0;
|
||||||
|
|
||||||
// We need to encrypt each packet in place
|
// We need to encrypt each packet in place
|
||||||
for (const auto &packet : packets) {
|
for (const auto &packet : packets) {
|
||||||
@ -668,23 +662,22 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
|||||||
4 + packet.payload_size + frame_footer_size_);
|
4 + packet.payload_size + frame_footer_size_);
|
||||||
|
|
||||||
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
|
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
|
||||||
if (err != 0) {
|
APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED);
|
||||||
state_ = State::FAILED;
|
if (aerr != APIError::OK)
|
||||||
HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str());
|
return aerr;
|
||||||
return APIError::CIPHERSTATE_ENCRYPT_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill in the encrypted size
|
// Fill in the encrypted size
|
||||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||||
|
|
||||||
// Add iovec for this encrypted packet
|
// Add iovec for this encrypted packet
|
||||||
this->reusable_iovs_.push_back(
|
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||||
{buf_start, 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
|
// 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) {
|
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_base = header;
|
||||||
iov[0].iov_len = 3;
|
iov[0].iov_len = 3;
|
||||||
if (len == 0) {
|
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_base = const_cast<uint8_t *>(data);
|
||||||
iov[1].iov_len = len;
|
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.
|
/** Initiate the data structures for the handshake.
|
||||||
@ -723,35 +716,27 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
|||||||
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
|
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
|
||||||
|
|
||||||
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
|
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
|
||||||
if (err != 0) {
|
APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||||
state_ = State::FAILED;
|
if (aerr != APIError::OK)
|
||||||
HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str());
|
return aerr;
|
||||||
return APIError::HANDSHAKESTATE_SETUP_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &psk = ctx_->get_psk();
|
const auto &psk = ctx_->get_psk();
|
||||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||||
if (err != 0) {
|
aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||||
state_ = State::FAILED;
|
if (aerr != APIError::OK)
|
||||||
HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str());
|
return aerr;
|
||||||
return APIError::HANDSHAKESTATE_SETUP_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
|
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
|
||||||
if (err != 0) {
|
aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||||
state_ = State::FAILED;
|
if (aerr != APIError::OK)
|
||||||
HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str());
|
return aerr;
|
||||||
return APIError::HANDSHAKESTATE_SETUP_FAILED;
|
|
||||||
}
|
|
||||||
// set_prologue copies it into handshakestate, so we can get rid of it now
|
// set_prologue copies it into handshakestate, so we can get rid of it now
|
||||||
prologue_ = {};
|
prologue_ = {};
|
||||||
|
|
||||||
err = noise_handshakestate_start(handshake_);
|
err = noise_handshakestate_start(handshake_);
|
||||||
if (err != 0) {
|
aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||||
state_ = State::FAILED;
|
if (aerr != APIError::OK)
|
||||||
HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str());
|
return aerr;
|
||||||
return APIError::HANDSHAKESTATE_SETUP_FAILED;
|
|
||||||
}
|
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,11 +752,9 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
|||||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||||
}
|
}
|
||||||
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
|
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
|
||||||
if (err != 0) {
|
APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED);
|
||||||
state_ = State::FAILED;
|
if (aerr != APIError::OK)
|
||||||
HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str());
|
return aerr;
|
||||||
return APIError::HANDSHAKESTATE_SPLIT_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
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.
|
* 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) {
|
if (frame == nullptr) {
|
||||||
HELPER_LOG("Bad argument for try_read_frame_");
|
HELPER_LOG("Bad argument for try_read_frame_");
|
||||||
return APIError::BAD_ARG;
|
return APIError::BAD_ARG;
|
||||||
@ -956,7 +939,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
|||||||
#ifdef HELPER_LOG_PACKETS
|
#ifdef HELPER_LOG_PACKETS
|
||||||
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str());
|
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str());
|
||||||
#endif
|
#endif
|
||||||
frame->msg = std::move(rx_buf_);
|
*frame = std::move(rx_buf_);
|
||||||
// consume msg
|
// consume msg
|
||||||
rx_buf_ = {};
|
rx_buf_ = {};
|
||||||
rx_buf_len_ = 0;
|
rx_buf_len_ = 0;
|
||||||
@ -971,7 +954,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
return APIError::WOULD_BLOCK;
|
return APIError::WOULD_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedFrame frame;
|
std::vector<uint8_t> frame;
|
||||||
aerr = try_read_frame_(&frame);
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
if (aerr == APIError::BAD_INDICATOR) {
|
if (aerr == APIError::BAD_INDICATOR) {
|
||||||
@ -991,12 +974,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
"Bad indicator byte";
|
"Bad indicator byte";
|
||||||
iov[0].iov_base = (void *) msg;
|
iov[0].iov_base = (void *) msg;
|
||||||
iov[0].iov_len = 19;
|
iov[0].iov_len = 19;
|
||||||
this->write_raw_(iov, 1);
|
this->write_raw_(iov, 1, 19);
|
||||||
}
|
}
|
||||||
return aerr;
|
return aerr;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer->container = std::move(frame.msg);
|
buffer->container = std::move(frame);
|
||||||
buffer->data_offset = 0;
|
buffer->data_offset = 0;
|
||||||
buffer->data_len = rx_header_parsed_len_;
|
buffer->data_len = rx_header_parsed_len_;
|
||||||
buffer->type = rx_header_parsed_type_;
|
buffer->type = rx_header_parsed_type_;
|
||||||
@ -1021,6 +1004,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
|||||||
|
|
||||||
this->reusable_iovs_.clear();
|
this->reusable_iovs_.clear();
|
||||||
this->reusable_iovs_.reserve(packets.size());
|
this->reusable_iovs_.reserve(packets.size());
|
||||||
|
uint16_t total_write_len = 0;
|
||||||
|
|
||||||
for (const auto &packet : packets) {
|
for (const auto &packet : packets) {
|
||||||
// Calculate varint sizes for header layout
|
// 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);
|
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||||
|
|
||||||
// Add iovec for this packet (header + payload)
|
// Add iovec for this packet (header + payload)
|
||||||
this->reusable_iovs_.push_back(
|
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
|
||||||
{buf_start + header_offset, 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
|
// 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
|
#endif // USE_API_PLAINTEXT
|
||||||
|
@ -111,29 +111,28 @@ class APIFrameHelper {
|
|||||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Struct for holding parsed frame data
|
|
||||||
struct ParsedFrame {
|
|
||||||
std::vector<uint8_t> msg;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Buffer containing data to be sent
|
// Buffer containing data to be sent
|
||||||
struct SendBuffer {
|
struct SendBuffer {
|
||||||
std::vector<uint8_t> data;
|
std::unique_ptr<uint8_t[]> data;
|
||||||
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
|
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
|
// 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; }
|
uint16_t remaining() const { return size - offset; }
|
||||||
const uint8_t *current_data() const { return data.data() + offset; }
|
const uint8_t *current_data() const { return data.get() + offset; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Common implementation for writing raw data to socket
|
// 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
|
// Try to send data from the tx buffer
|
||||||
APIError try_send_tx_buf_();
|
APIError try_send_tx_buf_();
|
||||||
|
|
||||||
// Helper method to buffer data from IOVs
|
// 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>
|
template<typename StateEnum>
|
||||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
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);
|
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||||
@ -210,11 +209,13 @@ class APINoiseFrameHelper : public APIFrameHelper {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIError state_action_();
|
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 write_frame_(const uint8_t *data, uint16_t len);
|
||||||
APIError init_handshake_();
|
APIError init_handshake_();
|
||||||
APIError check_handshake_finished_();
|
APIError check_handshake_finished_();
|
||||||
void send_explicit_handshake_reject_(const std::string &reason);
|
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)
|
// Pointers first (4 bytes each)
|
||||||
NoiseHandshakeState *handshake_{nullptr};
|
NoiseHandshakeState *handshake_{nullptr};
|
||||||
@ -263,7 +264,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
|
|||||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIError try_read_frame_(ParsedFrame *frame);
|
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||||
|
|
||||||
// Group 2-byte aligned types
|
// Group 2-byte aligned types
|
||||||
uint16_t rx_header_parsed_type_ = 0;
|
uint16_t rx_header_parsed_type_ = 0;
|
||||||
|
@ -94,17 +94,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
#ifdef USE_WEBSERVER
|
#ifdef USE_WEBSERVER
|
||||||
buffer.encode_uint32(10, this->webserver_port);
|
buffer.encode_uint32(10, this->webserver_port);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
|
||||||
buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags);
|
buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags);
|
||||||
#endif
|
#endif
|
||||||
buffer.encode_string(12, this->manufacturer);
|
buffer.encode_string(12, this->manufacturer);
|
||||||
buffer.encode_string(13, this->friendly_name);
|
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
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
buffer.encode_uint32(17, this->voice_assistant_feature_flags);
|
buffer.encode_uint32(17, this->voice_assistant_feature_flags);
|
||||||
#endif
|
#endif
|
||||||
@ -150,17 +144,11 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const {
|
|||||||
#ifdef USE_WEBSERVER
|
#ifdef USE_WEBSERVER
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->webserver_port);
|
ProtoSize::add_uint32_field(total_size, 1, this->webserver_port);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags);
|
ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags);
|
||||||
#endif
|
#endif
|
||||||
ProtoSize::add_string_field(total_size, 1, this->manufacturer);
|
ProtoSize::add_string_field(total_size, 1, this->manufacturer);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->friendly_name);
|
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
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags);
|
ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags);
|
||||||
#endif
|
#endif
|
||||||
@ -270,7 +258,6 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const {
|
|||||||
}
|
}
|
||||||
void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
|
void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
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(3, this->position);
|
||||||
buffer.encode_float(4, this->tilt);
|
buffer.encode_float(4, this->tilt);
|
||||||
buffer.encode_uint32(5, static_cast<uint32_t>(this->current_operation));
|
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 {
|
void CoverStateResponse::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed32_field(total_size, 1, this->key);
|
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->position);
|
||||||
ProtoSize::add_float_field(total_size, 1, this->tilt);
|
ProtoSize::add_float_field(total_size, 1, this->tilt);
|
||||||
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->current_operation));
|
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) {
|
bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
switch (field_id) {
|
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:
|
case 4:
|
||||||
this->has_position = value.as_bool();
|
this->has_position = value.as_bool();
|
||||||
break;
|
break;
|
||||||
@ -379,7 +359,6 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_bool(2, this->state);
|
buffer.encode_bool(2, this->state);
|
||||||
buffer.encode_bool(3, this->oscillating);
|
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_uint32(5, static_cast<uint32_t>(this->direction));
|
||||||
buffer.encode_int32(6, this->speed_level);
|
buffer.encode_int32(6, this->speed_level);
|
||||||
buffer.encode_string(7, this->preset_mode);
|
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_fixed32_field(total_size, 1, this->key);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->state);
|
ProtoSize::add_bool_field(total_size, 1, this->state);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->oscillating);
|
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_enum_field(total_size, 1, static_cast<uint32_t>(this->direction));
|
||||||
ProtoSize::add_int32_field(total_size, 1, this->speed_level);
|
ProtoSize::add_int32_field(total_size, 1, this->speed_level);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->preset_mode);
|
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:
|
case 3:
|
||||||
this->state = value.as_bool();
|
this->state = value.as_bool();
|
||||||
break;
|
break;
|
||||||
case 4:
|
|
||||||
this->has_speed = value.as_bool();
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
this->speed = static_cast<enums::FanSpeed>(value.as_uint32());
|
|
||||||
break;
|
|
||||||
case 6:
|
case 6:
|
||||||
this->has_oscillating = value.as_bool();
|
this->has_oscillating = value.as_bool();
|
||||||
break;
|
break;
|
||||||
@ -473,10 +445,6 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
for (auto &it : this->supported_color_modes) {
|
for (auto &it : this->supported_color_modes) {
|
||||||
buffer.encode_uint32(12, static_cast<uint32_t>(it), true);
|
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(9, this->min_mireds);
|
||||||
buffer.encode_float(10, this->max_mireds);
|
buffer.encode_float(10, this->max_mireds);
|
||||||
for (auto &it : this->effects) {
|
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_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->min_mireds);
|
||||||
ProtoSize::add_float_field(total_size, 1, this->max_mireds);
|
ProtoSize::add_float_field(total_size, 1, this->max_mireds);
|
||||||
if (!this->effects.empty()) {
|
if (!this->effects.empty()) {
|
||||||
@ -677,7 +641,6 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_bool(8, this->force_update);
|
buffer.encode_bool(8, this->force_update);
|
||||||
buffer.encode_string(9, this->device_class);
|
buffer.encode_string(9, this->device_class);
|
||||||
buffer.encode_uint32(10, static_cast<uint32_t>(this->state_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_bool(12, this->disabled_by_default);
|
||||||
buffer.encode_uint32(13, static_cast<uint32_t>(this->entity_category));
|
buffer.encode_uint32(13, static_cast<uint32_t>(this->entity_category));
|
||||||
#ifdef USE_DEVICES
|
#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_bool_field(total_size, 1, this->force_update);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->device_class);
|
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->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_bool_field(total_size, 1, this->disabled_by_default);
|
||||||
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category));
|
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category));
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
@ -1105,7 +1067,6 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_float(8, this->visual_min_temperature);
|
buffer.encode_float(8, this->visual_min_temperature);
|
||||||
buffer.encode_float(9, this->visual_max_temperature);
|
buffer.encode_float(9, this->visual_max_temperature);
|
||||||
buffer.encode_float(10, this->visual_target_temperature_step);
|
buffer.encode_float(10, this->visual_target_temperature_step);
|
||||||
buffer.encode_bool(11, this->legacy_supports_away);
|
|
||||||
buffer.encode_bool(12, this->supports_action);
|
buffer.encode_bool(12, this->supports_action);
|
||||||
for (auto &it : this->supported_fan_modes) {
|
for (auto &it : this->supported_fan_modes) {
|
||||||
buffer.encode_uint32(13, static_cast<uint32_t>(it), true);
|
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_min_temperature);
|
||||||
ProtoSize::add_float_field(total_size, 1, this->visual_max_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_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);
|
ProtoSize::add_bool_field(total_size, 1, this->supports_action);
|
||||||
if (!this->supported_fan_modes.empty()) {
|
if (!this->supported_fan_modes.empty()) {
|
||||||
for (const auto &it : this->supported_fan_modes) {
|
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(4, this->target_temperature);
|
||||||
buffer.encode_float(5, this->target_temperature_low);
|
buffer.encode_float(5, this->target_temperature_low);
|
||||||
buffer.encode_float(6, this->target_temperature_high);
|
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(8, static_cast<uint32_t>(this->action));
|
||||||
buffer.encode_uint32(9, static_cast<uint32_t>(this->fan_mode));
|
buffer.encode_uint32(9, static_cast<uint32_t>(this->fan_mode));
|
||||||
buffer.encode_uint32(10, static_cast<uint32_t>(this->swing_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);
|
||||||
ProtoSize::add_float_field(total_size, 1, this->target_temperature_low);
|
ProtoSize::add_float_field(total_size, 1, this->target_temperature_low);
|
||||||
ProtoSize::add_float_field(total_size, 1, this->target_temperature_high);
|
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->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->fan_mode));
|
||||||
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->swing_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:
|
case 8:
|
||||||
this->has_target_temperature_high = value.as_bool();
|
this->has_target_temperature_high = value.as_bool();
|
||||||
break;
|
break;
|
||||||
case 10:
|
|
||||||
this->unused_has_legacy_away = value.as_bool();
|
|
||||||
break;
|
|
||||||
case 11:
|
|
||||||
this->unused_legacy_away = value.as_bool();
|
|
||||||
break;
|
|
||||||
case 12:
|
case 12:
|
||||||
this->has_fan_mode = value.as_bool();
|
this->has_fan_mode = value.as_bool();
|
||||||
break;
|
break;
|
||||||
@ -1869,50 +1821,6 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
|
|||||||
}
|
}
|
||||||
return true;
|
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 {
|
void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_uint64(1, this->address);
|
buffer.encode_uint64(1, this->address);
|
||||||
buffer.encode_sint32(2, this->rssi);
|
buffer.encode_sint32(2, this->rssi);
|
||||||
|
@ -17,27 +17,13 @@ enum EntityCategory : uint32_t {
|
|||||||
ENTITY_CATEGORY_DIAGNOSTIC = 2,
|
ENTITY_CATEGORY_DIAGNOSTIC = 2,
|
||||||
};
|
};
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
enum LegacyCoverState : uint32_t {
|
|
||||||
LEGACY_COVER_STATE_OPEN = 0,
|
|
||||||
LEGACY_COVER_STATE_CLOSED = 1,
|
|
||||||
};
|
|
||||||
enum CoverOperation : uint32_t {
|
enum CoverOperation : uint32_t {
|
||||||
COVER_OPERATION_IDLE = 0,
|
COVER_OPERATION_IDLE = 0,
|
||||||
COVER_OPERATION_IS_OPENING = 1,
|
COVER_OPERATION_IS_OPENING = 1,
|
||||||
COVER_OPERATION_IS_CLOSING = 2,
|
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
|
#endif
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
enum FanSpeed : uint32_t {
|
|
||||||
FAN_SPEED_LOW = 0,
|
|
||||||
FAN_SPEED_MEDIUM = 1,
|
|
||||||
FAN_SPEED_HIGH = 2,
|
|
||||||
};
|
|
||||||
enum FanDirection : uint32_t {
|
enum FanDirection : uint32_t {
|
||||||
FAN_DIRECTION_FORWARD = 0,
|
FAN_DIRECTION_FORWARD = 0,
|
||||||
FAN_DIRECTION_REVERSE = 1,
|
FAN_DIRECTION_REVERSE = 1,
|
||||||
@ -65,11 +51,6 @@ enum SensorStateClass : uint32_t {
|
|||||||
STATE_CLASS_TOTAL_INCREASING = 2,
|
STATE_CLASS_TOTAL_INCREASING = 2,
|
||||||
STATE_CLASS_TOTAL = 3,
|
STATE_CLASS_TOTAL = 3,
|
||||||
};
|
};
|
||||||
enum SensorLastResetType : uint32_t {
|
|
||||||
LAST_RESET_NONE = 0,
|
|
||||||
LAST_RESET_NEVER = 1,
|
|
||||||
LAST_RESET_AUTO = 2,
|
|
||||||
};
|
|
||||||
#endif
|
#endif
|
||||||
enum LogLevel : uint32_t {
|
enum LogLevel : uint32_t {
|
||||||
LOG_LEVEL_NONE = 0,
|
LOG_LEVEL_NONE = 0,
|
||||||
@ -485,7 +466,7 @@ class DeviceInfo : public ProtoMessage {
|
|||||||
class DeviceInfoResponse : public ProtoMessage {
|
class DeviceInfoResponse : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "device_info_response"; }
|
const char *message_name() const override { return "device_info_response"; }
|
||||||
#endif
|
#endif
|
||||||
@ -507,17 +488,11 @@ class DeviceInfoResponse : public ProtoMessage {
|
|||||||
#ifdef USE_WEBSERVER
|
#ifdef USE_WEBSERVER
|
||||||
uint32_t webserver_port{0};
|
uint32_t webserver_port{0};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
|
||||||
uint32_t legacy_bluetooth_proxy_version{0};
|
|
||||||
#endif
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
uint32_t bluetooth_proxy_feature_flags{0};
|
uint32_t bluetooth_proxy_feature_flags{0};
|
||||||
#endif
|
#endif
|
||||||
std::string manufacturer{};
|
std::string manufacturer{};
|
||||||
std::string friendly_name{};
|
std::string friendly_name{};
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
|
||||||
uint32_t legacy_voice_assistant_version{0};
|
|
||||||
#endif
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
uint32_t voice_assistant_feature_flags{0};
|
uint32_t voice_assistant_feature_flags{0};
|
||||||
#endif
|
#endif
|
||||||
@ -646,11 +621,10 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
|
|||||||
class CoverStateResponse : public StateResponseProtoMessage {
|
class CoverStateResponse : public StateResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 22;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "cover_state_response"; }
|
const char *message_name() const override { return "cover_state_response"; }
|
||||||
#endif
|
#endif
|
||||||
enums::LegacyCoverState legacy_state{};
|
|
||||||
float position{0.0f};
|
float position{0.0f};
|
||||||
float tilt{0.0f};
|
float tilt{0.0f};
|
||||||
enums::CoverOperation current_operation{};
|
enums::CoverOperation current_operation{};
|
||||||
@ -665,12 +639,10 @@ class CoverStateResponse : public StateResponseProtoMessage {
|
|||||||
class CoverCommandRequest : public CommandProtoMessage {
|
class CoverCommandRequest : public CommandProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 30;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "cover_command_request"; }
|
const char *message_name() const override { return "cover_command_request"; }
|
||||||
#endif
|
#endif
|
||||||
bool has_legacy_command{false};
|
|
||||||
enums::LegacyCoverCommand legacy_command{};
|
|
||||||
bool has_position{false};
|
bool has_position{false};
|
||||||
float position{0.0f};
|
float position{0.0f};
|
||||||
bool has_tilt{false};
|
bool has_tilt{false};
|
||||||
@ -709,13 +681,12 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
|
|||||||
class FanStateResponse : public StateResponseProtoMessage {
|
class FanStateResponse : public StateResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 23;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "fan_state_response"; }
|
const char *message_name() const override { return "fan_state_response"; }
|
||||||
#endif
|
#endif
|
||||||
bool state{false};
|
bool state{false};
|
||||||
bool oscillating{false};
|
bool oscillating{false};
|
||||||
enums::FanSpeed speed{};
|
|
||||||
enums::FanDirection direction{};
|
enums::FanDirection direction{};
|
||||||
int32_t speed_level{0};
|
int32_t speed_level{0};
|
||||||
std::string preset_mode{};
|
std::string preset_mode{};
|
||||||
@ -730,14 +701,12 @@ class FanStateResponse : public StateResponseProtoMessage {
|
|||||||
class FanCommandRequest : public CommandProtoMessage {
|
class FanCommandRequest : public CommandProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 31;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "fan_command_request"; }
|
const char *message_name() const override { return "fan_command_request"; }
|
||||||
#endif
|
#endif
|
||||||
bool has_state{false};
|
bool has_state{false};
|
||||||
bool state{false};
|
bool state{false};
|
||||||
bool has_speed{false};
|
|
||||||
enums::FanSpeed speed{};
|
|
||||||
bool has_oscillating{false};
|
bool has_oscillating{false};
|
||||||
bool oscillating{false};
|
bool oscillating{false};
|
||||||
bool has_direction{false};
|
bool has_direction{false};
|
||||||
@ -760,15 +729,11 @@ class FanCommandRequest : public CommandProtoMessage {
|
|||||||
class ListEntitiesLightResponse : public InfoResponseProtoMessage {
|
class ListEntitiesLightResponse : public InfoResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 15;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_light_response"; }
|
const char *message_name() const override { return "list_entities_light_response"; }
|
||||||
#endif
|
#endif
|
||||||
std::vector<enums::ColorMode> supported_color_modes{};
|
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 min_mireds{0.0f};
|
||||||
float max_mireds{0.0f};
|
float max_mireds{0.0f};
|
||||||
std::vector<std::string> effects{};
|
std::vector<std::string> effects{};
|
||||||
@ -854,7 +819,7 @@ class LightCommandRequest : public CommandProtoMessage {
|
|||||||
class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
|
class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 16;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_sensor_response"; }
|
const char *message_name() const override { return "list_entities_sensor_response"; }
|
||||||
#endif
|
#endif
|
||||||
@ -863,7 +828,6 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
|
|||||||
bool force_update{false};
|
bool force_update{false};
|
||||||
std::string device_class{};
|
std::string device_class{};
|
||||||
enums::SensorStateClass state_class{};
|
enums::SensorStateClass state_class{};
|
||||||
enums::SensorLastResetType legacy_last_reset_type{};
|
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(uint32_t &total_size) const override;
|
void calculate_size(uint32_t &total_size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@ -1289,7 +1253,7 @@ class CameraImageRequest : public ProtoDecodableMessage {
|
|||||||
class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 46;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_climate_response"; }
|
const char *message_name() const override { return "list_entities_climate_response"; }
|
||||||
#endif
|
#endif
|
||||||
@ -1299,7 +1263,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
|||||||
float visual_min_temperature{0.0f};
|
float visual_min_temperature{0.0f};
|
||||||
float visual_max_temperature{0.0f};
|
float visual_max_temperature{0.0f};
|
||||||
float visual_target_temperature_step{0.0f};
|
float visual_target_temperature_step{0.0f};
|
||||||
bool legacy_supports_away{false};
|
|
||||||
bool supports_action{false};
|
bool supports_action{false};
|
||||||
std::vector<enums::ClimateFanMode> supported_fan_modes{};
|
std::vector<enums::ClimateFanMode> supported_fan_modes{};
|
||||||
std::vector<enums::ClimateSwingMode> supported_swing_modes{};
|
std::vector<enums::ClimateSwingMode> supported_swing_modes{};
|
||||||
@ -1322,7 +1285,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
|||||||
class ClimateStateResponse : public StateResponseProtoMessage {
|
class ClimateStateResponse : public StateResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 47;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "climate_state_response"; }
|
const char *message_name() const override { return "climate_state_response"; }
|
||||||
#endif
|
#endif
|
||||||
@ -1331,7 +1294,6 @@ class ClimateStateResponse : public StateResponseProtoMessage {
|
|||||||
float target_temperature{0.0f};
|
float target_temperature{0.0f};
|
||||||
float target_temperature_low{0.0f};
|
float target_temperature_low{0.0f};
|
||||||
float target_temperature_high{0.0f};
|
float target_temperature_high{0.0f};
|
||||||
bool unused_legacy_away{false};
|
|
||||||
enums::ClimateAction action{};
|
enums::ClimateAction action{};
|
||||||
enums::ClimateFanMode fan_mode{};
|
enums::ClimateFanMode fan_mode{};
|
||||||
enums::ClimateSwingMode swing_mode{};
|
enums::ClimateSwingMode swing_mode{};
|
||||||
@ -1351,7 +1313,7 @@ class ClimateStateResponse : public StateResponseProtoMessage {
|
|||||||
class ClimateCommandRequest : public CommandProtoMessage {
|
class ClimateCommandRequest : public CommandProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 48;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "climate_command_request"; }
|
const char *message_name() const override { return "climate_command_request"; }
|
||||||
#endif
|
#endif
|
||||||
@ -1363,8 +1325,6 @@ class ClimateCommandRequest : public CommandProtoMessage {
|
|||||||
float target_temperature_low{0.0f};
|
float target_temperature_low{0.0f};
|
||||||
bool has_target_temperature_high{false};
|
bool has_target_temperature_high{false};
|
||||||
float target_temperature_high{0.0f};
|
float target_temperature_high{0.0f};
|
||||||
bool unused_has_legacy_away{false};
|
|
||||||
bool unused_legacy_away{false};
|
|
||||||
bool has_fan_mode{false};
|
bool has_fan_mode{false};
|
||||||
enums::ClimateFanMode fan_mode{};
|
enums::ClimateFanMode fan_mode{};
|
||||||
bool has_swing_mode{false};
|
bool has_swing_mode{false};
|
||||||
@ -1736,41 +1696,6 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
|
|||||||
protected:
|
protected:
|
||||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
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 {
|
class BluetoothLERawAdvertisement : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
uint64_t address{0};
|
uint64_t address{0};
|
||||||
|
@ -23,16 +23,6 @@ template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::Entity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ifdef USE_COVER
|
#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) {
|
template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverOperation value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case enums::COVER_OPERATION_IDLE:
|
case enums::COVER_OPERATION_IDLE:
|
||||||
@ -45,32 +35,8 @@ template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverO
|
|||||||
return "UNKNOWN";
|
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
|
#endif
|
||||||
#ifdef USE_FAN
|
#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) {
|
template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirection value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case enums::FAN_DIRECTION_FORWARD:
|
case enums::FAN_DIRECTION_FORWARD:
|
||||||
@ -127,18 +93,6 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
|
|||||||
return "UNKNOWN";
|
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
|
#endif
|
||||||
template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) {
|
template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
@ -737,13 +691,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
|
|||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
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
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
out.append(" bluetooth_proxy_feature_flags: ");
|
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("'").append(this->friendly_name).append("'");
|
||||||
out.append("\n");
|
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
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
out.append(" voice_assistant_feature_flags: ");
|
out.append(" voice_assistant_feature_flags: ");
|
||||||
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->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(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
out.append(" legacy_state: ");
|
|
||||||
out.append(proto_enum_to_string<enums::LegacyCoverState>(this->legacy_state));
|
|
||||||
out.append("\n");
|
|
||||||
|
|
||||||
out.append(" position: ");
|
out.append(" position: ");
|
||||||
snprintf(buffer, sizeof(buffer), "%g", this->position);
|
snprintf(buffer, sizeof(buffer), "%g", this->position);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
@ -996,14 +932,6 @@ void CoverCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
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(" has_position: ");
|
||||||
out.append(YESNO(this->has_position));
|
out.append(YESNO(this->has_position));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
@ -1115,10 +1043,6 @@ void FanStateResponse::dump_to(std::string &out) const {
|
|||||||
out.append(YESNO(this->oscillating));
|
out.append(YESNO(this->oscillating));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
out.append(" speed: ");
|
|
||||||
out.append(proto_enum_to_string<enums::FanSpeed>(this->speed));
|
|
||||||
out.append("\n");
|
|
||||||
|
|
||||||
out.append(" direction: ");
|
out.append(" direction: ");
|
||||||
out.append(proto_enum_to_string<enums::FanDirection>(this->direction));
|
out.append(proto_enum_to_string<enums::FanDirection>(this->direction));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
@ -1157,14 +1081,6 @@ void FanCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(YESNO(this->state));
|
out.append(YESNO(this->state));
|
||||||
out.append("\n");
|
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(" has_oscillating: ");
|
||||||
out.append(YESNO(this->has_oscillating));
|
out.append(YESNO(this->has_oscillating));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
@ -1231,22 +1147,6 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
|
|||||||
out.append("\n");
|
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: ");
|
out.append(" min_mireds: ");
|
||||||
snprintf(buffer, sizeof(buffer), "%g", this->min_mireds);
|
snprintf(buffer, sizeof(buffer), "%g", this->min_mireds);
|
||||||
out.append(buffer);
|
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(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
|
||||||
out.append("\n");
|
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(" disabled_by_default: ");
|
||||||
out.append(YESNO(this->disabled_by_default));
|
out.append(YESNO(this->disabled_by_default));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
@ -2107,10 +2003,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
|
|||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
out.append(" legacy_supports_away: ");
|
|
||||||
out.append(YESNO(this->legacy_supports_away));
|
|
||||||
out.append("\n");
|
|
||||||
|
|
||||||
out.append(" supports_action: ");
|
out.append(" supports_action: ");
|
||||||
out.append(YESNO(this->supports_action));
|
out.append(YESNO(this->supports_action));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
@ -2223,10 +2115,6 @@ void ClimateStateResponse::dump_to(std::string &out) const {
|
|||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
out.append(" unused_legacy_away: ");
|
|
||||||
out.append(YESNO(this->unused_legacy_away));
|
|
||||||
out.append("\n");
|
|
||||||
|
|
||||||
out.append(" action: ");
|
out.append(" action: ");
|
||||||
out.append(proto_enum_to_string<enums::ClimateAction>(this->action));
|
out.append(proto_enum_to_string<enums::ClimateAction>(this->action));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
@ -2313,14 +2201,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
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(" has_fan_mode: ");
|
||||||
out.append(YESNO(this->has_fan_mode));
|
out.append(YESNO(this->has_fan_mode));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
@ -3053,66 +2933,6 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const
|
|||||||
out.append("\n");
|
out.append("\n");
|
||||||
out.append("}");
|
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 {
|
void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
|
||||||
__attribute__((unused)) char buffer[64];
|
__attribute__((unused)) char buffer[64];
|
||||||
out.append("BluetoothLERawAdvertisement {\n");
|
out.append("BluetoothLERawAdvertisement {\n");
|
||||||
|
@ -13,11 +13,180 @@ namespace bluetooth_proxy {
|
|||||||
|
|
||||||
static const char *const TAG = "bluetooth_proxy.connection";
|
static const char *const TAG = "bluetooth_proxy.connection";
|
||||||
|
|
||||||
|
static std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
|
||||||
|
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
|
||||||
|
return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]),
|
||||||
|
((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
|
||||||
|
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
|
||||||
|
}
|
||||||
|
|
||||||
void BluetoothConnection::dump_config() {
|
void BluetoothConnection::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
||||||
BLEClientBase::dump_config();
|
BLEClientBase::dump_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BluetoothConnection::loop() {
|
||||||
|
BLEClientBase::loop();
|
||||||
|
|
||||||
|
// Early return if no active connection or not in service discovery phase
|
||||||
|
if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle service discovery
|
||||||
|
this->send_service_for_discovery_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||||
|
// Send disconnection notification
|
||||||
|
this->proxy_->send_device_connection(this->address_, false, 0, reason);
|
||||||
|
|
||||||
|
// Important: If we were in the middle of sending services, we do NOT send
|
||||||
|
// send_gatt_services_done() here. This ensures the client knows that
|
||||||
|
// the service discovery was interrupted and can retry. The client
|
||||||
|
// (aioesphomeapi) implements a 30-second timeout (DEFAULT_BLE_TIMEOUT)
|
||||||
|
// to detect incomplete service discovery rather than relying on us to
|
||||||
|
// tell them about a partial list.
|
||||||
|
this->set_address(0);
|
||||||
|
this->send_service_ = DONE_SENDING_SERVICES;
|
||||||
|
this->proxy_->send_connections_free();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothConnection::send_service_for_discovery_() {
|
||||||
|
if (this->send_service_ == this->service_count_) {
|
||||||
|
this->send_service_ = DONE_SENDING_SERVICES;
|
||||||
|
this->proxy_->send_gatt_services_done(this->address_);
|
||||||
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||||
|
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||||
|
this->release_services();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early return if no API connection
|
||||||
|
auto *api_conn = this->proxy_->get_api_connection();
|
||||||
|
if (api_conn == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send next service
|
||||||
|
esp_gattc_service_elem_t service_result;
|
||||||
|
uint16_t service_count = 1;
|
||||||
|
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||||
|
&service_result, &service_count, this->send_service_);
|
||||||
|
this->send_service_++;
|
||||||
|
|
||||||
|
if (service_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), this->send_service_ - 1, service_status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service_count == 0) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), service_count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api::BluetoothGATTGetServicesResponse resp;
|
||||||
|
resp.address = this->address_;
|
||||||
|
resp.services.reserve(1); // Always one service per response in this implementation
|
||||||
|
api::BluetoothGATTService service_resp;
|
||||||
|
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
|
||||||
|
service_resp.handle = service_result.start_handle;
|
||||||
|
|
||||||
|
// Get the number of characteristics directly with one call
|
||||||
|
uint16_t total_char_count = 0;
|
||||||
|
esp_gatt_status_t char_count_status =
|
||||||
|
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||||
|
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||||
|
|
||||||
|
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
|
||||||
|
// Only reserve if we successfully got a count
|
||||||
|
service_resp.characteristics.reserve(total_char_count);
|
||||||
|
} else if (char_count_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), char_count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process characteristics
|
||||||
|
uint16_t char_offset = 0;
|
||||||
|
esp_gattc_char_elem_t char_result;
|
||||||
|
while (true) { // characteristics
|
||||||
|
uint16_t char_count = 1;
|
||||||
|
esp_gatt_status_t char_status =
|
||||||
|
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||||
|
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||||
|
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (char_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), char_status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (char_count == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
api::BluetoothGATTCharacteristic characteristic_resp;
|
||||||
|
characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid);
|
||||||
|
characteristic_resp.handle = char_result.char_handle;
|
||||||
|
characteristic_resp.properties = char_result.properties;
|
||||||
|
char_offset++;
|
||||||
|
|
||||||
|
// Get the number of descriptors directly with one call
|
||||||
|
uint16_t total_desc_count = 0;
|
||||||
|
esp_gatt_status_t desc_count_status =
|
||||||
|
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle,
|
||||||
|
service_result.end_handle, 0, &total_desc_count);
|
||||||
|
|
||||||
|
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
|
||||||
|
// Only reserve if we successfully got a count
|
||||||
|
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||||
|
} else if (desc_count_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), char_result.char_handle, desc_count_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process descriptors
|
||||||
|
uint16_t desc_offset = 0;
|
||||||
|
esp_gattc_descr_elem_t desc_result;
|
||||||
|
while (true) { // descriptors
|
||||||
|
uint16_t desc_count = 1;
|
||||||
|
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||||
|
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||||
|
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (desc_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), desc_status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (desc_count == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
api::BluetoothGATTDescriptor descriptor_resp;
|
||||||
|
descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid);
|
||||||
|
descriptor_resp.handle = desc_result.handle;
|
||||||
|
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
|
||||||
|
desc_offset++;
|
||||||
|
}
|
||||||
|
service_resp.characteristics.push_back(std::move(characteristic_resp));
|
||||||
|
}
|
||||||
|
resp.services.push_back(std::move(service_resp));
|
||||||
|
|
||||||
|
// Send the message (we already checked api_conn is not null at the beginning)
|
||||||
|
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) {
|
esp_ble_gattc_cb_param_t *param) {
|
||||||
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
||||||
@ -25,22 +194,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
|||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ESP_GATTC_DISCONNECT_EVT: {
|
case ESP_GATTC_DISCONNECT_EVT: {
|
||||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
this->reset_connection_(param->disconnect.reason);
|
||||||
this->set_address(0);
|
|
||||||
this->proxy_->send_connections_free();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ESP_GATTC_CLOSE_EVT: {
|
case ESP_GATTC_CLOSE_EVT: {
|
||||||
this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason);
|
this->reset_connection_(param->close.reason);
|
||||||
this->set_address(0);
|
|
||||||
this->proxy_->send_connections_free();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ESP_GATTC_OPEN_EVT: {
|
case ESP_GATTC_OPEN_EVT: {
|
||||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||||
this->proxy_->send_device_connection(this->address_, false, 0, param->open.status);
|
this->reset_connection_(param->open.status);
|
||||||
this->set_address(0);
|
|
||||||
this->proxy_->send_connections_free();
|
|
||||||
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
|
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
|
||||||
this->proxy_->send_connections_free();
|
this->proxy_->send_connections_free();
|
||||||
|
@ -12,6 +12,7 @@ class BluetoothProxy;
|
|||||||
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||||
public:
|
public:
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
void loop() override;
|
||||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) override;
|
esp_ble_gattc_cb_param_t *param) override;
|
||||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||||
@ -27,6 +28,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
|||||||
protected:
|
protected:
|
||||||
friend class BluetoothProxy;
|
friend class BluetoothProxy;
|
||||||
|
|
||||||
|
void send_service_for_discovery_();
|
||||||
|
void reset_connection_(esp_err_t reason);
|
||||||
|
|
||||||
// Memory optimized layout for 32-bit systems
|
// Memory optimized layout for 32-bit systems
|
||||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||||
BluetoothProxy *proxy_;
|
BluetoothProxy *proxy_;
|
||||||
|
@ -11,19 +11,6 @@ namespace esphome {
|
|||||||
namespace bluetooth_proxy {
|
namespace bluetooth_proxy {
|
||||||
|
|
||||||
static const char *const TAG = "bluetooth_proxy";
|
static const char *const TAG = "bluetooth_proxy";
|
||||||
static const int DONE_SENDING_SERVICES = -2;
|
|
||||||
|
|
||||||
std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
|
|
||||||
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
|
|
||||||
return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]),
|
|
||||||
((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
|
|
||||||
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch size for BLE advertisements to maximize WiFi efficiency
|
// Batch size for BLE advertisements to maximize WiFi efficiency
|
||||||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
||||||
@ -140,46 +127,6 @@ void BluetoothProxy::flush_pending_advertisements() {
|
|||||||
this->advertisement_count_ = 0;
|
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() {
|
void BluetoothProxy::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
|
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
@ -213,130 +160,12 @@ void BluetoothProxy::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Flush any pending BLE advertisements that have been accumulated but not yet sent
|
// Flush any pending BLE advertisements that have been accumulated but not yet sent
|
||||||
static uint32_t last_flush_time = 0;
|
|
||||||
uint32_t now = App.get_loop_component_start_time();
|
uint32_t now = App.get_loop_component_start_time();
|
||||||
|
|
||||||
// Flush accumulated advertisements every 100ms
|
// Flush accumulated advertisements every 100ms
|
||||||
if (now - last_flush_time >= 100) {
|
if (now - this->last_advertisement_flush_time_ >= 100) {
|
||||||
this->flush_pending_advertisements();
|
this->flush_pending_advertisements();
|
||||||
last_flush_time = now;
|
this->last_advertisement_flush_time_ = now;
|
||||||
}
|
|
||||||
for (auto *connection : this->connections_) {
|
|
||||||
if (connection->send_service_ == connection->service_count_) {
|
|
||||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
|
||||||
this->send_gatt_services_done(connection->get_address());
|
|
||||||
if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
|
||||||
connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
|
||||||
connection->release_services();
|
|
||||||
}
|
|
||||||
} else if (connection->send_service_ >= 0) {
|
|
||||||
esp_gattc_service_elem_t service_result;
|
|
||||||
uint16_t service_count = 1;
|
|
||||||
esp_gatt_status_t service_status =
|
|
||||||
esp_ble_gattc_get_service(connection->get_gattc_if(), connection->get_conn_id(), nullptr, &service_result,
|
|
||||||
&service_count, connection->send_service_);
|
|
||||||
connection->send_service_++;
|
|
||||||
if (service_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d",
|
|
||||||
connection->get_connection_index(), connection->address_str().c_str(), connection->send_service_ - 1,
|
|
||||||
service_status);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (service_count == 0) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d",
|
|
||||||
connection->get_connection_index(), connection->address_str().c_str(), service_count);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
api::BluetoothGATTGetServicesResponse resp;
|
|
||||||
resp.address = connection->get_address();
|
|
||||||
resp.services.reserve(1); // Always one service per response in this implementation
|
|
||||||
api::BluetoothGATTService service_resp;
|
|
||||||
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
|
|
||||||
service_resp.handle = service_result.start_handle;
|
|
||||||
uint16_t char_offset = 0;
|
|
||||||
esp_gattc_char_elem_t char_result;
|
|
||||||
// Get the number of characteristics directly with one call
|
|
||||||
uint16_t total_char_count = 0;
|
|
||||||
esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
|
|
||||||
connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
|
|
||||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
|
||||||
|
|
||||||
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
|
|
||||||
// Only reserve if we successfully got a count
|
|
||||||
service_resp.characteristics.reserve(total_char_count);
|
|
||||||
} else if (char_count_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
|
|
||||||
connection->address_str().c_str(), char_count_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now process characteristics
|
|
||||||
while (true) { // characteristics
|
|
||||||
uint16_t char_count = 1;
|
|
||||||
esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
|
|
||||||
connection->get_gattc_if(), connection->get_conn_id(), service_result.start_handle,
|
|
||||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
|
||||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (char_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", connection->get_connection_index(),
|
|
||||||
connection->address_str().c_str(), char_status);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (char_count == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
api::BluetoothGATTCharacteristic characteristic_resp;
|
|
||||||
characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid);
|
|
||||||
characteristic_resp.handle = char_result.char_handle;
|
|
||||||
characteristic_resp.properties = char_result.properties;
|
|
||||||
char_offset++;
|
|
||||||
|
|
||||||
// Get the number of descriptors directly with one call
|
|
||||||
uint16_t total_desc_count = 0;
|
|
||||||
esp_gatt_status_t desc_count_status =
|
|
||||||
esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
|
|
||||||
char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
|
|
||||||
|
|
||||||
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
|
|
||||||
// Only reserve if we successfully got a count
|
|
||||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
|
||||||
} else if (desc_count_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
|
|
||||||
connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
|
|
||||||
desc_count_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now process descriptors
|
|
||||||
uint16_t desc_offset = 0;
|
|
||||||
esp_gattc_descr_elem_t desc_result;
|
|
||||||
while (true) { // descriptors
|
|
||||||
uint16_t desc_count = 1;
|
|
||||||
esp_gatt_status_t desc_status =
|
|
||||||
esp_ble_gattc_get_all_descr(connection->get_gattc_if(), connection->get_conn_id(),
|
|
||||||
char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
|
||||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (desc_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", connection->get_connection_index(),
|
|
||||||
connection->address_str().c_str(), desc_status);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (desc_count == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
api::BluetoothGATTDescriptor descriptor_resp;
|
|
||||||
descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid);
|
|
||||||
descriptor_resp.handle = desc_result.handle;
|
|
||||||
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
|
|
||||||
desc_offset++;
|
|
||||||
}
|
|
||||||
service_resp.characteristics.push_back(std::move(characteristic_resp));
|
|
||||||
}
|
|
||||||
resp.services.push_back(std::move(service_resp));
|
|
||||||
this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ namespace esphome {
|
|||||||
namespace bluetooth_proxy {
|
namespace bluetooth_proxy {
|
||||||
|
|
||||||
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
||||||
|
static const int DONE_SENDING_SERVICES = -2;
|
||||||
|
|
||||||
using namespace esp32_ble_client;
|
using namespace esp32_ble_client;
|
||||||
|
|
||||||
@ -131,9 +132,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
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);
|
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
|
||||||
|
|
||||||
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
|
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::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
||||||
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
|
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
|
||||||
|
|
||||||
// Group 3: 1-byte types grouped together
|
// Group 3: 4-byte types
|
||||||
|
uint32_t last_advertisement_flush_time_{0};
|
||||||
|
|
||||||
|
// Group 4: 1-byte types grouped together
|
||||||
bool active_;
|
bool active_;
|
||||||
uint8_t advertisement_count_{0};
|
uint8_t advertisement_count_{0};
|
||||||
// 2 bytes used, 2 bytes padding
|
// 2 bytes used, 2 bytes padding
|
||||||
|
@ -31,6 +31,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
|
CoreModel,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, HexInt, TimePeriod
|
from esphome.core import CORE, HexInt, TimePeriod
|
||||||
@ -713,6 +714,7 @@ async def to_code(config):
|
|||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
|
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
|
||||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
|
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
|
||||||
|
cg.add_define(CoreModel.MULTI_ATOMICS)
|
||||||
|
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
@ -128,46 +128,53 @@ void ESP32BLETracker::loop() {
|
|||||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
|
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
while (read_idx != write_idx) {
|
while (read_idx != write_idx) {
|
||||||
// Process one result at a time directly from ring buffer
|
// Calculate how many contiguous results we can process in one batch
|
||||||
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx];
|
// If write > read: process all results from read to write
|
||||||
|
// If write <= read (wraparound): process from read to end of buffer first
|
||||||
|
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
|
||||||
|
|
||||||
|
// Process the batch for raw advertisements
|
||||||
if (this->raw_advertisements_) {
|
if (this->raw_advertisements_) {
|
||||||
for (auto *listener : this->listeners_) {
|
for (auto *listener : this->listeners_) {
|
||||||
listener->parse_devices(&scan_result, 1);
|
listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||||
}
|
}
|
||||||
for (auto *client : this->clients_) {
|
for (auto *client : this->clients_) {
|
||||||
client->parse_devices(&scan_result, 1);
|
client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process individual results for parsed advertisements
|
||||||
if (this->parse_advertisements_) {
|
if (this->parse_advertisements_) {
|
||||||
#ifdef USE_ESP32_BLE_DEVICE
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
ESPBTDevice device;
|
for (size_t i = 0; i < batch_size; i++) {
|
||||||
device.parse_scan_rst(scan_result);
|
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
|
||||||
|
ESPBTDevice device;
|
||||||
|
device.parse_scan_rst(scan_result);
|
||||||
|
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (auto *listener : this->listeners_) {
|
for (auto *listener : this->listeners_) {
|
||||||
if (listener->parse_device(device))
|
if (listener->parse_device(device))
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto *client : this->clients_) {
|
for (auto *client : this->clients_) {
|
||||||
if (client->parse_device(device)) {
|
if (client->parse_device(device)) {
|
||||||
found = true;
|
found = true;
|
||||||
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
||||||
promote_to_connecting = true;
|
promote_to_connecting = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!found && !this->scan_continuous_) {
|
if (!found && !this->scan_continuous_) {
|
||||||
this->print_bt_device_info(device);
|
this->print_bt_device_info(device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif // USE_ESP32_BLE_DEVICE
|
#endif // USE_ESP32_BLE_DEVICE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to next entry in ring buffer
|
// Update read index for entire batch
|
||||||
read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
|
read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
|
||||||
|
|
||||||
// Store with release to ensure reads complete before index update
|
// Store with release to ensure reads complete before index update
|
||||||
this->ring_read_index_.store(read_idx, std::memory_order_release);
|
this->ring_read_index_.store(read_idx, std::memory_order_release);
|
||||||
|
@ -15,6 +15,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
|
CoreModel,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.helpers import copy_file_if_changed
|
from esphome.helpers import copy_file_if_changed
|
||||||
@ -187,6 +188,7 @@ async def to_code(config):
|
|||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||||
|
cg.add_define(CoreModel.SINGLE)
|
||||||
|
|
||||||
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_HOST,
|
PLATFORM_HOST,
|
||||||
|
CoreModel,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ async def to_code(config):
|
|||||||
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
|
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
|
||||||
cg.add_build_flag("-std=gnu++20")
|
cg.add_build_flag("-std=gnu++20")
|
||||||
cg.add_define("ESPHOME_BOARD", "host")
|
cg.add_define("ESPHOME_BOARD", "host")
|
||||||
|
cg.add_define(CoreModel.MULTI_ATOMICS)
|
||||||
cg.add_platformio_option("platform", "platformio/native")
|
cg.add_platformio_option("platform", "platformio/native")
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
@ -20,6 +20,7 @@ from esphome.const import (
|
|||||||
KEY_FRAMEWORK_VERSION,
|
KEY_FRAMEWORK_VERSION,
|
||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
|
CoreModel,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
@ -260,6 +261,7 @@ async def component_to_code(config):
|
|||||||
cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}")
|
cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}")
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]])
|
cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]])
|
||||||
|
cg.add_define(CoreModel.MULTI_NO_ATOMICS)
|
||||||
|
|
||||||
# force using arduino framework
|
# force using arduino framework
|
||||||
cg.add_platformio_option("framework", "arduino")
|
cg.add_platformio_option("framework", "arduino")
|
||||||
|
@ -3,6 +3,7 @@ import esphome.codegen as cg
|
|||||||
from esphome.components import display, spi
|
from esphome.components import display, spi
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_FLIP_X,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_INTENSITY,
|
CONF_INTENSITY,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
@ -14,7 +15,6 @@ CODEOWNERS = ["@rspaargaren"]
|
|||||||
DEPENDENCIES = ["spi"]
|
DEPENDENCIES = ["spi"]
|
||||||
|
|
||||||
CONF_ROTATE_CHIP = "rotate_chip"
|
CONF_ROTATE_CHIP = "rotate_chip"
|
||||||
CONF_FLIP_X = "flip_x"
|
|
||||||
CONF_SCROLL_SPEED = "scroll_speed"
|
CONF_SCROLL_SPEED = "scroll_speed"
|
||||||
CONF_SCROLL_DWELL = "scroll_dwell"
|
CONF_SCROLL_DWELL = "scroll_dwell"
|
||||||
CONF_SCROLL_DELAY = "scroll_delay"
|
CONF_SCROLL_DELAY = "scroll_delay"
|
||||||
|
@ -16,6 +16,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
|
CoreModel,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, EsphomeError, coroutine_with_priority
|
from esphome.core import CORE, EsphomeError, coroutine_with_priority
|
||||||
from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file
|
from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file
|
||||||
@ -171,6 +172,7 @@ async def to_code(config):
|
|||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
cg.add_define("ESPHOME_VARIANT", "RP2040")
|
cg.add_define("ESPHOME_VARIANT", "RP2040")
|
||||||
|
cg.add_define(CoreModel.SINGLE)
|
||||||
|
|
||||||
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ from esphome.const import (
|
|||||||
CONF_BRIGHTNESS,
|
CONF_BRIGHTNESS,
|
||||||
CONF_CONTRAST,
|
CONF_CONTRAST,
|
||||||
CONF_EXTERNAL_VCC,
|
CONF_EXTERNAL_VCC,
|
||||||
|
CONF_FLIP_X,
|
||||||
|
CONF_FLIP_Y,
|
||||||
CONF_INVERT,
|
CONF_INVERT,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
@ -18,9 +20,6 @@ ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base")
|
|||||||
SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer)
|
SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer)
|
||||||
SSD1306Model = ssd1306_base_ns.enum("SSD1306Model")
|
SSD1306Model = ssd1306_base_ns.enum("SSD1306Model")
|
||||||
|
|
||||||
CONF_FLIP_X = "flip_x"
|
|
||||||
CONF_FLIP_Y = "flip_y"
|
|
||||||
|
|
||||||
MODELS = {
|
MODELS = {
|
||||||
"SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32,
|
"SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32,
|
||||||
"SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64,
|
"SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64,
|
||||||
|
@ -35,6 +35,14 @@ class Framework(StrEnum):
|
|||||||
ZEPHYR = "zephyr"
|
ZEPHYR = "zephyr"
|
||||||
|
|
||||||
|
|
||||||
|
class CoreModel(StrEnum):
|
||||||
|
"""Core model identifiers for ESPHome scheduler."""
|
||||||
|
|
||||||
|
SINGLE = "ESPHOME_CORES_SINGLE"
|
||||||
|
MULTI_NO_ATOMICS = "ESPHOME_CORES_MULTI_NO_ATOMICS"
|
||||||
|
MULTI_ATOMICS = "ESPHOME_CORES_MULTI_ATOMICS"
|
||||||
|
|
||||||
|
|
||||||
class PlatformFramework(Enum):
|
class PlatformFramework(Enum):
|
||||||
"""Combined platform-framework identifiers with tuple values."""
|
"""Combined platform-framework identifiers with tuple values."""
|
||||||
|
|
||||||
@ -375,6 +383,8 @@ CONF_FINGER_ID = "finger_id"
|
|||||||
CONF_FINGERPRINT_COUNT = "fingerprint_count"
|
CONF_FINGERPRINT_COUNT = "fingerprint_count"
|
||||||
CONF_FLASH_LENGTH = "flash_length"
|
CONF_FLASH_LENGTH = "flash_length"
|
||||||
CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
|
CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
|
||||||
|
CONF_FLIP_X = "flip_x"
|
||||||
|
CONF_FLIP_Y = "flip_y"
|
||||||
CONF_FLOW = "flow"
|
CONF_FLOW = "flow"
|
||||||
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
||||||
CONF_FONT = "font"
|
CONF_FONT = "font"
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
#define ESPHOME_VARIANT "ESP32"
|
#define ESPHOME_VARIANT "ESP32"
|
||||||
#define ESPHOME_DEBUG_SCHEDULER
|
#define ESPHOME_DEBUG_SCHEDULER
|
||||||
|
|
||||||
|
// Default threading model for static analysis (ESP32 is multi-core with atomics)
|
||||||
|
#define ESPHOME_CORES_MULTI_ATOMICS
|
||||||
|
|
||||||
// logger
|
// logger
|
||||||
#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ static void validate_static_string(const char *name) {
|
|||||||
ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str);
|
ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
// A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to
|
// A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to
|
||||||
// them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task,
|
// them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task,
|
||||||
@ -82,9 +82,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
item->callback = std::move(func);
|
item->callback = std::move(func);
|
||||||
item->remove = false;
|
item->remove = false;
|
||||||
|
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// Special handling for defer() (delay = 0, type = TIMEOUT)
|
// Special handling for defer() (delay = 0, type = TIMEOUT)
|
||||||
// ESP8266 and RP2040 are excluded because they don't need thread-safe defer handling
|
// Single-core platforms don't need thread-safe defer handling
|
||||||
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
||||||
// Put in defer queue for guaranteed FIFO execution
|
// Put in defer queue for guaranteed FIFO execution
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
@ -92,7 +92,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
this->defer_queue_.push_back(std::move(item));
|
this->defer_queue_.push_back(std::move(item));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* not ESPHOME_CORES_SINGLE */
|
||||||
|
|
||||||
// Get fresh timestamp for new timer/interval - ensures accurate scheduling
|
// Get fresh timestamp for new timer/interval - ensures accurate scheduling
|
||||||
const auto now = this->millis_64_(millis()); // Fresh millis() call
|
const auto now = this->millis_64_(millis()); // Fresh millis() call
|
||||||
@ -123,7 +123,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(),
|
ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(),
|
||||||
name_cstr ? name_cstr : "(null)", type_str, delay, static_cast<uint32_t>(item->next_execution_ - now));
|
name_cstr ? name_cstr : "(null)", type_str, delay, static_cast<uint32_t>(item->next_execution_ - now));
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
// If name is provided, do atomic cancel-and-add
|
// If name is provided, do atomic cancel-and-add
|
||||||
@ -231,7 +231,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
|||||||
return item->next_execution_ - now_64;
|
return item->next_execution_ - now_64;
|
||||||
}
|
}
|
||||||
void HOT Scheduler::call(uint32_t now) {
|
void HOT Scheduler::call(uint32_t now) {
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// Process defer queue first to guarantee FIFO execution order for deferred items.
|
// Process defer queue first to guarantee FIFO execution order for deferred items.
|
||||||
// Previously, defer() used the heap which gave undefined order for equal timestamps,
|
// Previously, defer() used the heap which gave undefined order for equal timestamps,
|
||||||
// causing race conditions on multi-core systems (ESP32, BK7200).
|
// causing race conditions on multi-core systems (ESP32, BK7200).
|
||||||
@ -239,8 +239,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
// - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_
|
// - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_
|
||||||
// - Items execute in exact order they were deferred (FIFO guarantee)
|
// - Items execute in exact order they were deferred (FIFO guarantee)
|
||||||
// - No deferred items exist in to_add_, so processing order doesn't affect correctness
|
// - No deferred items exist in to_add_, so processing order doesn't affect correctness
|
||||||
// ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach
|
// Single-core platforms don't use this queue and fall back to the heap-based approach.
|
||||||
// (ESP8266: single-core, RP2040: empty mutex implementation).
|
|
||||||
//
|
//
|
||||||
// Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
|
// Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
|
||||||
// processed here. They are removed from the queue normally via pop_front() but skipped
|
// processed here. They are removed from the queue normally via pop_front() but skipped
|
||||||
@ -262,7 +261,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
this->execute_item_(item.get(), now);
|
this->execute_item_(item.get(), now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* not ESPHOME_CORES_SINGLE */
|
||||||
|
|
||||||
// Convert the fresh timestamp from main loop to 64-bit for scheduler operations
|
// Convert the fresh timestamp from main loop to 64-bit for scheduler operations
|
||||||
const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop()
|
const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop()
|
||||||
@ -274,13 +273,15 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
if (now_64 - last_print > 2000) {
|
if (now_64 - last_print > 2000) {
|
||||||
last_print = now_64;
|
last_print = now_64;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
|
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
|
||||||
this->millis_major_, this->last_millis_.load(std::memory_order_relaxed));
|
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
|
||||||
#else
|
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
|
||||||
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
|
major_dbg, last_dbg);
|
||||||
|
#else /* not ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
|
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
|
||||||
this->millis_major_, this->last_millis_);
|
this->millis_major_, this->last_millis_);
|
||||||
#endif
|
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
while (!this->empty_()) {
|
while (!this->empty_()) {
|
||||||
std::unique_ptr<SchedulerItem> item;
|
std::unique_ptr<SchedulerItem> item;
|
||||||
{
|
{
|
||||||
@ -305,7 +306,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif // ESPHOME_DEBUG_SCHEDULER
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
// If we have too many items to remove
|
// If we have too many items to remove
|
||||||
if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
|
if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
|
||||||
@ -352,7 +353,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
||||||
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
|
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
|
||||||
item->next_execution_, now_64);
|
item->next_execution_, now_64);
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
// Warning: During callback(), a lot of stuff can happen, including:
|
// Warning: During callback(), a lot of stuff can happen, including:
|
||||||
// - timeouts/intervals get added, potentially invalidating vector pointers
|
// - timeouts/intervals get added, potentially invalidating vector pointers
|
||||||
@ -460,7 +461,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
|||||||
size_t total_cancelled = 0;
|
size_t total_cancelled = 0;
|
||||||
|
|
||||||
// Check all containers for matching items
|
// Check all containers for matching items
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// Only check defer queue for timeouts (intervals never go there)
|
// Only check defer queue for timeouts (intervals never go there)
|
||||||
if (type == SchedulerItem::TIMEOUT) {
|
if (type == SchedulerItem::TIMEOUT) {
|
||||||
for (auto &item : this->defer_queue_) {
|
for (auto &item : this->defer_queue_) {
|
||||||
@ -470,7 +471,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif /* not ESPHOME_CORES_SINGLE */
|
||||||
|
|
||||||
// Cancel items in the main heap
|
// Cancel items in the main heap
|
||||||
for (auto &item : this->items_) {
|
for (auto &item : this->items_) {
|
||||||
@ -495,24 +496,53 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
|||||||
|
|
||||||
uint64_t Scheduler::millis_64_(uint32_t now) {
|
uint64_t Scheduler::millis_64_(uint32_t now) {
|
||||||
// THREAD SAFETY NOTE:
|
// THREAD SAFETY NOTE:
|
||||||
// This function can be called from multiple threads simultaneously on ESP32/LibreTiny.
|
// This function has three implementations, based on the precompiler flags
|
||||||
// On single-threaded platforms (ESP8266, RP2040), atomics are not needed.
|
// - ESPHOME_CORES_SINGLE - Runs on single-core platforms (ESP8266, RP2040, etc.)
|
||||||
|
// - ESPHOME_CORES_MULTI_NO_ATOMICS - Runs on multi-core platforms without atomics (LibreTiny)
|
||||||
|
// - ESPHOME_CORES_MULTI_ATOMICS - Runs on multi-core platforms with atomics (ESP32, HOST, etc.)
|
||||||
|
//
|
||||||
|
// Make sure all changes are synchronized if you edit this function.
|
||||||
//
|
//
|
||||||
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
|
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
|
||||||
// handles out-of-order timestamps between threads, but minimizing time differences
|
// handles out-of-order timestamps between threads, but minimizing time differences
|
||||||
// helps maintain accuracy.
|
// helps maintain accuracy.
|
||||||
//
|
//
|
||||||
// The implementation handles the 32-bit rollover (every 49.7 days) by:
|
|
||||||
// 1. Using a lock when detecting rollover to ensure atomic update
|
|
||||||
// 2. Restricting normal updates to forward movement within the same epoch
|
|
||||||
// This prevents race conditions at the rollover boundary without requiring
|
|
||||||
// 64-bit atomics or locking on every call.
|
|
||||||
|
|
||||||
#ifdef USE_LIBRETINY
|
#ifdef ESPHOME_CORES_SINGLE
|
||||||
// LibreTiny: Multi-threaded but lacks atomic operation support
|
// This is the single core implementation.
|
||||||
// TODO: If LibreTiny ever adds atomic support, remove this entire block and
|
//
|
||||||
// let it fall through to the atomic-based implementation below
|
// Single-core platforms have no concurrency, so this is a simple implementation
|
||||||
// We need to use a lock when near the rollover boundary to prevent races
|
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
|
||||||
|
|
||||||
|
uint16_t major = this->millis_major_;
|
||||||
|
uint32_t last = this->last_millis_;
|
||||||
|
|
||||||
|
// Check for rollover
|
||||||
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
|
this->millis_major_++;
|
||||||
|
major++;
|
||||||
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
|
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||||
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if time moved forward
|
||||||
|
if (now > last) {
|
||||||
|
this->last_millis_ = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
||||||
|
return now + (static_cast<uint64_t>(major) << 32);
|
||||||
|
|
||||||
|
#elif defined(ESPHOME_CORES_MULTI_NO_ATOMICS)
|
||||||
|
// This is the multi core no atomics implementation.
|
||||||
|
//
|
||||||
|
// Without atomics, this implementation uses locks more aggressively:
|
||||||
|
// 1. Always locks when near the rollover boundary (within 10 seconds)
|
||||||
|
// 2. Always locks when detecting a large backwards jump
|
||||||
|
// 3. Updates without lock in normal forward progression (accepting minor races)
|
||||||
|
// This is less efficient but necessary without atomic operations.
|
||||||
|
uint16_t major = this->millis_major_;
|
||||||
uint32_t last = this->last_millis_;
|
uint32_t last = this->last_millis_;
|
||||||
|
|
||||||
// Define a safe window around the rollover point (10 seconds)
|
// Define a safe window around the rollover point (10 seconds)
|
||||||
@ -531,9 +561,10 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
|||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
// True rollover detected (happens every ~49.7 days)
|
// True rollover detected (happens every ~49.7 days)
|
||||||
this->millis_major_++;
|
this->millis_major_++;
|
||||||
|
major++;
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
}
|
}
|
||||||
// Update last_millis_ while holding lock
|
// Update last_millis_ while holding lock
|
||||||
this->last_millis_ = now;
|
this->last_millis_ = now;
|
||||||
@ -549,58 +580,76 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
|||||||
// If now <= last and we're not near rollover, don't update
|
// If now <= last and we're not near rollover, don't update
|
||||||
// This minimizes backwards time movement
|
// This minimizes backwards time movement
|
||||||
|
|
||||||
#elif !defined(USE_ESP8266) && !defined(USE_RP2040)
|
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
||||||
// Multi-threaded platforms with atomic support (ESP32)
|
return now + (static_cast<uint64_t>(major) << 32);
|
||||||
uint32_t last = this->last_millis_.load(std::memory_order_relaxed);
|
|
||||||
|
|
||||||
// If we might be near a rollover (large backwards jump), take the lock for the entire operation
|
#elif defined(ESPHOME_CORES_MULTI_ATOMICS)
|
||||||
// This ensures rollover detection and last_millis_ update are atomic together
|
// This is the multi core with atomics implementation.
|
||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
//
|
||||||
// Potential rollover - need lock for atomic rollover detection + update
|
// Uses atomic operations with acquire/release semantics to ensure coherent
|
||||||
LockGuard guard{this->lock_};
|
// reads of millis_major_ and last_millis_ across cores. Features:
|
||||||
// Re-read with lock held
|
// 1. Epoch-coherency retry loop to handle concurrent updates
|
||||||
last = this->last_millis_.load(std::memory_order_relaxed);
|
// 2. Lock only taken for actual rollover detection and update
|
||||||
|
// 3. Lock-free CAS updates for normal forward time progression
|
||||||
|
// 4. Memory ordering ensures cores see consistent time values
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
uint16_t major = this->millis_major_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Acquire so that if we later decide **not** to take the lock we still
|
||||||
|
* observe a `millis_major_` value coherent with the loaded `last_millis_`.
|
||||||
|
* The acquire load ensures any later read of `millis_major_` sees its
|
||||||
|
* corresponding increment.
|
||||||
|
*/
|
||||||
|
uint32_t last = this->last_millis_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
|
// If we might be near a rollover (large backwards jump), take the lock for the entire operation
|
||||||
|
// This ensures rollover detection and last_millis_ update are atomic together
|
||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
// True rollover detected (happens every ~49.7 days)
|
// Potential rollover - need lock for atomic rollover detection + update
|
||||||
this->millis_major_++;
|
LockGuard guard{this->lock_};
|
||||||
|
// Re-read with lock held; mutex already provides ordering
|
||||||
|
last = this->last_millis_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||||
|
// True rollover detected (happens every ~49.7 days)
|
||||||
|
this->millis_major_.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
major++;
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||||
#endif
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
}
|
}
|
||||||
// Update last_millis_ while holding lock to prevent races
|
/*
|
||||||
this->last_millis_.store(now, std::memory_order_relaxed);
|
* Update last_millis_ while holding the lock to prevent races
|
||||||
} else {
|
* Publish the new low-word *after* bumping `millis_major_` (done above)
|
||||||
// Normal case: Try lock-free update, but only allow forward movement within same epoch
|
* so readers never see a mismatched pair.
|
||||||
// This prevents accidentally moving backwards across a rollover boundary
|
*/
|
||||||
while (now > last && (now - last) < HALF_MAX_UINT32) {
|
this->last_millis_.store(now, std::memory_order_release);
|
||||||
if (this->last_millis_.compare_exchange_weak(last, now, std::memory_order_relaxed)) {
|
} else {
|
||||||
break;
|
// Normal case: Try lock-free update, but only allow forward movement within same epoch
|
||||||
|
// This prevents accidentally moving backwards across a rollover boundary
|
||||||
|
while (now > last && (now - last) < HALF_MAX_UINT32) {
|
||||||
|
if (this->last_millis_.compare_exchange_weak(last, now,
|
||||||
|
std::memory_order_release, // success
|
||||||
|
std::memory_order_relaxed)) { // failure
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// CAS failure means no data was published; relaxed is fine
|
||||||
|
// last is automatically updated by compare_exchange_weak if it fails
|
||||||
}
|
}
|
||||||
// last is automatically updated by compare_exchange_weak if it fails
|
|
||||||
}
|
}
|
||||||
|
uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed);
|
||||||
|
if (major_end == major)
|
||||||
|
return now + (static_cast<uint64_t>(major) << 32);
|
||||||
}
|
}
|
||||||
|
// Unreachable - the loop always returns when major_end == major
|
||||||
|
__builtin_unreachable();
|
||||||
|
|
||||||
#else
|
#else
|
||||||
// Single-threaded platforms (ESP8266, RP2040): No atomics needed
|
#error \
|
||||||
uint32_t last = this->last_millis_;
|
"No platform threading model defined. One of ESPHOME_CORES_SINGLE, ESPHOME_CORES_MULTI_NO_ATOMICS, or ESPHOME_CORES_MULTI_ATOMICS must be defined."
|
||||||
|
|
||||||
// Check for rollover
|
|
||||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
|
||||||
this->millis_major_++;
|
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
|
||||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
|
|
||||||
// Only update if time moved forward
|
|
||||||
if (now > last) {
|
|
||||||
this->last_millis_ = now;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
|
||||||
return now + (static_cast<uint64_t>(this->millis_major_) << 32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
|
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -204,23 +205,40 @@ class Scheduler {
|
|||||||
Mutex lock_;
|
Mutex lock_;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> items_;
|
std::vector<std::unique_ptr<SchedulerItem>> items_;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
|
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
|
#ifndef ESPHOME_CORES_SINGLE
|
||||||
// ESP8266 and RP2040 don't need the defer queue because:
|
// Single-core platforms don't need the defer queue and save 40 bytes of RAM
|
||||||
// ESP8266: Single-core with no preemptive multitasking
|
|
||||||
// RP2040: Currently has empty mutex implementation in ESPHome
|
|
||||||
// Both platforms save 40 bytes of RAM by excluding this
|
|
||||||
std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
|
std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
|
||||||
#endif
|
#endif /* ESPHOME_CORES_SINGLE */
|
||||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY)
|
uint32_t to_remove_{0};
|
||||||
// Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
|
|
||||||
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
|
/*
|
||||||
|
* Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
|
||||||
|
*
|
||||||
|
* MEMORY-ORDERING NOTE
|
||||||
|
* --------------------
|
||||||
|
* `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half.
|
||||||
|
* Writers publish `last_millis_` with memory_order_release and readers use
|
||||||
|
* memory_order_acquire. This ensures that once a reader sees the new low word,
|
||||||
|
* it also observes the corresponding increment of `millis_major_`.
|
||||||
|
*/
|
||||||
std::atomic<uint32_t> last_millis_{0};
|
std::atomic<uint32_t> last_millis_{0};
|
||||||
#else
|
#else /* not ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
// Platforms without atomic support or single-threaded platforms
|
// Platforms without atomic support or single-threaded platforms
|
||||||
uint32_t last_millis_{0};
|
uint32_t last_millis_{0};
|
||||||
#endif
|
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
// millis_major_ is protected by lock when incrementing
|
|
||||||
|
/*
|
||||||
|
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding
|
||||||
|
* `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race.
|
||||||
|
* Ordering relative to `last_millis_` is provided by its release store and the
|
||||||
|
* corresponding acquire loads.
|
||||||
|
*/
|
||||||
|
#ifdef ESPHOME_CORES_MULTI_ATOMICS
|
||||||
|
std::atomic<uint16_t> millis_major_{0};
|
||||||
|
#else /* not ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
uint16_t millis_major_{0};
|
uint16_t millis_major_{0};
|
||||||
uint32_t to_remove_{0};
|
#endif /* else ESPHOME_CORES_MULTI_ATOMICS */
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -971,11 +971,11 @@ class RepeatedTypeInfo(TypeInfo):
|
|||||||
|
|
||||||
def build_type_usage_map(
|
def build_type_usage_map(
|
||||||
file_desc: descriptor.FileDescriptorProto,
|
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.
|
"""Build mappings for both enums and messages to their ifdefs based on usage.
|
||||||
|
|
||||||
Returns:
|
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] = {}
|
enum_ifdef_map: dict[str, str | None] = {}
|
||||||
message_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[
|
message_usage: dict[
|
||||||
str, set[str]
|
str, set[str]
|
||||||
] = {} # message_name -> set of message names that use it
|
] = {} # 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
|
# Build message name to ifdef mapping for quick lookup
|
||||||
message_to_ifdef: dict[str, str | None] = {
|
message_to_ifdef: dict[str, str | None] = {
|
||||||
@ -996,17 +997,26 @@ def build_type_usage_map(
|
|||||||
|
|
||||||
# Analyze field usage
|
# Analyze field usage
|
||||||
for message in file_desc.message_type:
|
for message in file_desc.message_type:
|
||||||
|
# Skip deprecated messages entirely
|
||||||
|
if message.options.deprecated:
|
||||||
|
continue
|
||||||
|
|
||||||
for field in message.field:
|
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
|
type_name = field.type_name.split(".")[-1] if field.type_name else None
|
||||||
if not type_name:
|
if not type_name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Track enum usage
|
# Track enum usage (only from non-deprecated fields)
|
||||||
if field.type == 14: # TYPE_ENUM
|
if field.type == 14: # TYPE_ENUM
|
||||||
enum_usage.setdefault(type_name, set()).add(message.name)
|
enum_usage.setdefault(type_name, set()).add(message.name)
|
||||||
# Track message usage
|
# Track message usage
|
||||||
elif field.type == 11: # TYPE_MESSAGE
|
elif field.type == 11: # TYPE_MESSAGE
|
||||||
message_usage.setdefault(type_name, set()).add(message.name)
|
message_usage.setdefault(type_name, set()).add(message.name)
|
||||||
|
used_messages.add(type_name)
|
||||||
|
|
||||||
# Helper to get unique ifdef from a set of messages
|
# Helper to get unique ifdef from a set of messages
|
||||||
def get_unique_ifdef(message_names: set[str]) -> str | None:
|
def get_unique_ifdef(message_names: set[str]) -> str | None:
|
||||||
@ -1069,12 +1079,18 @@ def build_type_usage_map(
|
|||||||
# Build message source map
|
# Build message source map
|
||||||
# First pass: Get explicit sources for messages with source option or id
|
# First pass: Get explicit sources for messages with source option or id
|
||||||
for msg in file_desc.message_type:
|
for msg in file_desc.message_type:
|
||||||
|
# Skip deprecated messages
|
||||||
|
if msg.options.deprecated:
|
||||||
|
continue
|
||||||
|
|
||||||
if msg.options.HasExtension(pb.source):
|
if msg.options.HasExtension(pb.source):
|
||||||
# Explicit source option takes precedence
|
# Explicit source option takes precedence
|
||||||
message_source_map[msg.name] = get_opt(msg, pb.source, SOURCE_BOTH)
|
message_source_map[msg.name] = get_opt(msg, pb.source, SOURCE_BOTH)
|
||||||
elif msg.options.HasExtension(pb.id):
|
elif msg.options.HasExtension(pb.id):
|
||||||
# Service messages (with id) default to SOURCE_BOTH
|
# Service messages (with id) default to SOURCE_BOTH
|
||||||
message_source_map[msg.name] = 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
|
# Second pass: Determine sources for embedded messages based on their usage
|
||||||
for msg in file_desc.message_type:
|
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
|
# Not used by any message and no explicit source - default to encode-only
|
||||||
message_source_map[msg.name] = SOURCE_SERVER
|
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]:
|
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
|
total_size = 0
|
||||||
|
|
||||||
for field in desc.field:
|
for field in desc.field:
|
||||||
|
# Skip deprecated fields
|
||||||
|
if field.options.deprecated:
|
||||||
|
continue
|
||||||
|
|
||||||
ti = create_field_type_info(field)
|
ti = create_field_type_info(field)
|
||||||
|
|
||||||
# Add estimated size for this field
|
# Add estimated size for this field
|
||||||
@ -1213,6 +1238,10 @@ def build_message_type(
|
|||||||
public_content.append("#endif")
|
public_content.append("#endif")
|
||||||
|
|
||||||
for field in desc.field:
|
for field in desc.field:
|
||||||
|
# Skip deprecated fields completely
|
||||||
|
if field.options.deprecated:
|
||||||
|
continue
|
||||||
|
|
||||||
ti = create_field_type_info(field)
|
ti = create_field_type_info(field)
|
||||||
|
|
||||||
# Skip field declarations for fields that are in the base class
|
# Skip field declarations for fields that are in the base class
|
||||||
@ -1459,8 +1488,10 @@ def find_common_fields(
|
|||||||
if not messages:
|
if not messages:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Start with fields from the first message
|
# Start with fields from the first message (excluding deprecated fields)
|
||||||
first_msg_fields = {field.name: field for field in messages[0].field}
|
first_msg_fields = {
|
||||||
|
field.name: field for field in messages[0].field if not field.options.deprecated
|
||||||
|
}
|
||||||
common_fields = []
|
common_fields = []
|
||||||
|
|
||||||
# Check each field to see if it exists in all messages with same type
|
# 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:]:
|
for msg in messages[1:]:
|
||||||
found = False
|
found = False
|
||||||
for other_field in msg.field:
|
for other_field in msg.field:
|
||||||
|
# Skip deprecated fields
|
||||||
|
if other_field.options.deprecated:
|
||||||
|
continue
|
||||||
if (
|
if (
|
||||||
other_field.name == field_name
|
other_field.name == field_name
|
||||||
and other_field.type == field.type
|
and other_field.type == field.type
|
||||||
@ -1599,6 +1633,10 @@ def build_service_message_type(
|
|||||||
message_source_map: dict[str, int],
|
message_source_map: dict[str, int],
|
||||||
) -> tuple[str, str] | None:
|
) -> tuple[str, str] | None:
|
||||||
"""Builds the service message type."""
|
"""Builds the service message type."""
|
||||||
|
# Skip deprecated messages
|
||||||
|
if mt.options.deprecated:
|
||||||
|
return None
|
||||||
|
|
||||||
snake = camel_to_snake(mt.name)
|
snake = camel_to_snake(mt.name)
|
||||||
id_: int | None = get_opt(mt, pb.id)
|
id_: int | None = get_opt(mt, pb.id)
|
||||||
if id_ is None:
|
if id_ is None:
|
||||||
@ -1700,12 +1738,18 @@ namespace api {
|
|||||||
content += "namespace enums {\n\n"
|
content += "namespace enums {\n\n"
|
||||||
|
|
||||||
# Build dynamic ifdef mappings for both enums and messages
|
# 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
|
# Simple grouping of enums by ifdef
|
||||||
current_ifdef = None
|
current_ifdef = None
|
||||||
|
|
||||||
for enum in file.enum_type:
|
for enum in file.enum_type:
|
||||||
|
# Skip deprecated enums
|
||||||
|
if enum.options.deprecated:
|
||||||
|
continue
|
||||||
|
|
||||||
s, c, dc = build_enum_type(enum, enum_ifdef_map)
|
s, c, dc = build_enum_type(enum, enum_ifdef_map)
|
||||||
enum_ifdef = enum_ifdef_map.get(enum.name)
|
enum_ifdef = enum_ifdef_map.get(enum.name)
|
||||||
|
|
||||||
@ -1756,6 +1800,14 @@ namespace api {
|
|||||||
current_ifdef = None
|
current_ifdef = None
|
||||||
|
|
||||||
for m in mt:
|
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)
|
s, c, dc = build_message_type(m, base_class_fields, message_source_map)
|
||||||
msg_ifdef = message_ifdef_map.get(m.name)
|
msg_ifdef = message_ifdef_map.get(m.name)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user