Compare commits

..
113 changed files with 730 additions and 5902 deletions
-2
View File
@@ -183,7 +183,6 @@ esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81
esphome/components/es8311/* @kahrendt @kroimon
esphome/components/es8388/* @P4uLT
esphome/components/es9038q2m_katana/* @GioMez
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
esphome/components/esp32_ble_client/* @bdraco @jesserockz
@@ -257,7 +256,6 @@ esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/hdc2080/* @G-Pereira @jesserockz
esphome/components/hdc302x/* @joshuasing
esphome/components/hdmi_cec/* @JosVanEijndhoven
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
+33
View File
@@ -251,6 +251,9 @@ def _validate_supports_response(value: Any) -> str:
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
VARIABLE_SCHEMA = cv.Schema(
{
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
@@ -481,6 +484,29 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
return strings
def _action_strings_size(strings: list[str | None]) -> int:
"""Bytes needed to copy every string out of flash, each with its terminator."""
return sum(
len(string.encode("utf-8")) + 1 for string in strings if string is not None
)
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
if not CORE.is_esp8266:
return config
actions = config.get(CONF_ACTIONS, [])
has_metadata = _has_action_metadata(actions)
for conf in actions:
size = _action_strings_size(_action_strings(conf, has_metadata))
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
raise cv.Invalid(
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
f"description and example text; ESP8266 allows at most "
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
)
return config
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
if (
@@ -505,6 +531,7 @@ def _validate_wizard(config: ConfigType) -> ConfigType:
FINAL_VALIDATE_SCHEMA = cv.All(
_validate_esp8266_action_strings,
_validate_outgoing_host_ipv6,
_validate_wizard,
)
@@ -560,6 +587,7 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
scratch_size = 0
if actions:
# Metadata is compiled in for every action once any action declares it, because the
# string table layout is fixed by the define rather than per action
@@ -611,6 +639,8 @@ async def to_code(config: ConfigType) -> None:
func_args.append((native, name))
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
# Key is hashed here because the name is not readable at runtime on ESP8266
@@ -637,6 +667,9 @@ async def to_code(config: ConfigType) -> None:
cg.add(auto.add_action(unregister_action))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
await wizard.to_code(wizard_config)
+8 -11
View File
@@ -403,9 +403,6 @@ message WizardCapabilities {
message SendspinCapabilities {
// Bitmask of the Sendspin features this device supports
uint32 feature_flags = 1;
// The Sendspin client_id: base64url of the client's static public key, 43 characters.
// Empty until the Sendspin client has started once since boot.
string client_id = 2 [(max_data_length) = 43];
}
message DeviceCapabilitiesResponse {
@@ -1029,8 +1026,8 @@ message SubscribeHomeassistantServicesRequest {
}
message HomeassistantServiceMap {
string key = 1 [(progmem) = true];
string value = 2 [(progmem) = true];
string key = 1;
string value = 2;
}
message HomeassistantActionRequest {
@@ -1039,7 +1036,7 @@ message HomeassistantActionRequest {
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
string service = 1 [(progmem) = true];
string service = 1;
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
@@ -1160,21 +1157,21 @@ enum SupportsResponseType {
}
message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1 [(progmem) = true];
string name = 1;
ServiceArgType type = 2;
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ListEntitiesServicesResponse {
option (id) = 41;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1 [(progmem) = true];
string name = 1;
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
@@ -2131,7 +2131,6 @@ bool APIConnection::send_device_capabilities_response_() {
#endif
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
#endif
return this->send_message(resp);
}
+1 -1
View File
@@ -123,7 +123,7 @@ extend google.protobuf.FieldOptions {
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
optional bool track_presence = 50020 [default=false];
// progmem: The string field, or pointer_to_buffer bytes field, points into flash (PROGMEM).
// progmem: The pointer_to_buffer bytes field points into flash (PROGMEM).
// The generated encode and dump copy it with progmem_memcpy, as ESP8266
// cannot read flash with ordinary loads.
optional bool progmem = 50021 [default=false];
+8 -10
View File
@@ -310,14 +310,12 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.client_id);
return pos;
}
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.feature_flags);
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
return size;
}
#endif
@@ -1142,8 +1140,8 @@ uint32_t NoiseEncryptionSetKeyResponse::calc_size_msg(const void *self) {
#ifdef USE_API_HOMEASSISTANT_SERVICES
uint8_t *HomeassistantServiceMap::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const HomeassistantServiceMap *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
return pos;
}
uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
@@ -1155,7 +1153,7 @@ uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
}
uint8_t *HomeassistantActionRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const HomeassistantActionRequest *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
for (auto &it : msg.data) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 2, it);
}
@@ -1326,13 +1324,13 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
uint8_t *ListEntitiesServicesArgument::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const ListEntitiesServicesArgument *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(msg.type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
#endif
return pos;
}
@@ -1352,14 +1350,14 @@ uint32_t ListEntitiesServicesArgument::calc_size_msg(const void *self) {
uint8_t *ListEntitiesServicesResponse::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const ListEntitiesServicesResponse *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, msg.key);
for (auto &it : msg.args) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 3, it);
}
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
#endif
return pos;
}
+8 -9
View File
@@ -750,7 +750,6 @@ class WizardCapabilities final : public ProtoMessage {
class SendspinCapabilities final : public ProtoMessage {
public:
uint32_t feature_flags{0};
StringRef client_id{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -1389,8 +1388,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
#ifdef USE_API_HOMEASSISTANT_SERVICES
class HomeassistantServiceMap final : public ProtoMessage {
public:
ProgmemStringRef key{};
ProgmemStringRef value{};
StringRef key{nullptr, 0}; // null until set, encode only
StringRef value{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -1410,7 +1409,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); }
#endif
ProgmemStringRef service{};
StringRef service{nullptr, 0}; // null until set, encode only
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
@@ -1577,13 +1576,13 @@ class GetTimeResponse final : public ProtoDecodableMessage {
#ifdef USE_API_USER_DEFINED_ACTIONS
class ListEntitiesServicesArgument final : public ProtoMessage {
public:
ProgmemStringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProgmemStringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProgmemStringRef example{};
StringRef example{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1604,12 +1603,12 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
ProgmemStringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProgmemStringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
+10 -19
View File
@@ -12,9 +12,8 @@ namespace esphome::api {
#ifdef USE_ESP8266
// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site
void DumpBuffer::append_p_esp8266(const char *str) { this->append_p_esp8266(str, strlen_P(str)); }
void DumpBuffer::append_p_esp8266(const char *str, size_t len) {
void DumpBuffer::append_p_esp8266(const char *str) {
size_t len = strlen_P(str);
size_t space = CAPACITY - 1 - pos_;
if (len > space)
len = space;
@@ -135,13 +134,6 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
progmem_memcpy(data_buf, data, len);
dump_bytes_field(out, field_name, data_buf, len, indent);
}
// Helper for string fields in flash
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append_p(value.progmem_ptr(), value.size()).append("'\n");
}
#pragma GCC diagnostic pop
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
@@ -1127,7 +1119,6 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
return out.c_str();
}
#endif
@@ -1520,13 +1511,13 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
#ifdef USE_API_HOMEASSISTANT_SERVICES
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap"));
dump_progmem_string_field(out, ESPHOME_PSTR("key"), this->key);
dump_progmem_string_field(out, ESPHOME_PSTR("value"), this->value);
dump_field(out, ESPHOME_PSTR("key"), this->key);
dump_field(out, ESPHOME_PSTR("value"), this->value);
return out.c_str();
}
const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest"));
dump_progmem_string_field(out, ESPHOME_PSTR("service"), this->service);
dump_field(out, ESPHOME_PSTR("service"), this->service);
for (const auto &it : this->data) {
out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": ");
it.dump_to(out);
@@ -1621,19 +1612,19 @@ const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
#ifdef USE_API_USER_DEFINED_ACTIONS
const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_progmem_string_field(out, ESPHOME_PSTR("example"), this->example);
dump_field(out, ESPHOME_PSTR("example"), this->example);
#endif
return out.c_str();
}
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse"));
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("key"), this->key);
for (const auto &it : this->args) {
out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": ");
@@ -1642,7 +1633,7 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
}
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
return out.c_str();
}
+1 -1
View File
@@ -547,7 +547,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.progmem_ptr(),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected"));
}
+8 -8
View File
@@ -247,7 +247,7 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(service_name));
resp.service = StringRef(service_name);
global_api_server->send_homeassistant_action(resp);
}
@@ -267,12 +267,12 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(service_name));
resp.service = StringRef(service_name);
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = ProgmemStringRef(StringRef(it.first));
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
}
global_api_server->send_homeassistant_action(resp);
}
@@ -289,7 +289,7 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &event_name) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(event_name));
resp.service = StringRef(event_name);
resp.is_event = true;
global_api_server->send_homeassistant_action(resp);
}
@@ -309,13 +309,13 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(service_name));
resp.service = StringRef(service_name);
resp.is_event = true;
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = ProgmemStringRef(StringRef(it.first));
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
}
global_api_server->send_homeassistant_action(resp);
}
+28 -4
View File
@@ -116,18 +116,42 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
const Field *fields = this->fields_;
const size_t total = 1 + this->data_count_ + this->data_template_count_ + this->variables_count_;
// Lambda results must live until the send; the constant strings are encoded from flash where they are
// Lambda results, and on ESP8266 the RAM copies of the flash strings, must live until the send
size_t lambda_count = 0;
for (size_t i = 0; i < total; i++)
#ifdef USE_ESP8266
size_t flash_len = 0;
#endif
for (size_t i = 0; i < total; i++) {
lambda_count += fields[i].fn != nullptr;
#ifdef USE_ESP8266
if (fields[i].fn == nullptr)
flash_len += ESPHOME_strlen_P(fields[i].value);
if (fields[i].key != nullptr)
flash_len += ESPHOME_strlen_P(fields[i].key);
#endif
}
FixedVector<std::string> results;
results.init(lambda_count);
auto string_ref = [](const char *str) { return ProgmemStringRef(str, ESPHOME_strlen_P(str)); };
#ifdef USE_ESP8266
SmallBufferWithHeapFallback<128, char> flash_copy(flash_len);
char *cursor = flash_copy.get();
#endif
auto string_ref = [&](const char *str) {
#ifdef USE_ESP8266
size_t len = ESPHOME_strlen_P(str);
memcpy_P(cursor, str, len);
StringRef ref(cursor, len);
cursor += len;
return ref;
#else
return StringRef(str);
#endif
};
auto value_ref = [&](const Field &field) {
if (field.fn == nullptr)
return string_ref(field.value);
results.push_back(field.fn(x...));
return ProgmemStringRef(StringRef(results.back()));
return StringRef(results.back());
};
auto fill = [&](FixedVector<HomeassistantServiceMap> &dest, uint8_t count) {
dest.init(count);
+2 -1
View File
@@ -99,7 +99,8 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
UserActionScratch scratch;
auto resp = service->encode_list_service_response(scratch);
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
-34
View File
@@ -418,18 +418,6 @@ class ProtoEncode {
std::memcpy(pos + 2, ref.c_str(), ref.size());
return pos + 2 + ref.size();
}
/// encode_short_string_force for a string that may be in flash (PROGMEM).
[[nodiscard]] static inline uint8_t *encode_short_progmem_string_force(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, const ProgmemStringRef &ref) {
#ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_progmem_string_force: string exceeds max_data_length < 128");
#endif
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size());
progmem_memcpy(pos + 2, ref.progmem_ptr(), ref.size());
return pos + 2 + ref.size();
}
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
@@ -493,17 +481,6 @@ class ProtoEncode {
progmem_memcpy(pos, data, len);
return pos + len;
}
/// encode_progmem_bytes for a string field that may be in flash.
[[nodiscard]] static inline uint8_t *encode_progmem_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const ProgmemStringRef &ref) {
return encode_progmem_bytes(pos PROTO_ENCODE_DEBUG_ARG, field_id,
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
}
[[nodiscard]] static inline uint8_t *encode_progmem_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const ProgmemStringRef &ref) {
return encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, field_id,
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
}
[[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
@@ -703,20 +680,9 @@ class DumpBuffer {
return *this;
}
/// Append len bytes of a PROGMEM string
DumpBuffer &append_p(const char *str, size_t len) {
#ifdef USE_ESP8266
append_p_esp8266(str, len);
#else
append_impl_(str, len);
#endif
return *this;
}
#ifdef USE_ESP8266
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
void append_p_esp8266(const char *str);
void append_p_esp8266(const char *str, size_t len);
#endif
const char *c_str() const { return buf_; }
+20 -9
View File
@@ -1,36 +1,47 @@
#include "user_services.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
ProgmemStringRef UserServiceStatic::str_(size_t idx) const {
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
const char *s = progmem_read_ptr(&this->strings_[idx]);
if (s == nullptr)
return {};
return ProgmemStringRef(s, ESPHOME_strlen_P(s));
#ifdef USE_ESP8266
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
if (scratch.empty())
return {};
size_t len = strnlen_P(s, scratch.size() - 1);
progmem_memcpy(scratch.data(), s, len);
scratch[len] = '\0';
StringRef ref(scratch.data(), len);
scratch = scratch.subspan(len + 1);
return ref;
#else
return StringRef(s);
#endif
}
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
std::span<const enums::ServiceArgType> arg_types) const {
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
ListEntitiesServicesResponse msg;
msg.name = this->str_(0);
msg.name = this->str_(0, scratch);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
msg.description = this->str_(1);
msg.description = this->str_(1, scratch);
#endif
msg.args.init(arg_types.size());
for (size_t i = 0; i < arg_types.size(); i++) {
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = this->str_(base);
arg.name = this->str_(base, scratch);
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
arg.description = this->str_(base + 1);
arg.example = this->str_(base + 2);
arg.description = this->str_(base + 1, scratch);
arg.example = this->str_(base + 2, scratch);
#endif
}
return msg;
+21 -10
View File
@@ -20,8 +20,9 @@ class APIServer;
class UserServiceDescriptor {
public:
/// Build the list-entities message; its strings may point into flash (PROGMEM).
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -36,6 +37,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type();
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
#ifdef USE_ESP8266
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
#else
using UserActionScratch = std::array<char, 0>;
#endif
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
@@ -55,9 +63,12 @@ class UserServiceStatic : public UserServiceDescriptor {
: strings_(strings), key_(key), supports_response_(supports_response) {}
protected:
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types) const;
/// Table entry `idx` as it sits in flash; nullptr gives an empty string.
ProgmemStringRef str_(size_t idx) const;
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
std::span<char> scratch) const;
/// Reference table entry `idx`; nullptr gives an empty StringRef.
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
/// is advanced past the copy.
StringRef str_(size_t idx, std::span<char> &scratch) const;
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
uint32_t key_;
@@ -68,9 +79,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
public:
using UserServiceStatic::UserServiceStatic;
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
return this->encode_list_service_response_(arg_types);
return this->encode_list_service_response_(arg_types, scratch);
}
bool execute_service(const ExecuteServiceRequest &req) override {
@@ -114,9 +125,9 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
ListEntitiesServicesResponse msg;
msg.name = ProgmemStringRef(StringRef(this->name_));
msg.name = StringRef(this->name_);
msg.key = this->key_;
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
@@ -124,7 +135,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = ProgmemStringRef(StringRef(this->arg_names_[i]));
arg.name = StringRef(this->arg_names_[i]);
}
return msg;
}
@@ -382,7 +382,7 @@ void BluedroidGattClient::set_disconnecting_() {
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {};
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
+7 -11
View File
@@ -2,7 +2,6 @@
#include "bmi270_config.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::bmi270 {
@@ -12,9 +11,6 @@ using namespace bmm150; // NOLINT - sole consumer of the BMM150 aux-device defi
ESPHOME_LOG_TAG(TAG, "bmi270");
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±4g", "±8g", "±16g");
PROGMEM_STRING_TABLE(GyroRangeStrings, "±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s");
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
static constexpr size_t MAX_I2C_BUFFER_SIZE = 32;
#else
@@ -277,10 +273,11 @@ void BMI270Component::dump_config() {
return;
}
ESP_LOGCONFIG(TAG, " Accel range : %s",
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_, AccelRangeStrings::LAST_INDEX)));
ESP_LOGCONFIG(TAG, " Gyro range : %s",
LOG_STR_ARG(GyroRangeStrings::get_log_str(this->gyro_range_, GyroRangeStrings::LAST_INDEX)));
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
#ifdef USE_BMI270_BMM150
if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) {
ESP_LOGCONFIG(TAG, " Aux device : BMM150 magnetometer (%s)",
@@ -318,8 +315,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
}
// Scale factor: LSB/g depends on range
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
// In PROGMEM; each read is a 32-bit load, which is safe from flash on ESP8266
alignas(4) static constexpr float ACCEL_SCALE[] PROGMEM = {
static constexpr float ACCEL_SCALE[] = {
2.0f / 32768.0f,
4.0f / 32768.0f,
8.0f / 32768.0f,
@@ -333,7 +329,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
// Gyroscope: registers 0x12–0x17 (6 bytes)
// Scale: full-scale range / 2^15
alignas(4) static constexpr float GYRO_SCALE[] PROGMEM = {
static constexpr float GYRO_SCALE[] = {
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
};
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
@@ -118,8 +118,13 @@ void Ecocomfort2Fan::on_status() {
int new_speed = this->speed;
uint8_t base_speed = raw_speed & SPEED_MASK;
if (base_speed >= SPEED_SLEEP && base_speed <= SPEED_VEL3)
if (raw_speed & FLAG_BOOST) {
new_speed = SPEED_COUNT;
} else if (raw_speed & FLAG_NIGHT) {
new_speed = SPEED_SLEEP;
} else if (base_speed >= SPEED_SLEEP && base_speed <= SPEED_VEL3) {
new_speed = base_speed;
}
const Preset *preset = find_preset(mode, (raw_speed & FLAG_AUTO) != 0);
const char *new_preset = preset != nullptr ? preset->name : nullptr;
bool same_preset = new_preset == nullptr ? !this->has_preset_mode() : this->get_preset_mode().c_str() == new_preset;
@@ -205,10 +205,6 @@ void Ecocomfort2Hub::publish_unknown_() {
sensor->publish_state(NAN);
}
#endif
#ifdef USE_BINARY_SENSOR
if (this->boost_binary_sensor_ != nullptr)
this->boost_binary_sensor_->invalidate_state();
#endif
}
// C_OPER: mode byte, speed/flags byte
@@ -220,6 +216,10 @@ void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) {
this->mode_ = data[0];
this->speed_ = data[1];
ESP_LOGD(TAG, "[%s] C_OPER: mode=0x%02x, speed=0x%02x", this->parent_->address_str(), this->mode_, this->speed_);
#ifdef USE_BINARY_SENSOR
if (this->boost_binary_sensor_ != nullptr)
this->boost_binary_sensor_->publish_state((this->speed_ & FLAG_BOOST) != 0);
#endif
#ifdef ECOCOMFORT2_CHILD_COUNT
for (auto *child : this->children_) {
child->on_status();
@@ -227,9 +227,7 @@ void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) {
#endif
}
// C_STATE: bytes 0-1 the mode and speed the unit runs; byte 1 also has the boost flag (0x40) and the advanced
// control step (0x20). Byte 2 the air flow direction, bytes 3-4 temperature and 5-6 humidity in hundredths,
// 7-8 VOC in ppb
// C_STATE: bytes 3-4 temperature and 5-6 humidity in hundredths, 7-8 VOC in ppb, byte 2 the air flow direction
void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) {
if (len < 9) {
ESP_LOGW(TAG, "[%s] C_STATE too short: %u bytes", this->parent_->address_str(), len);
@@ -238,12 +236,8 @@ void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) {
const float temperature = static_cast<int16_t>(encode_uint16(data[3], data[4])) / 100.0f;
const float humidity = encode_uint16(data[5], data[6]) / 100.0f;
const uint16_t voc = encode_uint16(data[7], data[8]);
ESP_LOGD(TAG, "[%s] C_STATE: mode=0x%02x, speed=0x%02x, %.2f °C, %.2f %%, %u ppb, direction=%u",
this->parent_->address_str(), data[0], data[1], temperature, humidity, voc, data[2]);
#ifdef USE_BINARY_SENSOR
if (this->boost_binary_sensor_ != nullptr)
this->boost_binary_sensor_->publish_state((data[1] & FLAG_BOOST) != 0);
#endif
ESP_LOGD(TAG, "[%s] C_STATE: %.2f °C, %.2f %%, %u ppb, direction=%u", this->parent_->address_str(), temperature,
humidity, voc, data[2]);
#ifdef USE_SENSOR
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
@@ -273,7 +267,7 @@ void Ecocomfort2Hub::on_info_(const uint8_t *data, uint16_t len) {
}
#ifdef USE_TIME
// C_CLOCK: two-digit year, month, day, weekday with Monday as 0, hour, minute, second, automatic DST flag
// C_CLOCK: hour, minute, second, weekday with Monday as 0, day, month, year as two bytes
void Ecocomfort2Hub::sync_clock_() {
if (!this->is_ready() || this->char_handle_clock_ == 0)
return;
@@ -282,11 +276,15 @@ void Ecocomfort2Hub::sync_clock_() {
return;
// ESPTime counts Sunday=1 .. Saturday=7, the unit Monday=0 .. Sunday=6
const uint8_t weekday = (now.day_of_week + 5) % 7;
// The time source already applies DST and every sync rewrites the clock, so the unit must not shift it again
uint8_t data[8]{
static_cast<uint8_t>(now.year % 100), now.month, now.day_of_month, weekday, now.hour, now.minute, now.second, 0};
ESP_LOGD(TAG, "[%s] Syncing clock to %04u-%02u-%02u %02u:%02u:%02u", this->parent_->address_str(), now.year,
now.month, now.day_of_month, now.hour, now.minute, now.second);
uint8_t data[8]{now.hour,
now.minute,
now.second,
weekday,
now.day_of_month,
now.month,
static_cast<uint8_t>(now.year >> 8),
static_cast<uint8_t>(now.year)};
ESP_LOGD(TAG, "[%s] Syncing clock to %02u:%02u:%02u", this->parent_->address_str(), now.hour, now.minute, now.second);
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_clock_,
sizeof(data), data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
@@ -32,8 +32,7 @@ static constexpr uint8_t OPER_OUT = 0x02;
static constexpr uint8_t OPER_IN_OUT = 0x03;
static constexpr uint8_t OPER_SENSOR_OR_AUTO = 0x04;
// Speed byte: speed in the low nibble, flags in the high nibble.
// C_OPER carries FLAG_AUTO; the unit reports FLAG_BOOST in byte 1 of C_STATE.
// C_OPER byte 1: speed in the low nibble, flags in the high nibble
static constexpr uint8_t SPEED_OFF = 0x00;
static constexpr uint8_t SPEED_SLEEP = 0x01;
static constexpr uint8_t SPEED_VEL1 = 0x02;
@@ -42,6 +41,7 @@ static constexpr uint8_t SPEED_VEL3 = 0x04;
static constexpr uint8_t SPEED_MASK = 0x0F;
static constexpr uint8_t FLAG_AUTO = 0x10;
static constexpr uint8_t FLAG_BOOST = 0x40;
static constexpr uint8_t FLAG_NIGHT = 0x80;
/** Hub component for Ecocomfort 2.0 VMC via BLE. */
class Ecocomfort2Hub : public esphome::ble_client::BLEClientNode, public PollingComponent {
@@ -1,3 +0,0 @@
CODEOWNERS = ["@GioMez"]
DEPENDENCIES = ["i2c"]
DOMAIN = "es9038q2m_katana"
@@ -1,92 +0,0 @@
import esphome.codegen as cg
from esphome.components import audio_dac, i2c
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE
from esphome.types import ConfigType
CONF_DEEMPHASIS = "deemphasis"
CONF_DOP = "dop"
CONF_FILTER_SHAPE = "filter_shape"
# Fields of the Katana format register: channel layout, word length and rate code.
FORMAT_STEREO = 0x80
BITS_PER_SAMPLE_CODES = {16: 0x10, 24: 0x20, 32: 0x30}
SAMPLE_RATE_CODES = {
44100: 0x04,
48000: 0x05,
88200: 0x06,
96000: 0x07,
176400: 0x08,
192000: 0x09,
352800: 0x0A,
384000: 0x0B,
}
es9038q2m_katana_ns = cg.esphome_ns.namespace("es9038q2m_katana")
ES9038Q2MKatana = es9038q2m_katana_ns.class_(
"ES9038Q2MKatana",
audio_dac.AudioDac,
cg.Component,
i2c.I2CDevice,
)
FilterShape = es9038q2m_katana_ns.enum("FilterShape")
FILTER_SHAPES = {
"linear_phase_fast": FilterShape.FILTER_SHAPE_LINEAR_PHASE_FAST,
"linear_phase_slow": FilterShape.FILTER_SHAPE_LINEAR_PHASE_SLOW,
"minimum_phase_fast": FilterShape.FILTER_SHAPE_MIN_PHASE_FAST,
"minimum_phase_slow": FilterShape.FILTER_SHAPE_MIN_PHASE_SLOW,
"apodizing": FilterShape.FILTER_SHAPE_APODIZING,
"hybrid": FilterShape.FILTER_SHAPE_HYBRID,
"brick_wall": FilterShape.FILTER_SHAPE_BRICK_WALL,
}
DeemphasisMode = es9038q2m_katana_ns.enum("DeemphasisMode")
DEEMPHASIS_MODES = {
"bypass": DeemphasisMode.DEEMPHASIS_BYPASS,
"32khz": DeemphasisMode.DEEMPHASIS_32KHZ,
"44_1khz": DeemphasisMode.DEEMPHASIS_44_1KHZ,
"48khz": DeemphasisMode.DEEMPHASIS_48KHZ,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ES9038Q2MKatana),
cv.Optional(CONF_FILTER_SHAPE, default="apodizing"): cv.enum(
FILTER_SHAPES, lower=True
),
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
cv.float_with_unit("bits", "bit"),
cv.one_of(*BITS_PER_SAMPLE_CODES, int=True),
),
cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.one_of(
*SAMPLE_RATE_CODES, int=True
),
cv.Optional(CONF_DEEMPHASIS, default="bypass"): cv.enum(
DEEMPHASIS_MODES, lower=True
),
# Only switches the control logic; the source must already send DoP frames.
cv.Optional(CONF_DOP, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x30))
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_filter_shape(config[CONF_FILTER_SHAPE]))
cg.add(
var.set_format(
FORMAT_STEREO
| BITS_PER_SAMPLE_CODES[config[CONF_BITS_PER_SAMPLE]]
| SAMPLE_RATE_CODES[config[CONF_SAMPLE_RATE]]
)
)
cg.add(var.set_deemphasis_mode(config[CONF_DEEMPHASIS]))
cg.add(var.set_dop_enabled(config[CONF_DOP]))
@@ -1,176 +0,0 @@
#include "es9038q2m_katana.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cmath>
namespace esphome::es9038q2m_katana {
ESPHOME_LOG_TAG(TAG, "es9038q2m_katana");
namespace {
constexpr uint8_t KATANA_CHIP_ID = 0x30;
constexpr uint8_t KATANA_REG_CHIP_ID = 0x00;
constexpr uint8_t KATANA_REG_RESET = 0x01;
constexpr uint8_t KATANA_REG_VOLUME_1 = 0x02;
constexpr uint8_t KATANA_REG_VOLUME_2 = 0x03;
constexpr uint8_t KATANA_REG_MUTE = 0x04;
constexpr uint8_t KATANA_REG_DSP_PROGRAM = 0x05;
constexpr uint8_t KATANA_REG_DEEMPHASIS = 0x06;
constexpr uint8_t KATANA_REG_DOP = 0x07;
constexpr uint8_t KATANA_REG_FORMAT = 0x08;
constexpr uint8_t KATANA_REG_COMMAND = 0x09;
constexpr uint8_t KATANA_REG_MUTE_STREAM = 0x0A;
constexpr float KATANA_VOLUME_MIDPOINT = 0.5f;
constexpr float KATANA_VOLUME_MIDPOINT_STEPS = 50.0f; // -25 dB at 50% volume.
// Cubic amplitude gain: -20 * log10(volume^3) / 0.5 attenuation steps.
constexpr float KATANA_VOLUME_CUBIC_SCALE = 120.0f;
constexpr uint8_t KATANA_VOLUME_FULL_ATTENUATION = 255;
// The control logic ignores writes for this long after a reset.
constexpr uint32_t RESET_SETTLE_DELAY_MS = 10;
const LogString *filter_shape_to_str(FilterShape shape) {
switch (shape) {
case FILTER_SHAPE_LINEAR_PHASE_FAST:
return LOG_STR("linear_phase_fast");
case FILTER_SHAPE_LINEAR_PHASE_SLOW:
return LOG_STR("linear_phase_slow");
case FILTER_SHAPE_MIN_PHASE_FAST:
return LOG_STR("minimum_phase_fast");
case FILTER_SHAPE_MIN_PHASE_SLOW:
return LOG_STR("minimum_phase_slow");
case FILTER_SHAPE_APODIZING:
return LOG_STR("apodizing");
case FILTER_SHAPE_HYBRID:
return LOG_STR("hybrid");
default:
return LOG_STR("brick_wall");
}
}
const LogString *deemphasis_to_str(DeemphasisMode mode) {
switch (mode) {
case DEEMPHASIS_32KHZ:
return LOG_STR("32khz");
case DEEMPHASIS_44_1KHZ:
return LOG_STR("44_1khz");
case DEEMPHASIS_48KHZ:
return LOG_STR("48khz");
default:
return LOG_STR("bypass");
}
}
} // namespace
void ES9038Q2MKatana::setup() {
uint8_t chip_id = 0;
if (!this->read_byte(KATANA_REG_CHIP_ID, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (chip_id != KATANA_CHIP_ID) {
ESP_LOGE(TAG, "Unexpected chip ID 0x%02X", chip_id);
this->mark_failed(LOG_STR("Unexpected chip ID"));
return;
}
if (!this->write_byte(KATANA_REG_RESET, 0x01)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
delay(RESET_SETTLE_DELAY_MS);
// MUTE_STREAM is the gate the Linux driver closes between streams; ESPHome has no such
// notion, so it stays open and mute is only ever driven through MUTE.
if (!this->write_byte(KATANA_REG_FORMAT, this->format_) || !this->write_byte(KATANA_REG_COMMAND, 0x00) ||
!this->write_byte(KATANA_REG_DSP_PROGRAM, this->filter_shape_) ||
!this->write_byte(KATANA_REG_DEEMPHASIS, this->deemphasis_mode_) ||
!this->write_byte(KATANA_REG_DOP, this->dop_enabled_ ? 1 : 0) || !this->write_volume_() ||
!this->write_byte(KATANA_REG_MUTE, this->is_muted_ ? 1 : 0) || !this->write_byte(KATANA_REG_MUTE_STREAM, 0x00)) {
this->mark_failed(LOG_STR("Failed to configure the control logic"));
}
}
void ES9038Q2MKatana::dump_config() {
ESP_LOGCONFIG(TAG,
"ES9038Q2M Katana:\n"
" Filter shape: %s\n"
" De-emphasis: %s\n"
" Format register: 0x%02X\n"
" DoP: %s",
LOG_STR_ARG(filter_shape_to_str(this->filter_shape_)),
LOG_STR_ARG(deemphasis_to_str(this->deemphasis_mode_)), this->format_, ONOFF(this->dop_enabled_));
LOG_I2C_DEVICE(this);
if (this->is_failed())
return;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
for (uint8_t reg = KATANA_REG_CHIP_ID; reg <= KATANA_REG_MUTE_STREAM; reg++) {
uint8_t value = 0;
if (this->read_byte(reg, &value)) {
ESP_LOGV(TAG, " Register 0x%02X: 0x%02X", reg, value);
} else {
ESP_LOGV(TAG, " Register 0x%02X: read failed", reg);
}
}
#endif
}
// Both channel registers follow ESPHome's single master volume.
bool ES9038Q2MKatana::write_volume_() {
return this->write_byte(KATANA_REG_VOLUME_1, this->volume_reg_) &&
this->write_byte(KATANA_REG_VOLUME_2, this->volume_reg_);
}
// Records the outcome of a runtime write; the cached state only changes on success.
bool ES9038Q2MKatana::check_write_(bool ok) {
if (ok) {
this->status_clear_warning();
} else {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
}
return ok;
}
bool ES9038Q2MKatana::set_volume(float volume) {
if (this->is_failed())
return false;
volume = clamp(volume, 0.0f, 1.0f);
const uint8_t previous = this->volume_reg_;
float attenuation = KATANA_VOLUME_FULL_ATTENUATION;
if (volume >= KATANA_VOLUME_MIDPOINT) {
attenuation = KATANA_VOLUME_MIDPOINT_STEPS * (1.0f - volume) / (1.0f - KATANA_VOLUME_MIDPOINT);
} else if (volume > 0.0f) {
attenuation = KATANA_VOLUME_MIDPOINT_STEPS - KATANA_VOLUME_CUBIC_SCALE * log10f(volume / KATANA_VOLUME_MIDPOINT);
}
this->volume_reg_ =
static_cast<uint8_t>(lroundf(clamp(attenuation, 0.0f, static_cast<float>(KATANA_VOLUME_FULL_ATTENUATION))));
if (this->check_write_(this->write_volume_()))
return true;
this->volume_reg_ = previous;
return false;
}
float ES9038Q2MKatana::volume() {
if (this->volume_reg_ == KATANA_VOLUME_FULL_ATTENUATION)
return 0.0f;
if (this->volume_reg_ <= KATANA_VOLUME_MIDPOINT_STEPS)
return 1.0f - this->volume_reg_ * (1.0f - KATANA_VOLUME_MIDPOINT) / KATANA_VOLUME_MIDPOINT_STEPS;
return KATANA_VOLUME_MIDPOINT *
powf(10.0f, (KATANA_VOLUME_MIDPOINT_STEPS - this->volume_reg_) / KATANA_VOLUME_CUBIC_SCALE);
}
bool ES9038Q2MKatana::set_mute_state_(bool mute_state) {
if (this->is_failed())
return false;
if (!this->check_write_(this->write_byte(KATANA_REG_MUTE, mute_state ? 1 : 0)))
return false;
this->is_muted_ = mute_state;
return true;
}
} // namespace esphome::es9038q2m_katana
@@ -1,62 +0,0 @@
#pragma once
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::es9038q2m_katana {
// DSP program numbers of the Katana control interface; there is no program 5.
enum FilterShape : uint8_t {
FILTER_SHAPE_LINEAR_PHASE_FAST = 0,
FILTER_SHAPE_LINEAR_PHASE_SLOW = 1,
FILTER_SHAPE_MIN_PHASE_FAST = 2,
FILTER_SHAPE_MIN_PHASE_SLOW = 3,
FILTER_SHAPE_APODIZING = 4,
FILTER_SHAPE_HYBRID = 6,
FILTER_SHAPE_BRICK_WALL = 7,
};
enum DeemphasisMode : uint8_t {
DEEMPHASIS_BYPASS = 0,
DEEMPHASIS_32KHZ = 1,
DEEMPHASIS_44_1KHZ = 2,
DEEMPHASIS_48KHZ = 3,
};
// Boards that expose an ES9038Q2M through the Allo Katana control logic at I2C address 0x30.
// Ported from the Linux driver sound/soc/bcm/allo-katana-codec.c; the DAC's own registers
// are never written, the control logic applies the configuration itself.
class ES9038Q2MKatana : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
// Cubic below 50%, then 0.5 dB per 1% above, meeting at -25 dB.
// Zero selects full attenuation (-127.5 dB).
bool set_volume(float volume) override;
float volume() override;
bool set_mute_off() override { return this->set_mute_state_(false); }
bool set_mute_on() override { return this->set_mute_state_(true); }
bool is_muted() override { return this->is_muted_; }
void set_filter_shape(FilterShape shape) { this->filter_shape_ = shape; }
void set_format(uint8_t format) { this->format_ = format; }
void set_deemphasis_mode(DeemphasisMode mode) { this->deemphasis_mode_ = mode; }
void set_dop_enabled(bool enabled) { this->dop_enabled_ = enabled; }
protected:
bool set_mute_state_(bool mute_state);
bool write_volume_();
bool check_write_(bool ok);
FilterShape filter_shape_{FILTER_SHAPE_APODIZING};
DeemphasisMode deemphasis_mode_{DEEMPHASIS_BYPASS};
uint8_t format_{0};
// Attenuation in 0.5 dB steps: 0 is loudest, 255 quietest. Starts 20 dB down.
uint8_t volume_reg_{0x28};
bool dop_enabled_{false};
};
} // namespace esphome::es9038q2m_katana
+4 -1
View File
@@ -243,7 +243,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, hdmi_cec, opentherm, Arduino BLE libs
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs
"esp_driver_i2c", # I2C driver - re-included by i2c; esp32-camera pulls it back itself
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_ledc", # LEDC PWM driver - re-included by ledc; esp32-camera pulls it back itself
@@ -3044,6 +3044,9 @@ async def to_code(config):
"_ZSt25__throw_bad_function_callv",
]:
cg.add_build_flag(f"-Wl,--wrap={mangled}")
# Replace operator new so nothrow new returns nullptr. See operator_new.cpp.
# --undefined pulls it from libsrc.a before libstdc++ (_Znwj: 32 bit size_t on every ESP32).
cg.add_build_flag("-Wl,--undefined=_Znwj")
# Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r
# (~11 KB). See printf_stubs.cpp for implementation.
+92
View File
@@ -0,0 +1,92 @@
/*
* Replaces operator new for ESP-IDF builds without C++ exceptions. libstdc++'s nothrow forms call the
* throwing form and catch std::bad_alloc, but the throw aborts here, so they abort instead of returning
* nullptr. All forms stay in this file: --undefined=_Znwj pulls it in ahead of libstdc++.
*/
#include "esphome/core/defines.h"
#ifdef USE_ESP_IDF
#include <sdkconfig.h>
#ifndef CONFIG_COMPILER_CXX_EXCEPTIONS
#include <cstdlib>
#include <new>
#include <type_traits>
#include "esp_system.h"
#ifdef __cpp_exceptions
#error "CONFIG_COMPILER_CXX_EXCEPTIONS no longer matches -fexceptions"
#endif
// libstdc++'s aligned delete calls free() only when built with one of these
#if !defined(_GLIBCXX_HAVE_ALIGNED_ALLOC) && !defined(_GLIBCXX_HAVE_POSIX_MEMALIGN) && !defined(_GLIBCXX_HAVE_MEMALIGN)
#error "libstdc++'s aligned operator delete no longer frees with free()"
#endif
#ifndef CLANG_TIDY // clang-tidy parses Xtensa with a 64 bit host target
// --undefined=_Znwj in __init__.py relies on this mangling
static_assert(std::is_same_v<std::size_t, unsigned int>, "update --undefined=_Znwj");
#endif
namespace esphome::esp32 {} // namespace esphome::esp32
namespace {
// Standard new_handler retry loop; returns nullptr once no handler is installed
template<typename Alloc> void *alloc_or_null(std::size_t size, Alloc alloc) {
// malloc(0) may return NULL, but operator new must return a unique pointer
if (size == 0)
size = 1;
for (;;) {
void *ptr = alloc(size);
if (ptr != nullptr)
return ptr;
std::new_handler handler = std::get_new_handler();
if (handler == nullptr)
return nullptr;
handler();
}
}
void *abort_if_null(void *ptr) {
if (ptr == nullptr)
esp_system_abort("std::bad_alloc");
return ptr;
}
void *plain_alloc(std::size_t size) {
return alloc_or_null(size, [](std::size_t n) { return std::malloc(n); }); // NOLINT(cppcoreguidelines-no-malloc)
}
// IDF's aligned_alloc accepts any size (no C11 multiple of alignment rule) and pairs with free(),
// which libstdc++'s aligned delete uses (checked above)
void *aligned_alloc_or_null(std::size_t size, std::align_val_t align) {
return alloc_or_null(size, [align](std::size_t n) { return ::aligned_alloc(static_cast<std::size_t>(align), n); });
}
} // namespace
// libstdc++'s operator delete (free) already matches these
// NOLINTBEGIN(cert-dcl54-cpp,misc-new-delete-overloads)
void *operator new(std::size_t size) { return abort_if_null(plain_alloc(size)); }
void *operator new[](std::size_t size) { return abort_if_null(plain_alloc(size)); }
void *operator new(std::size_t size, const std::nothrow_t & /*tag*/) noexcept { return plain_alloc(size); }
void *operator new[](std::size_t size, const std::nothrow_t & /*tag*/) noexcept { return plain_alloc(size); }
void *operator new(std::size_t size, std::align_val_t align) {
return abort_if_null(aligned_alloc_or_null(size, align));
}
void *operator new[](std::size_t size, std::align_val_t align) {
return abort_if_null(aligned_alloc_or_null(size, align));
}
void *operator new(std::size_t size, std::align_val_t align, const std::nothrow_t & /*tag*/) noexcept {
return aligned_alloc_or_null(size, align);
}
void *operator new[](std::size_t size, std::align_val_t align, const std::nothrow_t & /*tag*/) noexcept {
return aligned_alloc_or_null(size, align);
}
// NOLINTEND(cert-dcl54-cpp,misc-new-delete-overloads)
#endif // CONFIG_COMPILER_CXX_EXCEPTIONS
#endif // USE_ESP_IDF
@@ -300,7 +300,7 @@ void BLEClientBase::log_warning_(const char *message) {
esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {};
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
+10 -101
View File
@@ -44,14 +44,11 @@ const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
}
#endif
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 60000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
// Milliseconds for data transfer. Covers the lwIP retransmit run seen in
// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits
// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000;
// A handshake this old gives way to a waiting connection: a client retrying over a lossy link otherwise queues
// behind its own stalled attempt until both time out
static constexpr uint32_t OTA_HANDSHAKE_PREEMPT = 10000;
static constexpr uint32_t OTA_PROGRESS_INTERVAL_MS = 1000;
static constexpr size_t OTA_SIZE_FIELD_BYTES = 4; // sizes on the wire are 4 bytes MSB first
@@ -202,9 +199,6 @@ static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_DEFLATE = 0x10;
#ifdef ESPHOME_OTA_UDP
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_UDP = 0x20;
#endif
// Noise needs the extended protocol: the prologue binds the 2-byte feature ack
static constexpr uint8_t CLIENT_NOISE_FEATURES =
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
@@ -214,10 +208,6 @@ static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
// Raw deflate, window <= OTA_INFLATE_WINDOW_SIZE. Binding once offered: the
// client must then send the image size frame and a deflate stream.
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_DEFLATE = 0x08;
#ifdef ESPHOME_OTA_UDP
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_UDP = 0x10;
static constexpr uint8_t UDP_REQUEST_V1 = 0x01; // espota2.UDP_REQUEST
#endif
#ifdef USE_OTA_ENCRYPTION
inline bool ESPHomeOTAComponent::noise_offered_() const {
@@ -276,12 +266,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
this->cleanup_connection_();
return;
}
// Only unauthenticated handshakes reach here; the data phase blocks inside handle_data_
if (now - this->client_connect_time_ > OTA_HANDSHAKE_PREEMPT && this->server_->ready()) {
ESP_LOGD(TAG, "Newer connection waiting");
this->cleanup_connection_();
return;
}
switch (this->ota_state_) {
case OTAState::MAGIC_READ: {
@@ -345,9 +329,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS;
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
#ifdef ESPHOME_OTA_UDP
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_UDP;
#endif
#ifdef USE_OTA_PARTITIONS
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
#endif
@@ -502,10 +483,6 @@ void ESPHomeOTAComponent::handle_data_() {
char *sbuf = reinterpret_cast<char *>(buf);
size_t image_size;
ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP;
#ifdef ESPHOME_OTA_UDP
UdpLink udp_link;
this->udp_ = nullptr; // client_connect_time_ is done with
#endif
// Set socket timeouts and blocking mode (see strategy table above)
struct timeval tv;
@@ -521,38 +498,6 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge auth OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
#ifdef ESPHOME_OTA_UDP
// The server always offers UDP on the extended protocol
if (this->extended_proto_() && (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_UDP) != 0) {
// Whether the client wants UDP (0: its link looks good), answered with our token
if (!this->data_readall_(buf, 1)) {
if (this->client_left_before_start_())
return;
this->log_read_error_(LOG_STR("UDP request"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
// 1 is this UDP format; other values are free for later ones, which we decline (a zero reply keeps TCP)
if (buf[0] == UDP_REQUEST_V1) {
this->udp_open_(udp_link);
if (udp_link.reply[0] == 0) {
ESP_LOGW(TAG, "UDP unavailable, using TCP");
}
}
if (!this->data_write_(udp_link.reply, sizeof(udp_link.reply)))
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
if (udp_link.reply[0] != 0) {
this->udp_ = &udp_link;
if (!this->udp_start_()) {
// Nothing started yet: just drop the session, which the client retries as a network failure
this->log_read_error_(LOG_STR("UDP start"));
this->cleanup_connection_();
return;
}
}
}
#endif
if (this->extended_proto_()) {
// Read ota type, 1 byte
if (!this->data_readall_(buf, 1)) {
@@ -653,17 +598,11 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge Update end OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
#ifdef ESPHOME_OTA_UDP
// UDP never reports a close: wait only until the client has our responses, not for its final ack
if (this->udp_ != nullptr) {
this->udp_linger_();
} else
#endif
// Read ACK
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
this->log_read_error_(LOG_STR("ack"));
// do not go to error, this is not fatal
}
// Read ACK
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
this->log_read_error_(LOG_STR("ack"));
// do not go to error, this is not fatal
}
this->cleanup_connection_();
delay(10);
@@ -684,9 +623,6 @@ void ESPHomeOTAComponent::handle_data_() {
error:
this->data_write_byte_(static_cast<uint8_t>(error_code));
#ifdef ESPHOME_OTA_UDP
this->udp_linger_();
#endif
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
// Always call abort() unconditionally: backends register external partitions before
@@ -715,7 +651,7 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
return false;
}
ssize_t read = this->transport_read_(buf + at, len - at);
ssize_t read = this->client_->read(buf + at, len - at);
if (read == -1) {
const int err = errno;
if (!this->would_block_(err)) {
@@ -740,10 +676,6 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
return true;
}
bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
#ifdef ESPHOME_OTA_UDP
if (this->udp_ != nullptr)
return this->udp_send_(buf, len);
#endif
uint32_t start = millis();
uint32_t at = 0;
while (len - at > 0) {
@@ -796,20 +728,7 @@ void ESPHomeOTAComponent::log_read_error_(const LogString *what) {
void ESPHomeOTAComponent::log_start_(const LogString *phase) {
char peername[socket::SOCKADDR_STR_LEN];
#ifdef ESPHOME_OTA_UDP
if (this->client_ == nullptr) {
// A committed UDP session has already dropped TCP
#ifdef ESPHOME_OTA_UDP_LWIP
ipaddr_ntoa_r(&this->udp_->peer_ip, peername, sizeof(peername));
#else
socket::format_sockaddr_to(reinterpret_cast<const struct sockaddr *>(&this->udp_->peer), this->udp_->peer_len,
peername);
#endif
} else
#endif
{
this->client_->getpeername_to(peername);
}
this->client_->getpeername_to(peername);
ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername);
}
@@ -921,7 +840,7 @@ ssize_t ESPHomeOTAComponent::receive_data_(uint8_t *buf, DataTransfer &xfer) {
break;
}
#endif
read = this->transport_read_(buf, requested);
read = this->client_->read(buf, requested);
if (read > 0)
break;
if (read == 0) {
@@ -955,11 +874,6 @@ ssize_t ESPHomeOTAComponent::receive_data_(uint8_t *buf, DataTransfer &xfer) {
void ESPHomeOTAComponent::send_chunk_acks_(DataTransfer &xfer) {
#if USE_OTA_VERSION == 2
#ifdef ESPHOME_OTA_UDP
// UDP acks every message itself
if (this->udp_ != nullptr)
return;
#endif
while (xfer.acknowledged + OTA_BLOCK_SIZE <= xfer.total ||
(xfer.total == xfer.ota_size && xfer.acknowledged < xfer.ota_size)) {
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
@@ -969,12 +883,7 @@ void ESPHomeOTAComponent::send_chunk_acks_(DataTransfer &xfer) {
}
void ESPHomeOTAComponent::cleanup_connection_() {
#ifdef ESPHOME_OTA_UDP
this->udp_ = nullptr; // the link itself lives on handle_data_'s stack
#endif
// Already gone once a UDP session committed
if (this->client_ != nullptr)
this->client_->close();
this->client_->close();
this->client_ = nullptr;
this->client_connect_time_ = 0;
this->handshake_buf_pos_ = 0;
+5 -103
View File
@@ -15,25 +15,6 @@
#include "esphome/core/preferences.h"
#include "esphome/core/hash_base.h"
// UDP data phase: raw lwIP needs a main loop lock (or a single thread); the host polls a socket instead
#ifdef USE_ESP32
#include <sdkconfig.h>
#endif
#if defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || \
(defined(USE_ESP32) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING))
#define ESPHOME_OTA_UDP
#define ESPHOME_OTA_UDP_LWIP
#include "lwip/ip_addr.h"
#include "lwip/pbuf.h"
#include "lwip/udp.h"
#if defined(USE_LIBRETINY) && !LWIP_TCPIP_CORE_LOCKING
#undef ESPHOME_OTA_UDP
#undef ESPHOME_OTA_UDP_LWIP
#endif
#elif defined(USE_HOST)
#define ESPHOME_OTA_UDP
#endif
namespace esphome {
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
@@ -84,69 +65,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
protected:
void handle_handshake_();
void handle_data_();
#ifdef ESPHOME_OTA_UDP
// UDP transport for the data phase; wire format in ota_esphome_udp.cpp
static constexpr size_t UDP_OUT_SIZE = 64; // our unacked responses: at most three encrypted ones
// Messages the device keeps unread: like TCP's 4 segment window, so no more memory than TCP holds
static constexpr uint8_t UDP_WINDOW = 4;
#ifdef ESPHOME_OTA_UDP_LWIP
using UdpSlot = struct pbuf *;
#else
using UdpSlot = struct OtaUdpDatagram *;
#endif
// Lives on handle_data_'s stack; the receive callback shares it under the lwIP lock
struct UdpLink {
~UdpLink();
void release(); // closes the socket and frees what is queued
#ifdef ESPHOME_OTA_UDP_LWIP
struct udp_pcb *pcb{nullptr};
ip_addr_t peer_ip;
#else
std::unique_ptr<socket::Socket> sock;
struct sockaddr_storage peer; // the TCP peer; its port is replaced by peer_port
socklen_t peer_len{0};
#endif
uint16_t peer_port{0}; // learned from the client's datagrams
// The session token, our answer to the client: top bit clear, so never an error code; all zero declines
uint8_t reply[4]{};
UdpSlot queue[UDP_WINDOW]{}; // slot seq % UDP_WINDOW for seq in [consumed, consumed + UDP_WINDOW)
uint16_t received{0}; // every seq before it is here
uint16_t consumed{0}; // the message being read
uint16_t read_offset{0}; // payload bytes of it already read
uint16_t out_base{0}; // stream offset of out[0]
uint8_t out_len{0};
bool committed{false}; // the client heard us over UDP
uint8_t out[UDP_OUT_SIZE];
};
// Fills link.reply on success; leaves it all zero (declined) otherwise
void udp_open_(UdpLink &link);
// Waits for the client to commit to UDP or fall back to TCP; false on failure
bool udp_start_();
// Like a blocking socket read with SO_RCVTIMEO: up to len bytes, or -1 with EWOULDBLOCK
ssize_t udp_read_(uint8_t *buf, size_t len);
bool udp_send_(const uint8_t *data, size_t len);
// In a UDP session, keeps answering until the client has our responses (an error code), briefly
void udp_linger_();
// A datagram from the client's IP; caller holds the lwIP lock (or runs in the receive callback)
static void udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port);
// Caller holds the lwIP lock (or runs in the receive callback)
static void udp_send_ack(UdpLink &link, uint16_t prompted_by);
#ifdef ESPHOME_OTA_UDP_LWIP
static void udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, uint16_t port);
void udp_poll_() {}
#else
void udp_poll_(); // hands what the socket received to udp_on_datagram
#endif
#endif
// Data phase only (udp_ shares storage with the handshake's connect time): TCP or, once committed, the UDP stream
inline ssize_t transport_read_(uint8_t *buf, size_t len) {
#ifdef ESPHOME_OTA_UDP
if (this->udp_ != nullptr)
return this->udp_read_(buf, len);
#endif
return this->client_->read(buf, len);
}
#ifdef USE_OTA_PASSWORD
static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
bool handle_auth_send_();
@@ -157,6 +75,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
#endif // USE_OTA_PASSWORD
bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len);
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
#ifdef USE_OTA_ENCRYPTION
// Heap-allocated only while an encrypted OTA session is active.
@@ -186,21 +105,18 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
bool noise_readall_(uint8_t *buf, size_t len);
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
static constexpr size_t NOISE_MAX_RESPONSE = 4; // the UDP token
bool noise_write_(const uint8_t *data, size_t len);
bool noise_write_byte_(uint8_t byte);
#endif // USE_OTA_ENCRYPTION
// Data-phase I/O dispatch: through the noise transport when a session is
// active, straight to the socket otherwise.
// len is at most NOISE_MAX_RESPONSE
inline bool data_write_(const uint8_t *data, size_t len) {
inline bool data_write_byte_(uint8_t byte) {
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr)
return this->noise_write_(data, len);
return this->noise_write_byte_(byte);
#endif
return this->writeall_(data, len);
return this->write_byte_(byte);
}
inline bool data_write_byte_(uint8_t byte) { return this->data_write_(&byte, 1); }
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
inline bool data_readall_(uint8_t *buf, size_t len) {
#ifdef USE_OTA_ENCRYPTION
@@ -284,21 +200,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
std::unique_ptr<socket::Socket> client_;
ota::OTABackendPtr backend_;
#ifdef ESPHOME_OTA_UDP_LWIP
union {
uint32_t client_connect_time_{0}; // handshake states
UdpLink *udp_; // data phase (never both), points into handle_data_'s stack
};
static_assert(sizeof(UdpLink *) == sizeof(uint32_t), "clearing client_connect_time_ must clear udp_");
#else
uint32_t client_connect_time_{0};
#ifdef ESPHOME_OTA_UDP
UdpLink *udp_{nullptr}; // data phase, points into handle_data_'s stack
#endif
#endif
#if defined(ESPHOME_OTA_UDP) && defined(USE_OTA_ENCRYPTION)
static_assert(sizeof(UdpLink::reply) <= NOISE_MAX_RESPONSE, "the token goes out through noise_write_");
#endif
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
// Buffer size for OTA data transfer. The upload client derives its maximum
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
@@ -266,15 +266,13 @@ ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
}
/// Blocking write of a short response (at most NOISE_MAX_RESPONSE bytes) as one encrypted frame.
bool ESPHomeOTAComponent::noise_write_(const uint8_t *data, size_t len) {
uint8_t frame[noise::FRAME_HEADER_SIZE + NOISE_MAX_RESPONSE + noise::MAC_SIZE];
if (len > NOISE_MAX_RESPONSE)
return false;
memcpy(frame + noise::FRAME_HEADER_SIZE, data, len);
/// Blocking write of one response byte as an encrypted frame.
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
frame[noise::FRAME_HEADER_SIZE] = byte;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, len, len + noise::MAC_SIZE);
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
if (err != 0) {
ESP_LOGW(TAG, "Encrypt: %d", err);
@@ -1,375 +0,0 @@
#include "ota_esphome.h"
#if defined(USE_OTA) && defined(ESPHOME_OTA_UDP)
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef ESPHOME_OTA_UDP_LWIP
#include "lwip/inet.h"
#ifdef USE_LIBRETINY
#include "lwip/tcpip.h"
#endif
#endif
#include <algorithm>
#include <cstring>
#include <memory>
/*
* UDP data phase: the same byte stream as TCP, cut into numbered datagrams.
* client -> device [type | COMMITTED][token u32][seq u16][device bytes received u16][payload]
* device -> client [ACK][token u32][received u16][seq that prompted it u16][stream offset u16][window u8][our bytes]
* The lwIP callback acks each datagram on arrival, so flash writes never look like loss, and keeps messages in its
* UDP_WINDOW slots until the OTA loop reads them. "window": free slots in the high nibble, and in the low nibble bit k
* set when received + k is already here (like TCP SACK).
*/
namespace esphome {
ESPHOME_LOG_TAG(TAG, "esphome.ota");
// PING (0x02) and PROBE (0x03) only prompt an ACK
static constexpr uint8_t UDP_MSG_DATA = 0x01;
static constexpr uint8_t UDP_FLAG_COMMITTED = 0x80;
static constexpr uint8_t UDP_MSG_ACK = 0x40;
static constexpr size_t UDP_HEADER_SIZE = 9;
static constexpr size_t UDP_ACK_HEADER_SIZE = 12;
// The client's 60 s wait for the token over a lossy TCP link, plus its 15 s probe
static constexpr uint32_t UDP_COMMIT_TIMEOUT = 80000;
// Like the TCP socket's SO_RCVTIMEO in handle_data_, so readall_ treats both alike
static constexpr uint32_t UDP_READ_WAIT_MS = 2000;
// How often the commit wait looks at the UDP link between reads of the TCP fallback byte
static constexpr uint32_t UDP_COMMIT_POLL_MS = 100;
static constexpr uint32_t UDP_LINGER_MS = 3000;
#ifdef ESPHOME_OTA_UDP_LWIP
#ifdef USE_LIBRETINY
// LibreTiny's LwIPLock is a no-op; take the core lock directly (the header drops UDP without one)
struct UdpLock {
UdpLock() { LOCK_TCPIP_CORE(); }
~UdpLock() { UNLOCK_TCPIP_CORE(); }
};
#else
using UdpLock = LwIPLock;
#endif
static uint16_t slot_len(struct pbuf *p) { return p->tot_len; }
static uint16_t slot_copy(struct pbuf *p, void *dst, uint16_t len, uint16_t offset) {
return pbuf_copy_partial(p, dst, len, offset);
}
static void slot_free(struct pbuf *p) { pbuf_free(p); }
#else
// The host has no lwIP thread: the OTA loop drains the socket itself. Empty bodies, not = default, so clang-tidy
// does not flag the guards as unused
struct UdpLock {
UdpLock() {}
~UdpLock() {}
};
struct OtaUdpDatagram {
uint16_t len;
uint8_t data[1500];
};
static uint16_t slot_len(OtaUdpDatagram *p) { return p->len; }
static uint16_t slot_copy(OtaUdpDatagram *p, void *dst, uint16_t len, uint16_t offset) {
const uint16_t n = std::min<uint16_t>(len, p->len - offset);
memcpy(dst, p->data + offset, n);
return n;
}
static void slot_free(OtaUdpDatagram *p) { delete p; }
#endif
static void put_u16(uint8_t *at, uint16_t value) {
at[0] = value >> 8;
at[1] = value & 0xFF;
}
ESPHomeOTAComponent::UdpLink::~UdpLink() { this->release(); }
void ESPHomeOTAComponent::UdpLink::release() {
UdpLock lock;
#ifdef ESPHOME_OTA_UDP_LWIP
if (this->pcb != nullptr)
udp_remove(this->pcb);
this->pcb = nullptr;
#else
this->sock = nullptr;
#endif
for (auto &p : this->queue) {
if (p != nullptr)
slot_free(p);
p = nullptr;
}
}
void ESPHomeOTAComponent::udp_send_ack(UdpLink &link, uint16_t prompted_by) {
const uint16_t len = UDP_ACK_HEADER_SIZE + link.out_len;
#ifdef ESPHOME_OTA_UDP_LWIP
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
if (p == nullptr)
return; // Like a lost ACK: the client asks again
auto *ack = static_cast<uint8_t *>(p->payload);
#else
uint8_t ack[UDP_ACK_HEADER_SIZE + UDP_OUT_SIZE];
#endif
ack[0] = UDP_MSG_ACK;
memcpy(ack + 1, link.reply, 4);
put_u16(ack + 5, link.received);
put_u16(ack + 7, prompted_by);
put_u16(ack + 9, link.out_base);
uint8_t window = (UDP_WINDOW - static_cast<uint16_t>(link.received - link.consumed)) << 4;
for (uint8_t k = 1; k < UDP_WINDOW; k++) {
if (link.queue[(link.received + k) % UDP_WINDOW] != nullptr)
window |= 1 << k;
}
ack[11] = window;
memcpy(ack + UDP_ACK_HEADER_SIZE, link.out, link.out_len);
// Best effort: a lost ACK is repeated for the client's next datagram
#ifdef ESPHOME_OTA_UDP_LWIP
udp_sendto(link.pcb, p, &link.peer_ip, link.peer_port);
pbuf_free(p);
#else
struct sockaddr_storage to = link.peer;
if (to.ss_family == AF_INET6) {
reinterpret_cast<sockaddr_in6 *>(&to)->sin6_port = htons(link.peer_port);
} else {
reinterpret_cast<sockaddr_in *>(&to)->sin_port = htons(link.peer_port);
}
link.sock->sendto(ack, len, 0, reinterpret_cast<const struct sockaddr *>(&to), link.peer_len);
#endif
}
void ESPHomeOTAComponent::udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port) {
uint8_t hdr[UDP_HEADER_SIZE];
if (slot_copy(p, hdr, UDP_HEADER_SIZE, 0) != UDP_HEADER_SIZE || memcmp(hdr + 1, link.reply, 4) != 0) {
slot_free(p);
return;
}
// The token proves the sender; answer the port it arrived from, which NAT may have rewritten
link.peer_port = port;
link.committed |= (hdr[0] & UDP_FLAG_COMMITTED) != 0;
// Drop the responses the client already has
const uint16_t acked = encode_uint16(hdr[7], hdr[8]) - link.out_base;
if (acked != 0 && acked <= link.out_len) {
link.out_len -= acked;
memmove(link.out, link.out + acked, link.out_len);
link.out_base += acked;
}
const uint16_t seq = encode_uint16(hdr[5], hdr[6]);
UdpSlot &slot = link.queue[seq % UDP_WINDOW];
if ((hdr[0] & ~UDP_FLAG_COMMITTED) == UDP_MSG_DATA && slot_len(p) > UDP_HEADER_SIZE &&
static_cast<uint16_t>(seq - link.consumed) < UDP_WINDOW && slot == nullptr) {
slot = p;
while (static_cast<uint16_t>(link.received - link.consumed) < UDP_WINDOW &&
link.queue[link.received % UDP_WINDOW] != nullptr)
link.received++;
} else {
slot_free(p);
}
udp_send_ack(link, seq);
}
#ifdef ESPHOME_OTA_UDP_LWIP
// lwIP context: the tcpip thread (ESP32, LibreTiny), SYS (ESP8266) or an IRQ (RP2040)
void ESPHomeOTAComponent::udp_recv_cb(void *arg, struct udp_pcb * /*pcb*/, struct pbuf *p, const ip_addr_t *addr,
uint16_t port) {
auto &link = *static_cast<UdpLink *>(arg);
if (!ip_addr_cmp(addr, &link.peer_ip)) {
pbuf_free(p);
return;
}
udp_on_datagram(link, p, port);
}
#else
static bool same_ip(const struct sockaddr_storage &a, const struct sockaddr_storage &b) {
uint32_t ipv4_a, ipv4_b;
const bool v4_a = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&a), &ipv4_a);
const bool v4_b = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&b), &ipv4_b);
if (v4_a || v4_b)
return v4_a && v4_b && ipv4_a == ipv4_b;
return a.ss_family == AF_INET6 && b.ss_family == AF_INET6 &&
memcmp(&reinterpret_cast<const sockaddr_in6 *>(&a)->sin6_addr,
&reinterpret_cast<const sockaddr_in6 *>(&b)->sin6_addr, sizeof(in6_addr)) == 0;
}
void ESPHomeOTAComponent::udp_poll_() {
UdpLink &link = *this->udp_;
for (;;) {
uint8_t buf[sizeof(OtaUdpDatagram::data)];
struct sockaddr_storage from;
socklen_t from_len = sizeof(from);
const ssize_t len = link.sock->recvfrom(buf, sizeof(buf), reinterpret_cast<struct sockaddr *>(&from), &from_len);
if (len < 0)
return;
if (!same_ip(from, link.peer))
continue;
auto *datagram = new OtaUdpDatagram; // NOLINT(cppcoreguidelines-owning-memory)
datagram->len = len;
memcpy(datagram->data, buf, len);
const uint16_t port = from.ss_family == AF_INET6 ? ntohs(reinterpret_cast<sockaddr_in6 *>(&from)->sin6_port)
: ntohs(reinterpret_cast<sockaddr_in *>(&from)->sin_port);
udp_on_datagram(link, datagram, port);
}
}
#endif
void ESPHomeOTAComponent::udp_open_(UdpLink &link) {
struct sockaddr_storage peer;
socklen_t peer_len = sizeof(peer);
if (this->client_->getpeername(reinterpret_cast<struct sockaddr *>(&peer), &peer_len) != 0)
return;
uint8_t token[4];
if (!random_bytes(token, sizeof(token)))
return;
#ifdef ESPHOME_OTA_UDP_LWIP
uint32_t ipv4;
if (socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&peer), &ipv4)) {
ip_addr_set_ip4_u32(&link.peer_ip, ipv4);
} else {
#if USE_NETWORK_IPV6 && LWIP_IPV6
inet6_addr_to_ip6addr(ip_2_ip6(&link.peer_ip), &reinterpret_cast<const sockaddr_in6 *>(&peer)->sin6_addr);
IP_SET_TYPE_VAL(link.peer_ip, IPADDR_TYPE_V6);
#else
return;
#endif
}
UdpLock lock;
#if LWIP_IPV6
link.pcb = udp_new_ip_type(IPADDR_TYPE_ANY);
#else
link.pcb = udp_new();
#endif
// On failure the reply stays all zero and ~UdpLink removes the pcb
if (link.pcb == nullptr || udp_bind(link.pcb, IP_ANY_TYPE, this->port_) != ERR_OK)
return;
udp_recv(link.pcb, udp_recv_cb, &link);
#else
link.peer = peer;
link.peer_len = peer_len;
// Same domain as the listener, so the peer address from getpeername fits
link.sock = socket::socket_ip(SOCK_DGRAM, IPPROTO_UDP);
struct sockaddr_storage local;
const socklen_t local_len =
socket::set_sockaddr_any(reinterpret_cast<struct sockaddr *>(&local), sizeof(local), this->port_);
if (link.sock == nullptr || local_len == 0 ||
link.sock->bind(reinterpret_cast<struct sockaddr *>(&local), local_len) != 0 ||
link.sock->setblocking(false) != 0)
return;
#endif
// Below the error codes (0x80 and up) the client checks the first byte against, and never all zero
token[0] = (token[0] & 0x7F) | 0x01;
memcpy(link.reply, token, sizeof(token));
}
bool ESPHomeOTAComponent::udp_start_() {
// TCP now only carries a fallback byte; short read timeouts let the UDP link be checked in between
struct timeval tv {
0, UDP_COMMIT_POLL_MS * 1000
};
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
const uint32_t start = millis();
while (millis() - start < UDP_COMMIT_TIMEOUT) {
this->udp_poll_();
bool committed;
{
UdpLock lock;
committed = this->udp_->committed;
}
if (committed) {
// The client heard us over UDP, so TCP is done. Outside the lock: closing a socket waits on the tcpip thread
this->client_ = nullptr;
return true;
}
// A raw byte outside any Noise frame: the client heard nothing over UDP. Forging it only forces the ordinary
// TCP path, which dropping our datagrams forces anyway
uint8_t fallback;
const ssize_t read = this->client_->read(&fallback, 1);
if (read == 1) {
this->udp_->release();
this->udp_ = nullptr;
tv = {UDP_READ_WAIT_MS / 1000, 0};
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
return true;
}
if (read == 0) {
this->remote_closed_ = true;
return false;
}
if (!this->would_block_(errno))
return false;
App.feed_wdt();
}
return false;
}
ssize_t ESPHomeOTAComponent::udp_read_(uint8_t *buf, size_t len) {
UdpLink &link = *this->udp_;
const uint32_t start = millis();
size_t got = 0;
for (;;) {
this->udp_poll_();
UdpSlot p;
{
UdpLock lock;
p = link.consumed != link.received ? link.queue[link.consumed % UDP_WINDOW] : nullptr;
}
if (p == nullptr) {
if (got != 0)
return got;
if (millis() - start >= UDP_READ_WAIT_MS)
break;
this->yield_and_feed_watchdog_();
continue;
}
// The callback never touches the slot being read, so the copy runs without the lock
const uint16_t offset = UDP_HEADER_SIZE + link.read_offset;
const uint16_t n = slot_copy(p, buf + got, std::min<size_t>(len - got, slot_len(p) - offset), offset);
got += n;
link.read_offset += n;
if (UDP_HEADER_SIZE + link.read_offset >= slot_len(p)) {
UdpLock lock;
slot_free(p);
link.queue[link.consumed % UDP_WINDOW] = nullptr;
// A full window just opened: tell the client, which is holding back
const bool was_full = static_cast<uint16_t>(link.received - link.consumed) == UDP_WINDOW;
link.consumed++;
link.read_offset = 0;
if (was_full)
udp_send_ack(link, link.received - 1);
}
if (got == len)
return got;
}
errno = EWOULDBLOCK;
return -1;
}
bool ESPHomeOTAComponent::udp_send_(const uint8_t *data, size_t len) {
UdpLink &link = *this->udp_;
UdpLock lock;
if (link.out_len + len > UDP_OUT_SIZE)
return false;
memcpy(link.out + link.out_len, data, len);
link.out_len += len;
// Prompted by nothing new, so the client takes no round trip sample from it
udp_send_ack(link, link.received - 1);
return true;
}
void ESPHomeOTAComponent::udp_linger_() {
if (this->udp_ == nullptr)
return;
const uint32_t start = millis();
while (millis() - start < UDP_LINGER_MS) {
this->udp_poll_();
{
UdpLock lock;
if (this->udp_->out_len == 0)
return;
}
this->yield_and_feed_watchdog_();
}
}
} // namespace esphome
#endif // USE_OTA && ESPHOME_OTA_UDP
-38
View File
@@ -1,40 +1,2 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN
from esphome.types import ConfigType
CODEOWNERS = ["@remcom"]
DEPENDENCIES = ["i2c"]
DOMAIN = "fusb302b"
CONF_REQUEST_VOLTAGE = "request_voltage"
fusb302b_ns = cg.esphome_ns.namespace("fusb302b")
FUSB302B = fusb302b_ns.class_("FUSB302B", cg.Component, i2c.I2CDevice)
# USB PD fixed supply voltages are 5, 9, 12, 15 and 20 V; the highest one that does not exceed the
# requested voltage is chosen.
validate_voltage = cv.int_range(min=5, max=20)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(FUSB302B),
cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pullup_pin_schema,
cv.Required(CONF_REQUEST_VOLTAGE): validate_voltage,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x22)),
cv.only_on_esp32,
)
async def to_code(config: ConfigType) -> None:
interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
var = cg.new_Pvariable(config[CONF_ID], interrupt_pin)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_request_voltage(config[CONF_REQUEST_VOLTAGE]))
-415
View File
@@ -1,415 +0,0 @@
#include "fusb302b.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include "fusb302b_registers.h"
namespace esphome::fusb302b {
ESPHOME_LOG_TAG(TAG, "fusb302b");
// Task notification bits
static constexpr uint32_t NOTIFY_IRQ = 1 << 0;
static constexpr uint32_t NOTIFY_STOP = 1 << 1;
static constexpr uint32_t TASK_STACK_SIZE = 4096;
static constexpr UBaseType_t TASK_PRIORITY = 18;
static constexpr uint32_t STARTUP_DELAY_MS = 2000;
static constexpr uint32_t CC_SETTLE_MS = 5;
static constexpr uint32_t CC_POLL_INTERVAL_MS = 500;
static constexpr uint32_t PD_POLL_INTERVAL_MS = 100;
// Safety net in case an interrupt edge is missed while attached and idle
static constexpr uint32_t IDLE_POLL_INTERVAL_MS = 1000;
// Time to wait for the source to send its capabilities on its own after attach
static constexpr uint32_t SOURCE_CAP_WAIT_MS = 5000;
// Time to wait after each GET_SOURCE_CAP / soft reset before the next recovery step
static constexpr uint32_t SOURCE_CAP_RETRY_MS = 2500;
// Upper bound on back-to-back interrupt handling while INT_N stays asserted
static constexpr uint8_t MAX_IRQ_ROUNDS = 8;
// BC_LVL is read this many extra times to confirm a stable measurement
static constexpr uint8_t CC_STABLE_READS = 5;
static constexpr uint8_t CC_LEVEL_UNSTABLE = 0xFF;
void IRAM_ATTR FUSB302B::gpio_intr(FUSB302B *arg) {
TaskHandle_t handle = arg->task_.get_handle();
if (handle == nullptr)
return;
BaseType_t woken = pdFALSE;
xTaskNotifyFromISR(handle, NOTIFY_IRQ, eSetBits, &woken);
portYIELD_FROM_ISR(woken);
}
void FUSB302B::task_func(void *arg) { static_cast<FUSB302B *>(arg)->run_task_(); }
void FUSB302B::setup() {
uint8_t device_id = 0;
if (!this->read_byte(FUSB_DEVICE_ID, &device_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
const uint8_t version = device_id & FUSB_DEVICE_ID_VERSION_MASK;
if (version != FUSB_DEVICE_ID_VERSION_A && version != FUSB_DEVICE_ID_VERSION_B) {
ESP_LOGE(TAG, "Unknown device ID 0x%02X", device_id);
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Device ID 0x%02X (product %u, revision %u)", device_id, (device_id >> 2) & 0x3, device_id & 0x3);
if (!this->init_chip_()) {
this->mark_failed(LOG_STR("Initialization failed"));
return;
}
this->interrupt_pin_->setup();
// The task must exist before the interrupt is attached, since the ISR notifies it
if (!this->task_.create(FUSB302B::task_func, "fusb302b", TASK_STACK_SIZE, this, TASK_PRIORITY, false)) {
this->mark_failed(LOG_STR("Failed to create task"));
return;
}
this->interrupt_pin_->attach_interrupt(&FUSB302B::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
}
void FUSB302B::dump_config() {
ESP_LOGCONFIG(TAG,
"FUSB302B:\n"
" Request Voltage: %u V",
this->get_request_voltage());
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
}
void FUSB302B::on_shutdown() {
this->interrupt_pin_->detach_interrupt();
// The task parks itself instead of being deleted, so it can never be stopped in the middle of an I2C transfer
if (this->task_.is_created())
xTaskNotify(this->task_.get_handle(), NOTIFY_STOP, eSetBits);
}
void FUSB302B::run_task_() {
// Give the source time to settle after power-up before the first CC measurement
vTaskDelay(pdMS_TO_TICKS(STARTUP_DELAY_MS));
while (true) {
uint32_t wait_ms = CC_POLL_INTERVAL_MS;
if (this->attached_) {
const bool busy = this->waiting_for_source_caps_ || this->retry_request_pending_ || this->check_ams_();
wait_ms = busy ? PD_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
}
uint32_t bits = 0;
xTaskNotifyWait(0, UINT32_MAX, &bits, pdMS_TO_TICKS(wait_ms));
if (bits & NOTIFY_STOP) {
while (true)
vTaskSuspend(nullptr);
}
// INT_N is held low until the interrupt registers are read, so a new event that arrives while it is
// already low has no falling edge: keep servicing while the pin stays asserted.
if (bits & NOTIFY_IRQ)
this->handle_interrupt_();
for (uint8_t i = 0; i < MAX_IRQ_ROUNDS && !this->interrupt_pin_->digital_read(); i++)
this->handle_interrupt_();
if (!this->attached_) {
this->try_attach_();
continue;
}
this->check_transition_timeout_();
this->check_request_retry_();
this->check_source_caps_();
}
}
bool FUSB302B::init_chip_() {
// Software reset restores all registers to their defaults
if (!this->write_byte(FUSB_RESET, FUSB_RESET_SW_RES))
return false;
if (!this->reset_pd_())
return false;
// Unmask the interrupts used for PD messaging and VBUS changes
if (!this->write_byte(FUSB_MASK1, FUSB_MASK1_DEFAULT) || !this->write_byte(FUSB_MASKA, 0) ||
!this->write_byte(FUSB_MASKB, 0))
return false;
// Let the chip retransmit unacknowledged messages up to 3 times
uint8_t control3 = 0;
if (!this->read_byte(FUSB_CONTROL3, &control3))
return false;
control3 =
(control3 & ~FUSB_CONTROL3_N_RETRIES_MASK) | (3 << FUSB_CONTROL3_N_RETRIES_SHIFT) | FUSB_CONTROL3_AUTO_RETRY;
if (!this->write_byte(FUSB_CONTROL3, control3))
return false;
if (!this->write_byte(FUSB_POWER, FUSB_POWER_ALL))
return false;
return this->reset_pd_();
}
bool FUSB302B::reset_pd_() {
this->reset_protocol_();
// Writing CONTROL0 as a whole byte also clears INT_MASK, which is what enables the INT_N pin
return this->write_byte(FUSB_CONTROL0, FUSB_CONTROL0_TX_FLUSH) &&
this->write_byte(FUSB_CONTROL1, FUSB_CONTROL1_RX_FLUSH) && this->write_byte(FUSB_RESET, FUSB_RESET_PD_RESET);
}
bool FUSB302B::enable_auto_crc_() {
uint8_t switches1 = 0;
return this->read_byte(FUSB_SWITCHES1, &switches1) &&
this->write_byte(FUSB_SWITCHES1, switches1 | FUSB_SWITCHES1_AUTO_CRC);
}
void FUSB302B::handle_interrupt_() {
// STATUS0A, STATUS1A, INTERRUPTA, INTERRUPTB, STATUS0, STATUS1, INTERRUPT; reading clears the interrupts
uint8_t regs[7];
if (this->read_register(FUSB_STATUS0A, regs, sizeof(regs)) != i2c::ERROR_OK) {
this->enter_error_();
return;
}
const uint8_t interrupta = regs[2];
const uint8_t status0 = regs[4];
uint8_t status1 = regs[5];
const uint8_t interrupt = regs[6];
if ((interrupt & FUSB_INTERRUPT_I_VBUSOK) && !(status0 & FUSB_STATUS0_VBUSOK)) {
if (this->attached_)
this->detach_();
return;
}
if (interrupta & FUSB_INTERRUPTA_I_HARDRST) {
if (this->attached_)
this->handle_hard_reset_();
return;
}
if (interrupta & FUSB_INTERRUPTA_I_SOFTRST) {
ESP_LOGV(TAG, "Soft reset received");
}
if ((interrupta & FUSB_INTERRUPTA_I_RETRYFAIL) && this->attached_)
this->handle_transmit_failed_();
if (!this->attached_)
return;
PdMsg msg;
while (!(status1 & FUSB_STATUS1_RX_EMPTY)) {
bool is_sop = false;
if (!this->read_message_(msg, is_sop)) {
ESP_LOGW(TAG, "Reading message failed");
// A partial read leaves the FIFO misaligned
this->write_byte(FUSB_CONTROL1, FUSB_CONTROL1_RX_FLUSH);
return;
}
// Only SOP packets are addressed to a sink; SOP'/SOP'' are for cable plugs
if (is_sop)
this->handle_message_(msg);
if (!this->read_byte(FUSB_STATUS1, &status1))
return;
}
}
bool FUSB302B::read_message_(PdMsg &msg, bool &is_sop) {
uint8_t token = 0;
uint8_t header[2];
if (!this->read_byte(FUSB_FIFOS, &token) || this->read_register(FUSB_FIFOS, header, 2) != i2c::ERROR_OK)
return false;
msg.set_header(encode_uint16(header[1], header[0]));
// Data objects, followed by the CRC32 that the chip already checked
uint8_t buf[PD_MAX_NUM_DATA_OBJECTS * 4 + 4];
if (this->read_register(FUSB_FIFOS, buf, msg.num_of_obj * 4 + 4) != i2c::ERROR_OK)
return false;
for (uint8_t i = 0; i < msg.num_of_obj; i++) {
const uint8_t *obj = &buf[i * 4];
msg.data_objects[i] = encode_uint32(obj[3], obj[2], obj[1], obj[0]);
}
is_sop = (token & FUSB_FIFO_RX_TOKEN_BITS) == FUSB_FIFO_RX_SOP;
return true;
}
bool FUSB302B::send_message(const PdMsg &msg) {
// Register address, then the TX tokens; written in one transfer without the copy write_register() makes
uint8_t buf[1 + 4 + 1 + 2 + PD_MAX_NUM_DATA_OBJECTS * 4 + 4];
size_t len = 0;
const uint16_t header = msg.get_coded_header();
buf[len++] = FUSB_FIFOS;
buf[len++] = FUSB_TX_TOKEN_SYNC1;
buf[len++] = FUSB_TX_TOKEN_SYNC1;
buf[len++] = FUSB_TX_TOKEN_SYNC1;
buf[len++] = FUSB_TX_TOKEN_SYNC2;
buf[len++] = FUSB_TX_TOKEN_PACKSYM | (msg.num_of_obj * 4 + 2);
buf[len++] = header & 0xFF;
buf[len++] = header >> 8;
for (uint8_t i = 0; i < msg.num_of_obj; i++) {
const uint32_t obj = msg.data_objects[i];
buf[len++] = obj & 0xFF;
buf[len++] = (obj >> 8) & 0xFF;
buf[len++] = (obj >> 16) & 0xFF;
buf[len++] = obj >> 24;
}
buf[len++] = FUSB_TX_TOKEN_JAM_CRC;
buf[len++] = FUSB_TX_TOKEN_EOP;
buf[len++] = FUSB_TX_TOKEN_TXOFF;
buf[len++] = FUSB_TX_TOKEN_TXON;
i2c::ErrorCode err = this->write(buf, len);
if (err != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Sending message type %u failed (error %d)", msg.type, static_cast<int>(err));
return false;
}
return true;
}
bool FUSB302B::measure_cc_(uint8_t meas_switch, uint8_t &level) {
if (!this->write_byte(FUSB_SWITCHES0, FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | meas_switch))
return false;
vTaskDelay(pdMS_TO_TICKS(CC_SETTLE_MS));
uint8_t status0 = 0;
if (!this->read_byte(FUSB_STATUS0, &status0))
return false;
level = status0 & FUSB_STATUS0_BC_LVL;
for (uint8_t i = 0; i < CC_STABLE_READS; i++) {
if (!this->read_byte(FUSB_STATUS0, &status0))
return false;
if ((status0 & FUSB_STATUS0_BC_LVL) != level) {
level = CC_LEVEL_UNSTABLE;
return true;
}
}
return true;
}
void FUSB302B::try_attach_() {
if (this->state_ == PdState::PD_STATE_ERROR) {
if (!this->init_chip_())
return;
ESP_LOGI(TAG, "Communication restored");
this->set_state_(PdState::PD_STATE_DISCONNECTED);
}
uint8_t status0 = 0;
if (!this->write_byte(FUSB_POWER, FUSB_POWER_ALL) || !this->read_byte(FUSB_STATUS0, &status0)) {
this->enter_error_();
return;
}
// A source may present its CC pull-up before VBUS is on; detach is only detected when VBUS drops
if (!(status0 & FUSB_STATUS0_VBUSOK)) {
this->cc_ambiguous_logged_ = false;
return;
}
uint8_t cc1 = 0;
uint8_t cc2 = 0;
if (!this->write_byte(FUSB_SWITCHES1, FUSB_SWITCHES1_SPECREV_REV2) ||
!this->write_byte(FUSB_MEASURE, FUSB_MEASURE_MDAC_CC) || !this->measure_cc_(FUSB_SWITCHES0_MEAS_CC1, cc1) ||
!this->measure_cc_(FUSB_SWITCHES0_MEAS_CC2, cc2)) {
this->enter_error_();
return;
}
// Exactly one CC line carries the source pull-up; the other one is open
uint8_t meas_switch;
uint8_t tx_switch;
if (cc1 != CC_LEVEL_UNSTABLE && cc1 > 0 && cc2 == 0) {
meas_switch = FUSB_SWITCHES0_MEAS_CC1;
tx_switch = FUSB_SWITCHES1_TXCC1;
} else if (cc2 != CC_LEVEL_UNSTABLE && cc2 > 0 && cc1 == 0) {
meas_switch = FUSB_SWITCHES0_MEAS_CC2;
tx_switch = FUSB_SWITCHES1_TXCC2;
} else {
if (!this->cc_ambiguous_logged_) {
ESP_LOGD(TAG, "VBUS present but no single CC line with a source pull-up (CC1 %u, CC2 %u)", cc1, cc2);
this->cc_ambiguous_logged_ = true;
}
return;
}
if (!this->write_byte(FUSB_SWITCHES0, FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | meas_switch) ||
!this->write_byte(FUSB_SWITCHES1, tx_switch | FUSB_SWITCHES1_SPECREV_REV2) || !this->enable_auto_crc_() ||
!this->reset_pd_()) {
this->enter_error_();
return;
}
ESP_LOGD(TAG, "Attached on CC%u", meas_switch == FUSB_SWITCHES0_MEAS_CC1 ? 1 : 2);
this->attached_ = true;
this->cc_ambiguous_logged_ = false;
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT);
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
}
void FUSB302B::detach_() {
ESP_LOGD(TAG, "Detached (VBUS lost)");
this->attached_ = false;
this->waiting_for_source_caps_ = false;
this->reset_pd_();
this->clear_contract_();
this->set_state_(PdState::PD_STATE_DISCONNECTED);
}
void FUSB302B::handle_hard_reset_() {
ESP_LOGD(TAG, "Hard reset received");
// The source returns to Type-C default power and sends its capabilities again
// Also flushes the RX FIFO, so the caller has nothing left to read
if (!this->reset_pd_()) {
this->enter_error_();
return;
}
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT);
}
void FUSB302B::enter_error_() {
if (this->state_ == PdState::PD_STATE_ERROR)
return;
ESP_LOGW(TAG, "Communication with the chip failed");
this->attached_ = false;
this->waiting_for_source_caps_ = false;
this->clear_contract_();
this->set_state_(PdState::PD_STATE_ERROR);
}
void FUSB302B::send_soft_reset_() {
this->reset_pd_();
this->send_message(this->make_control_msg_(PD_CNTRL_SOFT_RESET));
// The source answers a soft reset with new source capabilities
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_SOFT_RESET_SENT);
}
void FUSB302B::start_source_cap_wait_(SourceCapStep step) {
this->waiting_for_source_caps_ = true;
this->source_cap_step_ = step;
this->source_cap_step_start_ = millis();
}
void FUSB302B::check_source_caps_() {
if (!this->waiting_for_source_caps_ || this->check_ams_())
return;
const uint32_t elapsed = millis() - this->source_cap_step_start_;
switch (this->source_cap_step_) {
case SourceCapStep::SOURCE_CAP_STEP_WAIT:
if (elapsed < SOURCE_CAP_WAIT_MS)
return;
ESP_LOGD(TAG, "No source capabilities received, requesting them");
this->send_message(this->make_control_msg_(PD_CNTRL_GET_SOURCE_CAP));
this->source_cap_step_ = SourceCapStep::SOURCE_CAP_STEP_GET_SENT;
break;
case SourceCapStep::SOURCE_CAP_STEP_GET_SENT:
if (elapsed < SOURCE_CAP_RETRY_MS)
return;
ESP_LOGD(TAG, "No source capabilities received, sending soft reset");
this->send_soft_reset_();
return;
case SourceCapStep::SOURCE_CAP_STEP_SOFT_RESET_SENT:
if (elapsed < SOURCE_CAP_RETRY_MS)
return;
ESP_LOGW(TAG, "PD negotiation failed, staying at 5 V");
this->waiting_for_source_caps_ = false;
this->set_state_(PdState::PD_STATE_PD_TIMEOUT);
return;
}
this->source_cap_step_start_ = millis();
}
} // namespace esphome::fusb302b
#endif // USE_ESP32
-74
View File
@@ -1,74 +0,0 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/static_task.h"
#include "pd.h"
namespace esphome::fusb302b {
/// FUSB302B USB Type-C / Power Delivery sink controller.
///
/// A dedicated task owns all I2C traffic to the chip so that PD replies meet the protocol timing
/// even while the main loop is busy.
class FUSB302B : public PowerDelivery, public Component, public i2c::I2CDevice {
public:
explicit FUSB302B(InternalGPIOPin *interrupt_pin) : interrupt_pin_(interrupt_pin) {}
void setup() override;
void dump_config() override;
void on_shutdown() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
enum class SourceCapStep : uint8_t {
SOURCE_CAP_STEP_WAIT,
SOURCE_CAP_STEP_GET_SENT,
SOURCE_CAP_STEP_SOFT_RESET_SENT,
};
static void gpio_intr(FUSB302B *arg);
static void task_func(void *arg);
bool send_message(const PdMsg &msg) override;
void on_state_changed() override {}
void on_soft_reset_received() override { this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT); }
void on_source_caps_received() override { this->waiting_for_source_caps_ = false; }
void on_transition_timeout() override { this->send_soft_reset_(); }
// PHY task only
[[noreturn]] void run_task_();
bool init_chip_();
bool reset_pd_();
bool enable_auto_crc_();
void handle_interrupt_();
bool read_message_(PdMsg &msg, bool &is_sop);
void try_attach_();
bool measure_cc_(uint8_t meas_switch, uint8_t &level);
void detach_();
void handle_hard_reset_();
void start_source_cap_wait_(SourceCapStep step);
void send_soft_reset_();
void check_source_caps_();
void enter_error_();
InternalGPIOPin *interrupt_pin_;
StaticTask task_;
// PHY task only
uint32_t source_cap_step_start_{0};
SourceCapStep source_cap_step_{SourceCapStep::SOURCE_CAP_STEP_WAIT};
bool attached_{false};
bool waiting_for_source_caps_{false};
bool cc_ambiguous_logged_{false};
};
} // namespace esphome::fusb302b
#endif // USE_ESP32
@@ -1,94 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::fusb302b {
// Register addresses
static constexpr uint8_t FUSB_DEVICE_ID = 0x01;
static constexpr uint8_t FUSB_SWITCHES0 = 0x02;
static constexpr uint8_t FUSB_SWITCHES1 = 0x03;
static constexpr uint8_t FUSB_MEASURE = 0x04;
static constexpr uint8_t FUSB_CONTROL0 = 0x06;
static constexpr uint8_t FUSB_CONTROL1 = 0x07;
static constexpr uint8_t FUSB_CONTROL3 = 0x09;
static constexpr uint8_t FUSB_MASK1 = 0x0A;
static constexpr uint8_t FUSB_POWER = 0x0B;
static constexpr uint8_t FUSB_RESET = 0x0C;
static constexpr uint8_t FUSB_MASKA = 0x0E;
static constexpr uint8_t FUSB_MASKB = 0x0F;
static constexpr uint8_t FUSB_STATUS0A = 0x3C;
static constexpr uint8_t FUSB_STATUS0 = 0x40;
static constexpr uint8_t FUSB_STATUS1 = 0x41;
static constexpr uint8_t FUSB_FIFOS = 0x43;
// DEVICE_ID: version in bits 7:4, product variant (I2C address 0x22-0x25) in bits 3:2, silicon revision in bits 1:0
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_MASK = 0xF0;
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_A = 0x80;
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_B = 0x90;
// SWITCHES0 bits
static constexpr uint8_t FUSB_SWITCHES0_MEAS_CC2 = 1 << 3;
static constexpr uint8_t FUSB_SWITCHES0_MEAS_CC1 = 1 << 2;
static constexpr uint8_t FUSB_SWITCHES0_PDWN_2 = 1 << 1;
static constexpr uint8_t FUSB_SWITCHES0_PDWN_1 = 1 << 0;
// SWITCHES1 bits
static constexpr uint8_t FUSB_SWITCHES1_SPECREV_REV2 = 0x01 << 5;
static constexpr uint8_t FUSB_SWITCHES1_AUTO_CRC = 1 << 2;
static constexpr uint8_t FUSB_SWITCHES1_TXCC2 = 1 << 1;
static constexpr uint8_t FUSB_SWITCHES1_TXCC1 = 1 << 0;
// MEASURE value used for CC detection
static constexpr uint8_t FUSB_MEASURE_MDAC_CC = 49;
// CONTROL0 bits
static constexpr uint8_t FUSB_CONTROL0_TX_FLUSH = 1 << 6;
// CONTROL1 bits
static constexpr uint8_t FUSB_CONTROL1_RX_FLUSH = 1 << 2;
// CONTROL3 bits
static constexpr uint8_t FUSB_CONTROL3_N_RETRIES_SHIFT = 1;
static constexpr uint8_t FUSB_CONTROL3_N_RETRIES_MASK = 0x3 << FUSB_CONTROL3_N_RETRIES_SHIFT;
static constexpr uint8_t FUSB_CONTROL3_AUTO_RETRY = 1 << 0;
// MASK1: mask the ACTIVITY, CRC_CHK and BC_LVL interrupts
static constexpr uint8_t FUSB_MASK1_DEFAULT = 0x51;
// POWER bits
static constexpr uint8_t FUSB_POWER_ALL = 0x0F;
// RESET bits
static constexpr uint8_t FUSB_RESET_PD_RESET = 1 << 1;
static constexpr uint8_t FUSB_RESET_SW_RES = 1 << 0;
// INTERRUPTA bits
static constexpr uint8_t FUSB_INTERRUPTA_I_RETRYFAIL = 1 << 4;
static constexpr uint8_t FUSB_INTERRUPTA_I_SOFTRST = 1 << 1;
static constexpr uint8_t FUSB_INTERRUPTA_I_HARDRST = 1 << 0;
// INTERRUPT bits
static constexpr uint8_t FUSB_INTERRUPT_I_VBUSOK = 1 << 7;
// STATUS0 bits
static constexpr uint8_t FUSB_STATUS0_VBUSOK = 1 << 7;
static constexpr uint8_t FUSB_STATUS0_BC_LVL = 0x03;
// STATUS1 bits
static constexpr uint8_t FUSB_STATUS1_RX_EMPTY = 1 << 5;
// RX FIFO token
static constexpr uint8_t FUSB_FIFO_RX_TOKEN_BITS = 0xE0;
static constexpr uint8_t FUSB_FIFO_RX_SOP = 0xE0;
// TX FIFO tokens
static constexpr uint8_t FUSB_TX_TOKEN_TXON = 0xA1;
static constexpr uint8_t FUSB_TX_TOKEN_SYNC1 = 0x12;
static constexpr uint8_t FUSB_TX_TOKEN_SYNC2 = 0x13;
static constexpr uint8_t FUSB_TX_TOKEN_PACKSYM = 0x80;
static constexpr uint8_t FUSB_TX_TOKEN_JAM_CRC = 0xFF;
static constexpr uint8_t FUSB_TX_TOKEN_EOP = 0x14;
static constexpr uint8_t FUSB_TX_TOKEN_TXOFF = 0xFE;
} // namespace esphome::fusb302b
-1
View File
@@ -30,7 +30,6 @@ enum PdControlMsgType : uint8_t {
PD_CNTRL_REJECT = 0x04,
PD_CNTRL_PING = 0x05,
PD_CNTRL_PS_RDY = 0x06,
PD_CNTRL_GET_SOURCE_CAP = 0x07,
PD_CNTRL_GET_SINK_CAP = 0x08,
PD_CNTRL_WAIT = 0x0C,
PD_CNTRL_SOFT_RESET = 0x0D,
-141
View File
@@ -1,141 +0,0 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_DATA,
CONF_ID,
CONF_ON_MESSAGE,
CONF_PIN,
CONF_SOURCE,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RP2,
PLATFORM_RTL87XX,
)
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@JosVanEijndhoven"]
DOMAIN = "hdmi_cec"
MAX_LOGICAL_ADDRESS = 15
MAX_PAYLOAD_LENGTH = 15
CONF_PROMISCUOUS_MODE = "promiscuous_mode"
CONF_MONITOR_MODE = "monitor_mode"
CONF_DESTINATION = "destination"
CONF_OPCODE = "opcode"
hdmi_cec_ns = cg.esphome_ns.namespace("hdmi_cec")
HDMICEC = hdmi_cec_ns.class_("HDMICEC", cg.Component)
_PAYLOAD_SPAN = cg.std_span.template(cg.uint8.operator("const"))
_PAYLOAD_VECTOR = cg.std_vector.template(cg.uint8)
_MESSAGE_ARGS = [
(cg.uint8, "source"),
(cg.uint8, "destination"),
(_PAYLOAD_SPAN, "data"),
]
_request_message_callback_slot = cg.slot_counter("HDMI_CEC_MESSAGE_CALLBACK_COUNT")
logical_address = cv.int_range(min=0, max=MAX_LOGICAL_ADDRESS)
payload = cv.All(cv.ensure_list(cv.hex_uint8_t), cv.Length(max=MAX_PAYLOAD_LENGTH))
payload_prefix = cv.All(payload, cv.Length(min=1))
def _validate_message_filter(config: ConfigType) -> ConfigType:
# The opcode is the first payload byte: fold it into the data prefix
if (opcode := config.pop(CONF_OPCODE, None)) is not None:
data = config.setdefault(CONF_DATA, [opcode])
if data[0] != opcode:
raise cv.Invalid(
f"opcode 0x{opcode:02X} differs from the first data byte 0x{data[0]:02X}"
)
return config
def _message_filter(conf: ConfigType) -> str | None:
"""The C++ condition an on_message automation runs under, or None for every frame."""
checks = []
if (source := conf.get(CONF_SOURCE)) is not None:
checks.append(f"source == {source}")
if (destination := conf.get(CONF_DESTINATION)) is not None:
checks.append(f"destination == {destination}")
if data := conf.get(CONF_DATA):
checks.append(f"data.size() >= {len(data)}")
checks.extend(f"data[{i}] == 0x{byte:02X}" for i, byte in enumerate(data))
return " && ".join(checks) or None
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HDMICEC),
cv.Required(CONF_PIN): pins.internal_gpio_input_pullup_pin_schema,
cv.Required(CONF_ADDRESS): logical_address,
cv.Optional(CONF_PROMISCUOUS_MODE, default=False): cv.boolean,
cv.Optional(CONF_MONITOR_MODE, default=False): cv.boolean,
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{
cv.Optional(CONF_SOURCE): logical_address,
cv.Optional(CONF_DESTINATION): logical_address,
cv.Optional(CONF_OPCODE): cv.hex_uint8_t,
cv.Optional(CONF_DATA): payload_prefix,
},
_validate_message_filter,
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(
[
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
PLATFORM_LN882X,
]
),
)
async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
# The acknowledge bit is released from a general purpose timer, left out of the build by default
include_builtin_idf_component("esp_driver_gptimer")
pin = await cg.gpio_pin_expression(config[CONF_PIN])
var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_ADDRESS])
await cg.register_component(var, config)
if config[CONF_PROMISCUOUS_MODE]:
cg.add(var.set_promiscuous_mode(True))
if config[CONF_MONITOR_MODE]:
cg.add(var.set_monitor_mode(True))
for conf in config.get(CONF_ON_MESSAGE, []):
_request_message_callback_slot()
await automation.build_callback_automation(
var,
"add_on_message_callback",
_MESSAGE_ARGS,
conf,
when=_message_filter(conf),
)
automation.register_apply_action(
"hdmi_cec.send",
cv.Schema(
{
cv.GenerateID(): cv.use_id(HDMICEC),
cv.Required(CONF_DESTINATION): cv.templatable(logical_address),
cv.Required(CONF_DATA): cv.templatable(payload),
}
),
automation.ApplyCall(
"send({}, {})",
((CONF_DESTINATION, cg.uint8), (CONF_DATA, _PAYLOAD_VECTOR)),
),
)
-484
View File
@@ -1,484 +0,0 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <map>
#include <array>
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#ifdef HDMI_CEC_USE_DECODER
#include "hdmi_cec.h"
#include "cec_decoder.h"
namespace esphome {
namespace hdmi_cec {
const std::array<const char *, 0x77> Decoder::UI_COMMANDS = {
/* 0x00 = */ "Select",
"Up",
"Down",
"Left",
"Right",
"Right-Up",
"Right-Down",
"Left-Up",
/* 0x08 = */ "Left-Down",
"Root Menu",
"Setup Menu",
"Contents Menu",
"Favorite Menu",
"Exit",
"Reserved",
"Reserved",
/* 0x10 = */ "Media Top Menu",
"Media Context-sensitive Menu",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
/* 0x18 = */ "Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Number Entry Mode",
"11",
"12",
/* 0x20 = */ "0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
/* 0x28 = */ "8",
"9",
"Dot",
"Enter",
"Clear",
"Reserved",
"Reserved",
"Next Favorite",
/* 0x30 = */ "Channel Up",
"Channel Down",
"Previous Channel",
"Sound Select",
"Input Select",
"Display Information",
"Help",
"Page Up",
/* 0x38 = */ "Page Down",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
/* 0x40 = */ "Power",
"Volume Up",
"Volume Down",
"Mute",
"Play",
"Stop",
"Pause",
"Record",
/* 0x48 = */ "Rewind",
"Fast forward",
"Eject",
"Forward",
"Backward",
"Stop-Record",
"Pause-Record",
"Reserved",
/* 0x50 = */ "Angle",
"Sub picture",
"Video on Demand",
"Electronic Program Guide",
"Timer Programming",
"Initial Configuration",
"Select Broadcast Type",
"Select Sound Presentation",
/* 0x58 = */ "Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
/* 0x60 = */ "Play Function",
"Pause-Play Function",
"Record Function",
"Pause-Record Function",
"Stop Function",
"Mute Function",
"Restore Volume Function",
"Tune Function",
/* 0x68 = */ "Select Media Function",
"Select A/V Input Function",
"Select Audio Input Function",
"Power Toggle Function",
"Power Off Function",
"Power On Function",
"Reserved",
"Reserved",
/* 0x70 = */ "Reserved",
"F1 (Blue)",
"F2 (Red)",
"F3 (Green)",
"F4 (Yellow)",
"F5",
"Data"};
// See 'short audio descriptor' in https://en.wikipedia.org/wiki/Extended_Display_Identification_Data
const std::array<const char *, 0x11> Decoder::AUDIO_FORMATS = {
"reserved", "LPCM", "AC3", "MPEG-1", "MP3", "MPEG-2", "AAC", "DTS", "ATRAC",
"DSD", "DD+", "DTS-HD", "MAT/Dolby TrueHD", "DST Audio", "WMA Pro", "Extension?"};
const std::array<const char *, 8> Decoder::AUDIO_SAMPLERATES = {"32", "44.1", "48", "88",
"96", "176", "192", "Reserved"};
template<uint32_t OPERANDS> bool Decoder::do_operand_() {
if (OPERANDS <= 0xFF) {
// generic function called for single operand of unkown type and length
return append_operand_(".");
} else {
// generic function to handle two or more operands
return do_operand_<OPERANDS & 0xFF>() && do_operand_<(OPERANDS >> 8u)>();
}
}
#define APPEND_LINE(...) (length_ = buf_append_printf(line_, size_, length_, __VA_ARGS__))
/**
* List of specialised operand decode functions, one function per operand type.
* (Not fully complete, might be extended later)
*/
template<> bool Decoder::do_operand_<Decoder::NONE>() { return false; }
template<> bool Decoder::do_operand_<Decoder::ABORT_REASON>() {
const static std::array<const char *, 6> NAMES = {
"Unrecognized opcode", "Not in correct mode to respond", "Cannot provide source", "Invalid operand", "Refused",
"Unable to determine"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::AUDIO_FORMAT>() {
// this type of operand comes in a sequence, until exhausted
bool ok = true;
while (ok && (offset_ < frame_.size())) {
ok = append_operand_<AUDIO_FORMATS.size()>(AUDIO_FORMATS);
}
return ok;
}
template<> bool Decoder::do_operand_<Decoder::AUDIO_STATUS>() {
char line[20];
if (offset_ < frame_.size()) {
uint8_t field = frame_[offset_];
std::snprintf(line, sizeof(line), "Mute=%d,Vol=%02X", (field >> 7), (field & 0x7f));
return append_operand_(line);
} else {
return append_operand_("?");
}
}
template<> bool Decoder::do_operand_<Decoder::DEVICE_TYPE>() {
const static std::array<const char *, 8> NAMES = {"TV", "Recording Device", "Reserved",
"Tuner", "Playback Device", "Audio System",
"CEC Switch", "Video Processor"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::DISPLAY_CONTROL>() {
const static std::array<const char *, 4> NAMES = {"Default time", "Until cleared", "Clear previous", "Reserved"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::FEATURE_OPCODE>() {
if (offset_ >= frame_.size()) {
return false;
}
uint8_t opcode = frame_[offset_];
return append_operand_(find_opcode_name_(opcode));
}
template<> bool Decoder::do_operand_<Decoder::OSD_STRING>() {
char line[20]; // frame size() is at most 16
unsigned int i;
for (i = 0; i < frame_.size() - offset_; i++) {
line[i] = (char) (frame_[offset_ + i]);
}
line[i] = '\0'; // append '\0' termination: that is likely NOT part of the frame
offset_ = frame_.size(); // no useful data further in this frame
return append_operand_(line);
}
template<> bool Decoder::do_operand_<Decoder::PHYSICAL_ADDRESS>() {
// Exception: if this is an operand of <System Audio Mode Request> 0x70, then this operand is
// merely optional, and its absence means 'Off'
if (frame_.at(1) == 0x70 && offset_ >= frame_.size()) {
return append_operand_("Off");
}
if (offset_ + 1 >= frame_.size()) {
return append_operand_("?", 2);
}
char line[12];
std::snprintf(line, sizeof(line), "%1x.%1x.%1x.%1x", (frame_[offset_] >> 4) & 0xF, frame_[offset_] & 0xF,
(frame_[offset_ + 1] >> 4) & 0xF, frame_[offset_ + 1] & 0xF);
return append_operand_(line, 2);
}
template<> bool Decoder::do_operand_<Decoder::POWER_STATUS>() {
const static std::array<const char *, 4> NAMES = {"On", "Standby", "Standby->On", "On->Standby"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::SHORT_AUDIO_DESCRIPTOR>() {
// The frame can have a sequence of these operands, count is not fixed;
// each such operand takes 3 bytes in the frame.
// For a specification of this "Short Audio descriptor" format, see the Wikipedia page on
// "Extended Display Identification Data (EDID)", section "Audio Data Blocks".
std::array<char, 100> line;
uint32_t pos = 0;
bool ok = true;
while (ok && (offset_ + 2 < frame_.size())) {
const uint8_t descriptor = frame_[offset_];
uint8_t format = (descriptor >> 3) & 0x0F;
pos = buf_append_printf(&line[0], line.size(), 0, "%s", AUDIO_FORMATS[format]);
pos = buf_append_printf(&line[0], line.size(), pos, ",num_channels=%d", (descriptor & 0x07));
uint8_t rates = frame_[offset_ + 1];
for (int bit = 0; rates; bit++, rates >>= 1) {
if (rates & 0x1) {
// show support of various audio sample rates
pos = buf_append_printf(&line[0], line.size(), pos, ",%skHz", AUDIO_SAMPLERATES[bit]);
}
}
if (format == 1) {
// for LPCM format
uint8_t widths = frame_[offset_ + 2] & 0x7;
for (int i = 0; widths; i++, widths >>= 1) {
if (widths & 0x1) {
// show support of audio samble bit widths of 16, 20, and/or 24
pos = buf_append_printf(&line[0], line.size(), pos, ",%dbits", (16 + 4 * i));
}
}
}
ok = append_operand_(line.data(), 3);
}
// Note: Further descriptor 'extensions' not yet decoded
return ok;
}
template<> bool Decoder::do_operand_<Decoder::SYSTEM_AUDIO_STATUS>() {
const static std::array<const char *, 2> NAMES = {"Off", "On"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::UI_COMMAND>() {
uint8_t command = frame_[offset_];
bool ok = append_operand_<UI_COMMANDS.size()>(UI_COMMANDS);
if (!ok) {
return false;
}
// out of the 100+ UI commands, a few exceptional UI commands have appended an extra parameter:
switch (command) {
case 0x56:
return do_operand_<UI_BROADCAST_TYPE>();
case 0x57:
return do_operand_<UI_SOUND_PRESENTATION_CONTROL>();
case 0x60:
return do_operand_<PLAY_MODE>();
case 0x67:
return do_operand_<CHANNEL_IDENTIFIER>();
case 0x68:
return do_operand_<UI_FUNCTION_MEDIA>();
case 0x69:
return do_operand_<UI_FUNCTION_SELECT_AV_INPUT>();
case 0x6A:
return do_operand_<UI_FUNCTION_SELECT_AUDIO_INPUT>();
default:
return ok;
}
}
template<> bool Decoder::do_operand_<Decoder::VENDOR_ID>() {
if (offset_ + 2 >= frame_.size()) {
return append_operand_("?", 3);
}
uint32_t id = (uint32_t) (frame_[offset_]) << 16 | (uint32_t) (frame_[offset_ + 1]) << 8 | frame_[offset_ + 2];
const char *const *name_lookup = VENDOR_IDS.find(id);
const char *vendor_name = name_lookup ? (*name_lookup) : nullptr;
if (!vendor_name) {
// if the hdmi-cec vendor id is not in our list, the id value itself is printed.
char line[12];
std::snprintf(line, sizeof(line), "ID=%06x", id);
return append_operand_(line, 3);
}
return append_operand_(vendor_name, 3);
}
template<> bool Decoder::do_operand_<Decoder::CEC_VERSION>() {
const static std::array<const char *, 9> NAMES = {"?", "1.2", "1.2a", "1.3", "1.3a", "1.4", "2.0", "2.x", "2.x"};
return append_operand_<NAMES.size()>(NAMES);
}
const Decoder::CecOpcodeTable Decoder::CEC_OPCODE_TABLE = {
{// Must be given sorted on increasing opcode:
// opcode, name, function pointer with specification of its operands
{0x00, {"Feature Abort", &Decoder::do_operand_<two(FEATURE_OPCODE, ABORT_REASON)>}},
{0x04, {"Image View On", &Decoder::do_operand_<NONE>}},
{0x05, {"Tuner Step Increment", &Decoder::do_operand_<NONE>}},
{0x06, {"Tuner Step Decrement", &Decoder::do_operand_<NONE>}},
{0x07, {"Tuner Device Status", &Decoder::do_operand_<TUNER_DEVICE_INFO>}},
{0x08, {"Give Tuner Device Status", &Decoder::do_operand_<STATUS_REQUEST>}},
{0x09, {"Record On", &Decoder::do_operand_<RECORD_SOURCE>}},
{0x0A, {"Record Status", &Decoder::do_operand_<RECORD_STATUS_INFO>}},
{0x0B, {"Record Off", &Decoder::do_operand_<NONE>}},
{0x0D, {"Text View On", &Decoder::do_operand_<NONE>}},
{0x0F, {"Record TV Screen", &Decoder::do_operand_<NONE>}},
{0x1A, {"Give Deck Status", &Decoder::do_operand_<STATUS_REQUEST>}},
{0x1B, {"Deck Status", &Decoder::do_operand_<DECK_INFO>}},
{0x32, {"Set Menu Language", &Decoder::do_operand_<LANGUAGE>}},
{0x33, {"Clear Analogue Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x34, {"Set Analogue Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x35, {"Timer Status", &Decoder::do_operand_<TIMER_STATUS_DATA>}},
{0x36, {"Standby", &Decoder::do_operand_<NONE>}},
{0x41, {"Play", &Decoder::do_operand_<PLAY_MODE>}},
{0x42, {"Deck Control", &Decoder::do_operand_<DECK_CONTROL_MODE>}},
{0x43, {"Timer Cleared Status", &Decoder::do_operand_<TIMER_CLEARED_STATUS_DATA>}},
{0x44, {"User Control Pressed", &Decoder::do_operand_<UI_COMMAND>}},
{0x45, {"User Control Released", &Decoder::do_operand_<NONE>}},
{0x46, {"Give OSD Name", &Decoder::do_operand_<NONE>}},
{0x47, {"Set OSD Name", &Decoder::do_operand_<OSD_NAME>}},
{0x64, {"Set OSD String", &Decoder::do_operand_<two(DISPLAY_CONTROL, OSD_STRING)>}},
{0x67, {"Set Timer Program Title", &Decoder::do_operand_<PROGRAM_TITLE_STRING>}},
{0x70, {"System Audio Mode Request", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x71, {"Give Audio Status", &Decoder::do_operand_<NONE>}},
{0x72, {"Set System Audio Mode", &Decoder::do_operand_<SYSTEM_AUDIO_STATUS>}},
{0x7A, {"Report Audio Status", &Decoder::do_operand_<AUDIO_STATUS>}},
{0x7D, {"Give System Audio Mode Status", &Decoder::do_operand_<NONE>}},
{0x7E, {"System Audio Mode Status", &Decoder::do_operand_<SYSTEM_AUDIO_STATUS>}},
{0x80, {"Routing Change", &Decoder::do_operand_<two(PHYSICAL_ADDRESS, PHYSICAL_ADDRESS)>}},
{0x81, {"Routing Information", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x82, {"Active Source", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x83, {"Give Physical Address", &Decoder::do_operand_<NONE>}},
{0x84, {"Report Physical Address", &Decoder::do_operand_<two(PHYSICAL_ADDRESS, DEVICE_TYPE)>}},
{0x85, {"Request Active Source", &Decoder::do_operand_<NONE>}},
{0x86, {"Set Stream Path", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x87, {"Device Vendor ID", &Decoder::do_operand_<VENDOR_ID>}},
{0x89, {"Vendor Command", &Decoder::do_operand_<VENDOR_SPECIFIC_DATA>}},
{0x8A, {"Vendor Remote Button Down", &Decoder::do_operand_<VENDOR_SPECIFIC_RC_CODE>}},
{0x8B, {"Vendor Remote Button Up", &Decoder::do_operand_<NONE>}},
{0x8C, {"Give Device Vendor ID", &Decoder::do_operand_<NONE>}},
{0x8D, {"Menu Request", &Decoder::do_operand_<MENU_REQUEST_TYPE>}},
{0x8E, {"Menu Status", &Decoder::do_operand_<MENU_STATE>}},
{0x8F, {"Give Device Power Status", &Decoder::do_operand_<NONE>}},
{0x90, {"Report Power Status", &Decoder::do_operand_<POWER_STATUS>}},
{0x91, {"Get Menu Language", &Decoder::do_operand_<NONE>}},
{0x92,
{"Select Analogue Service",
&Decoder::do_operand_<three(ANALOG_BROADCAST_TYPE, ANALOG_FREQUENCY, BROADCAST_SYSTEM)>}},
{0x93, {"Select Digital Service", &Decoder::do_operand_<DIGITAL_SERVICE_IDENTIFICATION>}},
{0x9A, {"Set Audio Rate", &Decoder::do_operand_<AUDIO_RATE>}},
{0x9D, {"Inactive Source", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x97, {"Set Digital Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x99, {"Clear Digital Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x9E, {"CEC Version", &Decoder::do_operand_<CEC_VERSION>}},
{0x9F, {"Get CEC Version", &Decoder::do_operand_<NONE>}},
{0xA0, {"Vendor Command With ID", &Decoder::do_operand_<two(VENDOR_ID, VENDOR_SPECIFIC_DATA)>}},
{0xA1, {"Clear External Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0xA2, {"Set External Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0xA3, {"Report Short Audio Descriptor", &Decoder::do_operand_<SHORT_AUDIO_DESCRIPTOR>}},
{0xA4, {"Request Short Audio Descriptor", &Decoder::do_operand_<AUDIO_FORMAT>}},
{0xC0, {"Initiate ARC", &Decoder::do_operand_<NONE>}},
{0xC1, {"Report ARC Initiated", &Decoder::do_operand_<NONE>}},
{0xC2, {"Report ARC Terminated", &Decoder::do_operand_<NONE>}},
{0xC3, {"Request ARC Initiation", &Decoder::do_operand_<NONE>}},
{0xC4, {"Request ARC Termination", &Decoder::do_operand_<NONE>}},
{0xC5, {"Terminate ARC", &Decoder::do_operand_<NONE>}},
{0xF8, {"CDC Message", &Decoder::do_operand_<NONE>}},
{0xFF, {"Abort", &Decoder::do_operand_<NONE>}}}};
const Decoder::VendorIdTable Decoder::VENDOR_IDS = {
{// Must be given sorted on increasing id:
{0x000039, "Toshiba"}, {0x0000F0, "Samsung"}, {0x0005CD, "Denon"}, {0x000678, "Maranz"},
{0x000982, "Loewe"}, {0x0009B0, "Onkyo"}, {0x000CB8, "Medion"}, {0x000CE7, "Toshiba"},
{0x0010FA, "Apple"}, {0x001582, "Pulse Eight"}, {0x001950, "Harman Kardon"}, {0x001A11, "Google"},
{0x0020C7, "Akai"}, {0x002467, "AOC"}, {0x008045, "Panasonic"}, {0x00903E, "Philips"},
{0x009053, "Daewoo"}, {0x00A0DE, "Yamaha"}, {0x00D0D5, "Grundig"}, {0x00E036, "Pioneer"},
{0x00E091, "LG"}, {0x08001F, "Sharp"}, {0x080046, "Sony"}, {0x18C086, "Broadcom"},
{0x534850, "Sharp"}, {0x6B746D, "Vizio"}, {0x8065E9, "Benq"}, {0x9C645E, "Harman Kardon"}}};
void Decoder::address_decode_() {
const static std::array<const char *, 16> NAMES = {"TV", "RecordingDev1", "RecordingDev2", "Tuner1",
"PlaybackDev1", "AudioSystem", "Tuner2", "Tuner3",
"PlaybackDev2", "RecordingDev3", "Tuner4", "PlaybackDev3",
"Reserved", "Reserved", "SpecificUse", "Unregistered"};
const char *dst = (frame_.is_broadcast()) ? "All" : NAMES[frame_.destination_addr()];
const char *src = NAMES[frame_.initiator_addr()];
APPEND_LINE("%s to %s: ", src, dst);
}
const char *Decoder::find_opcode_name_(uint32_t opcode) const {
const FrameType *frametype = CEC_OPCODE_TABLE.find(opcode);
if (!frametype)
return "?";
return frametype->name_;
}
/**
* Helper function to implement the 'do_operand' methods, to gather a textual representation.
* @return true if a further operand can be decoded, false otherwise
*/
bool Decoder::append_operand_(const char *word, uint8_t offset_incr /* default 1 */) {
APPEND_LINE("[%s]", word);
offset_ += offset_incr;
return (length_ < size_) && (offset_ < frame_.size());
}
/**
* Entry function 'decode' to call for full decode of a CEC frame
*/
int Decoder::decode() {
// prepare text buffer state variables for an empty line
length_ = 0; // currently accumulated length of text of operands
line_[length_] = '\0'; // initialise output text to empty string
if (!line_ || !size_)
return 0;
// print src and dest fields into 'line_'
address_decode_();
// print opcode field
const FrameType *frametype = nullptr;
if (frame_.size() <= 1) {
// Missing frame operation field?
APPEND_LINE("Ping");
} else {
// find operation name
frametype = CEC_OPCODE_TABLE.find(frame_.opcode());
const char *opname = frametype ? frametype->name_ : "?";
APPEND_LINE("<%s>", opname);
}
// convert operand fields to text:
if (frametype) {
offset_ = 2; // location in frame of first operand, after address-byte and opcode-byte
OperandDecode_f f = frametype->decode_f_;
(this->*f)(); // call one of the above 'do_operand_<xx>' methods
}
return length_;
}
} // namespace hdmi_cec
} // namespace esphome
#endif // HDMI_CEC_USE_DECODER
-196
View File
@@ -1,196 +0,0 @@
#pragma once
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <array>
#include "hdmi_cec.h"
namespace esphome::hdmi_cec {
/**
* This LookupTable is const-initialised and only provides a simple 'find()'.
* This creates a compact (smaller code) alternative to an std::map;
*/
template<typename KeyT, typename ValueT, unsigned int SIZE> class LookupTable {
public:
struct KeyValue {
KeyT key_;
ValueT value_;
};
const ValueT *find(KeyT key) const {
int lo_inx = 0;
int hi_inx = SIZE - 1;
while (lo_inx <= hi_inx) {
// Search for 'key' in the array range lo_inx..hi_inx, bounds included.
// Rely on the array being sorted on increasing opcode!
int mid = (lo_inx + hi_inx) / 2;
if (key == table_[mid].key_) {
return &table_[mid].value_;
}
if (key < table_[mid].key_) {
hi_inx = mid - 1;
} else {
lo_inx = mid + 1;
}
}
return nullptr; // requested 'key' not found in table
}
const KeyValue table_[SIZE];
};
/**
* This Decoder class interprets a binary CEC Frame to create a textual representation.
* The information to create this decoder is mostly extracted from the HDMI 1.3a standard document,
* from its section "Supplement 1 Consumer Electronics Control (CEC)".
* Some further details were found in the Linux kernel source code of the "v4l-utils" repository,
* such as the "ARC" related functionality of HDMI-CEC 1.4, and the HDMI vendor ID names.
* Details on the digital audio format decoding came from the wikipedia page on
* "Extended Display Identification Data (EDID)"
*/
class Decoder {
public:
Decoder(const Frame &frame, char *line, unsigned int size)
: frame_(frame), line_(line), size_(size), length_(0), offset_(2) {}
/**
* Create a one-line textual representation of the Frame content for logging and debugging
* Store the text in the earlier provided 'line' buffer
* @return the length of the written text, excluding the terminating null character
*/
int decode();
protected:
const char *find_opcode_name_(uint32_t opcode) const;
void address_decode_();
/**
* Generic operand decode method, later specialised with operand-type-specific methods
* @return true if further conversions can continue, false when to stop.
*/
template<uint32_t OPERANDS> bool do_operand_();
/**
* The CecOpcodeTable is extracted from the HDMI CEC standard (1.4):
* It lists all Frame opcodes with their <name> and their expected [operand argument type(s)]
*/
using OperandDecode_f = bool (Decoder::*)();
struct FrameType {
const char *name_; // name of the operation (of the op_code)
const OperandDecode_f decode_f_; // a pointer to the corresponding 'do_operand_()' method
};
using CecOpcodeTable = LookupTable<uint8_t, FrameType, 71>;
using VendorIdTable = LookupTable<uint32_t, const char *, 28>;
const Frame &frame_;
char *const line_; // buffer to hold the text of the decoded frame
const unsigned int size_; // size of the 'line_' buffer
unsigned int length_; // currently accumulated length of output text in 'line'
unsigned int offset_; // current offset in frame to process next operand byte(s) (frame[0] and [1] are skipped)
/**
* The HDMI CEC standard specifies a set of distinct operand (parameter) types,
* used across the frame opcodes, denoted with "[operand type name]".
* These specified operand types are enumerated here for later type-specific decoding to text
*/
enum Operand : uint8_t {
NONE,
ABORT_REASON,
ANALOG_BROADCAST_TYPE,
ANALOG_FREQUENCY,
ASCII_DIGIT,
ASCII,
AUDIO_FORMAT,
AUDIO_RATE,
AUDIO_STATUS,
BOOLEAN,
BROADCAST_SYSTEM,
CEC_VERSION,
CHANNEL_IDENTIFIER,
// DayOfMonth, integrated in StartDateTime
DECK_CONTROL_MODE,
DECK_INFO,
DEVICE_TYPE,
DIGITAL_SERVICE_IDENTIFICATION,
DISPLAY_CONTROL,
DURATION,
EXTERNAL_PHYSICAL_ADDRESS,
EXTERNAL_PLUG,
EXTERNAL_SOURCE_SPECIFIER,
HOUR,
FEATURE_OPCODE,
LANGUAGE,
MENU_REQUEST_TYPE,
MENU_STATE,
MINUTE,
// MonthOfYear, integrated in StartDateTime
NEW_ADDRESS,
ORIGINAL_ADDRESS,
OSD_NAME,
OSD_STRING = OSD_NAME,
PHYSICAL_ADDRESS,
PLAY_MODE,
POWER_STATUS,
PROGRAM_TITLE_STRING,
RECORD_SOURCE,
RECORD_STATUS_INFO,
RECORDING_SEQUENCE,
SHORT_AUDIO_DESCRIPTOR,
STATUS_REQUEST,
START_DATE_TIME,
SYSTEM_AUDIO_STATUS,
TIME,
TIMER_CLEARED_STATUS_DATA,
TIMER_STATUS_DATA,
TUNER_DEVICE_INFO,
UI_BROADCAST_TYPE,
UI_COMMAND,
UI_FUNCTION_MEDIA,
UI_FUNCTION_SELECT_AV_INPUT,
UI_FUNCTION_SELECT_AUDIO_INPUT,
UI_SOUND_PRESENTATION_CONTROL,
VENDOR_ID,
VENDOR_SPECIFIC_DATA,
VENDOR_SPECIFIC_RC_CODE,
};
/**
* The plain 'operand types' are uint8.
* Further uint32 'operand type' values are used to encode a sequence of upto 4 (potentially different) operands
* in the MSB bytes of an uint32 value.
*/
constexpr static uint32_t two(uint32_t first, uint32_t secnd) { return first | (secnd << 8); }
constexpr static uint32_t three(uint32_t first, uint32_t secnd, uint32_t third) {
return first | (secnd << 8) | (third << 16);
}
/**
* Helper function to implement the 'do_operand' methods:
* Append one word
* @return true if a further operand shall be decoded, false to terminate operand decoding
*/
bool append_operand_(const char *word, uint8_t offset_incr = 1);
/**
* Append one word, determined by the operand value as index to an array of names
* @return true if a further operand shall be decoded, false to terminate operand decoding
*/
template<uint32_t N_STRINGS> bool append_operand_(const std::array<const char *, N_STRINGS> &strings) {
uint32_t operand_value = frame_[offset_];
const char *s = (operand_value < N_STRINGS) ? strings[operand_value] : "?";
return append_operand_(s);
}
/**
* String tables used in the subsequent 'do_operand' decode functions
*/
const static std::array<const char *, 0x77> UI_COMMANDS;
const static std::array<const char *, 0x11> AUDIO_FORMATS;
const static std::array<const char *, 8> AUDIO_SAMPLERATES;
const static CecOpcodeTable CEC_OPCODE_TABLE;
const static VendorIdTable VENDOR_IDS;
}; // class Decoder
} // namespace esphome::hdmi_cec
-450
View File
@@ -1,450 +0,0 @@
#include "hdmi_cec.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266
#include <Arduino.h>
#endif
#ifdef USE_RP2040
#include <pico/time.h>
#endif
namespace esphome::hdmi_cec {
ESPHOME_LOG_TAG(TAG, "hdmi_cec");
#if defined(USE_ESP8266) || defined(USE_RP2040)
// The timer interrupt of these platforms carries no argument
static HDMICEC *ack_timer_owner = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif
// Bit timing from the HDMI 1.4 CEC specification, in microseconds
static constexpr uint32_t START_BIT_MIN_US = 3500; // a low period at least this long is a start bit
static constexpr uint32_t START_BIT_NOM_US = 3700;
static constexpr uint32_t START_BIT_HIGH_US = 800;
static constexpr uint32_t HIGH_BIT_MIN_US = 400; // shortest low period of a logical 1
static constexpr uint32_t HIGH_BIT_US = 600; // nominal low period of a logical 1
static constexpr uint32_t LOW_BIT_US = 1500; // nominal low period of a logical 0
static constexpr uint32_t SAFE_SAMPLE_US = 1050; // the line is sampled here; a longer low period is a logical 0
static constexpr uint32_t TOTAL_BIT_US = 2400;
static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_RECEIVE = TOTAL_BIT_US * 5; // also for a new initiator
static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_XMIT_FAIL = TOTAL_BIT_US * 3;
static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_XMIT_SUCCESS = TOTAL_BIT_US * 7;
static constexpr uint32_t ABORTED_FRAME_US = TOTAL_BIT_US * 15; // no edge for this long: our frame was cut short
static constexpr uint8_t MAX_ATTEMPTS = 8; // CEC allows 5, but a busy bus counts as an attempt here too
Frame::Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload) {
this->data_[0] = ((initiator_addr & 0xf) << 4) | (destination_addr & 0xf);
this->size_ = 1 + payload.size();
std::copy(payload.begin(), payload.end(), this->data_.begin() + 1);
}
void HDMICEC::setup() {
// The line is open drain with a weak pull-up: driven low to send, released to read
this->pin_->setup();
this->isr_pin_ = this->pin_->to_isr();
this->set_pin_input_high_();
this->required_idle_period_ = SIGNAL_FREE_TIME_AFTER_RECEIVE;
if (!this->setup_ack_timer_()) {
ESP_LOGE(TAG, "Failed to set up the acknowledge timer");
this->mark_failed();
return;
}
this->pin_->attach_interrupt(HDMICEC::gpio_isr_s, this, gpio::INTERRUPT_ANY_EDGE);
// loop() only has work once the interrupt queues a frame or send() queues one
this->disable_loop();
}
void HDMICEC::dump_config() {
ESP_LOGCONFIG(TAG,
"HDMI-CEC:\n"
" Address: 0x%X\n"
" Monitor mode: %s\n"
" Promiscuous mode: %s",
this->address_, YESNO(this->monitor_mode_), YESNO(this->promiscuous_mode_));
LOG_PIN(" Pin: ", this->pin_);
}
void HDMICEC::loop() {
const uint8_t dropped = atomic_get(this->dropped_frames_);
if (dropped != this->reported_dropped_frames_) {
const uint8_t count = dropped - this->reported_dropped_frames_;
ESP_LOGW(TAG, "%u received frame%s dropped (queue full or oversized)", count,
count == 1 ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("s"));
this->reported_dropped_frames_ = dropped;
}
// One inbound frame per pass keeps the time spent in the automations bounded
if (const Frame *frame = this->frames_queue_.front()) {
#ifdef ESPHOME_LOG_HAS_DEBUG
char line[format_hex_pretty_size(Frame::MAX_LENGTH)];
ESP_LOGD(TAG, "Received: %s", format_hex_pretty_to(line, frame->bytes().data(), frame->size()));
#endif
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
this->message_callbacks_.call(frame->initiator_addr(), frame->destination_addr(), frame->payload());
#endif
this->frames_queue_.pop_front();
}
// A frame being sent stays at the front of the queue until its attempts are over
if (!this->send_queue_.is_empty()) {
this->transmit_message_();
}
if (this->frames_queue_.is_empty() && this->send_queue_.is_empty()) {
this->disable_loop();
}
}
bool HDMICEC::send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload) {
if (this->monitor_mode_) {
ESP_LOGW(TAG, "Monitor mode, not sending 0x%X->0x%X", source, destination);
return false;
}
if (source > 0xf || destination > 0xf) {
ESP_LOGE(TAG, "Send abort: logical address 0x%X->0x%X out of range", source, destination);
return false;
}
if (payload.size() > Frame::MAX_PAYLOAD_LENGTH) {
ESP_LOGE(TAG, "Send abort 0x%X->0x%X: payload of %zu bytes exceeds %zu", source, destination, payload.size(),
Frame::MAX_PAYLOAD_LENGTH);
return false;
}
Frame *frame = this->send_queue_.back();
if (frame == nullptr) {
ESP_LOGW(TAG, "Send queue full, frame 0x%X->0x%X dropped", source, destination);
return false;
}
*frame = Frame(source, destination, payload);
this->send_queue_.push_back();
this->enable_loop();
return true;
}
void HDMICEC::transmit_message_() {
const uint32_t now = micros();
const uint32_t since_last_edge = now - atomic_get(this->last_edge_us_);
if (this->transmitting_) {
// The interrupt sees our own frame; wait for its end of message bit, or give up when the
// bus went quiet without one
if (!atomic_get(this->eom_received_) && since_last_edge < ABORTED_FRAME_US) {
return;
}
this->transmitting_ = false;
atomic_set(this->eom_received_, false); // the end of our own frame is not a foreign one
const Frame *frame = this->send_queue_.front();
// A broadcast is acknowledged by nobody; a directed frame on every byte
const bool bytes_ok = this->n_bytes_received_ == frame->size();
const bool acks_ok = this->n_acks_received_ == (frame->is_broadcast() ? 0 : frame->size());
if (bytes_ok && acks_ok) {
ESP_LOGD(TAG, "Send frame success in %u attempt%s", this->transmit_attempts_,
this->transmit_attempts_ > 1 ? LOG_STR_LITERAL("s") : LOG_STR_LITERAL(""));
} else if (!bytes_ok) {
ESP_LOGD(TAG, "Send frame incorrect on attempt %u, saw %u bytes but expected %zu", this->transmit_attempts_,
this->n_bytes_received_, frame->size());
} else {
ESP_LOGD(TAG, "Send frame not acknowledged on attempt %u", this->transmit_attempts_);
}
this->finish_attempt_(bytes_ok && acks_ok);
} else if (atomic_get(this->eom_received_)) {
// Another device finished a frame; the standard asks a new initiator to wait after it
this->required_idle_period_ = SIGNAL_FREE_TIME_AFTER_RECEIVE;
atomic_set(this->eom_received_, false);
}
const Frame *frame = this->send_queue_.front();
// An edge within the signal free time means the bus is in use or not yet free for us; a start
// bit refused by a held line drives no edge, so that attempt is timed on its own
if (frame == nullptr || since_last_edge < this->required_idle_period_ ||
now - this->last_attempt_us_ < this->required_idle_period_) {
return;
}
this->last_attempt_us_ = now;
this->transmit_attempts_++;
#ifdef ESPHOME_LOG_HAS_DEBUG
if (this->transmit_attempts_ == 1) {
char line[format_hex_pretty_size(Frame::MAX_LENGTH)];
ESP_LOGD(TAG, "Sending: %s", format_hex_pretty_to(line, frame->bytes().data(), frame->size()));
}
#endif
// The interrupt counts the bytes and acknowledge bits of our own frame
this->n_bytes_received_ = 0;
this->n_acks_received_ = 0;
atomic_set(this->eom_received_, false);
this->transmitting_ = true;
if (!this->transmit_frame_(frame)) {
// The line was held, or another initiator sent at the same time
this->transmitting_ = false;
this->finish_attempt_(false);
}
// Otherwise the acknowledge result arrives through the interrupt
}
void HDMICEC::finish_attempt_(bool sent_ok) {
this->required_idle_period_ = sent_ok ? SIGNAL_FREE_TIME_AFTER_XMIT_SUCCESS : SIGNAL_FREE_TIME_AFTER_XMIT_FAIL;
if (!sent_ok) {
if (this->transmit_attempts_ < MAX_ATTEMPTS) {
return; // the signal free time has to pass before the retry
}
ESP_LOGW(TAG, "Frame not sent after %u attempts, dropped", this->transmit_attempts_);
}
this->transmit_attempts_ = 0;
this->send_queue_.pop_front();
}
bool HDMICEC::transmit_frame_(const Frame *frame) {
if (!this->send_start_bit_()) {
return false;
}
const auto bytes = frame->bytes();
for (size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) {
const uint8_t current_byte = bytes[byte_inx];
// Most significant bit first. The four initiator address bits watch for a collision with
// another initiator that started at the same moment; a lower address wins by holding the line
for (int8_t i = 7; i >= 0; i--) {
const bool bit_value = (current_byte >> i) & 0x1;
if (!this->send_bit_(bit_value) && byte_inx == 0 && i >= 4) {
ESP_LOGD(TAG, "Bus collision on sending initiator address 0x%X on attempt %u", frame->initiator_addr(),
this->transmit_attempts_);
return false;
}
}
// End of message bit, then the acknowledge bit which the receiver may pull low
this->send_bit_(byte_inx == bytes.size() - 1);
this->send_bit_(true);
}
return true;
}
bool HDMICEC::send_start_bit_() {
if (!this->isr_pin_.digital_read()) {
ESP_LOGD(TAG, "Bus occupied before start bit on attempt %u", this->transmit_attempts_);
return false;
}
this->set_pin_output_low_();
delay_microseconds_safe(START_BIT_NOM_US);
// Release the line and check that nobody else keeps it low
this->set_pin_input_high_();
delay_microseconds_safe(START_BIT_HIGH_US / 2);
bool value = this->isr_pin_.digital_read();
delay_microseconds_safe(START_BIT_HIGH_US / 2);
value &= this->isr_pin_.digital_read();
if (!value) {
ESP_LOGD(TAG, "Bus collision on sending start bit on attempt %u", this->transmit_attempts_);
}
return value;
}
bool HDMICEC::send_bit_(bool bit_value) {
const uint32_t start_us = micros();
this->set_pin_output_low_();
delay_microseconds_safe(bit_value ? HIGH_BIT_US : LOW_BIT_US);
this->set_pin_input_high_();
bool clear = true;
if (bit_value) {
// Sample in the middle of the safe sample period: a line still low means another initiator
// is sending a logical 0 at the same time
uint32_t elapsed = micros() - start_us;
if (elapsed < SAFE_SAMPLE_US) {
delay_microseconds_safe(SAFE_SAMPLE_US - elapsed);
}
clear = this->isr_pin_.digital_read();
}
const uint32_t elapsed = micros() - start_us;
if (elapsed < TOTAL_BIT_US) {
delay_microseconds_safe(TOTAL_BIT_US - elapsed);
}
return clear;
}
void IRAM_ATTR HDMICEC::send_ack_() {
// The acknowledge has to start right at this falling edge; the timer releases the line
this->set_pin_output_low_();
this->start_ack_timer_();
}
#if defined(USE_ESP32)
bool HDMICEC::setup_ack_timer_() {
gptimer_config_t config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1000000, // one tick per microsecond
};
if (gptimer_new_timer(&config, &this->ack_timer_) != ESP_OK) {
return false;
}
gptimer_event_callbacks_t callbacks = {.on_alarm = HDMICEC::ack_timer_isr_s};
// The alarm reloads the count to zero itself, so the interrupt only has to start the timer
gptimer_alarm_config_t alarm = {
.alarm_count = LOW_BIT_US, .reload_count = 0, .flags = {.auto_reload_on_alarm = true}};
return gptimer_register_event_callbacks(this->ack_timer_, &callbacks, this) == ESP_OK &&
gptimer_set_alarm_action(this->ack_timer_, &alarm) == ESP_OK && gptimer_enable(this->ack_timer_) == ESP_OK;
}
void IRAM_ATTR HDMICEC::start_ack_timer_() { gptimer_start(this->ack_timer_); }
bool IRAM_ATTR HDMICEC::ack_timer_isr_s(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *arg) {
auto *self = static_cast<HDMICEC *>(arg);
self->set_pin_input_high_();
gptimer_stop(timer);
return false; // no task to wake
}
#elif defined(USE_ESP8266)
static constexpr uint32_t TIMER1_TICKS_PER_US = 5; // TIM_DIV16 of the 80 MHz clock
bool HDMICEC::setup_ack_timer_() {
ack_timer_owner = this;
timer1_isr_init();
timer1_attachInterrupt(HDMICEC::ack_timer_isr_s);
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); // a write arms one shot
return true;
}
void IRAM_ATTR HDMICEC::start_ack_timer_() { timer1_write(LOW_BIT_US * TIMER1_TICKS_PER_US); }
void IRAM_ATTR HDMICEC::ack_timer_isr_s() { ack_timer_owner->set_pin_input_high_(); }
#elif defined(USE_RP2040)
bool HDMICEC::setup_ack_timer_() {
this->ack_alarm_ = hardware_alarm_claim_unused(false);
if (this->ack_alarm_ < 0) {
return false; // every hardware alarm is taken
}
ack_timer_owner = this;
hardware_alarm_set_callback(this->ack_alarm_, HDMICEC::ack_timer_isr_s);
return true;
}
void IRAM_ATTR HDMICEC::start_ack_timer_() {
hardware_alarm_set_target(this->ack_alarm_, make_timeout_time_us(LOW_BIT_US));
}
void IRAM_ATTR HDMICEC::ack_timer_isr_s(uint alarm_num) { ack_timer_owner->set_pin_input_high_(); }
#else
bool HDMICEC::setup_ack_timer_() { return true; }
// Without a one-shot timer the interrupt holds the line itself
void IRAM_ATTR HDMICEC::start_ack_timer_() {
delay_microseconds_safe(LOW_BIT_US);
this->set_pin_input_high_();
}
#endif
void IRAM_ATTR HDMICEC::gpio_isr_s(HDMICEC *self) { self->gpio_isr_(); }
void IRAM_ATTR HDMICEC::gpio_isr_() {
const uint32_t now = micros();
const bool level = this->isr_pin_.digital_read();
if (level == this->prev_pin_level_) {
return; // a mode change of the pin, not a bus edge
}
this->prev_pin_level_ = level;
atomic_set(this->last_edge_us_, now);
if (!level) {
// Falling edge: the start of a low period
if (now - this->last_falling_edge_us_ > START_BIT_NOM_US + TOTAL_BIT_US) {
// The bus was quiet; whatever was gathered belongs to an aborted frame
this->reset_receiver_();
}
this->last_falling_edge_us_ = now;
if (this->recv_ack_queued_) {
// Acknowledge by stretching this low period
this->send_ack_();
this->recv_ack_queued_ = false;
}
return;
}
// Rising edge: the low period tells the bit value
const uint32_t pulse_duration = now - this->last_falling_edge_us_;
if (pulse_duration > START_BIT_MIN_US) {
this->reset_receiver_();
this->receiver_state_ = ReceiverState::RECEIVER_STATE_RECEIVING_BYTE;
// Without a free slot the frame is neither acknowledged nor delivered, so its initiator retries
this->recv_frame_ = this->frames_queue_.back();
if (this->recv_frame_ != nullptr) {
this->recv_frame_->clear();
}
return;
}
if (pulse_duration < HIGH_BIT_MIN_US / 4) {
return; // glitch
}
const bool value = pulse_duration <= SAFE_SAMPLE_US;
// Our own frame is on the bus until its end of message; send() may give it any initiator address
const bool own_frame = this->transmitting_ && !atomic_get(this->eom_received_);
switch (this->receiver_state_) {
case ReceiverState::RECEIVER_STATE_IDLE:
break;
case ReceiverState::RECEIVER_STATE_RECEIVING_BYTE:
this->recv_byte_ = (this->recv_byte_ << 1) | value;
if (++this->recv_bit_counter_ == 8) {
if (this->recv_length_++ == 0) {
this->recv_header_ = this->recv_byte_;
}
if (this->recv_frame_ != nullptr && !this->recv_frame_->push_back(this->recv_byte_)) {
this->recv_frame_ = nullptr; // longer than the bus allows: corrupt, do not deliver it
}
this->recv_bit_counter_ = 0;
this->receiver_state_ = ReceiverState::RECEIVER_STATE_WAITING_FOR_EOM;
}
break;
case ReceiverState::RECEIVER_STATE_WAITING_FOR_EOM:
this->recv_eom_ = value;
// A frame for us is acknowledged on the next bit, unless there is no room for it (so its
// initiator retries) or it is our own, which includes a polling message from a device
// claiming our logical address. A polling message for us (header only) needs no room, and
// goes unacknowledged only when nobody holds the address
if (!this->monitor_mode_ && !own_frame && (this->recv_frame_ != nullptr || (value && this->recv_length_ == 1)) &&
(this->recv_header_ & 0xf) == this->address_ && (this->recv_header_ >> 4) != this->address_) {
this->recv_ack_queued_ = true;
}
this->receiver_state_ = ReceiverState::RECEIVER_STATE_WAITING_FOR_ACK;
break;
case ReceiverState::RECEIVER_STATE_WAITING_FOR_ACK:
if (own_frame) {
// A low acknowledge bit means the addressed device took the byte
// (for a broadcast it means some device refused it)
this->n_bytes_received_++;
if (!value) {
this->n_acks_received_++;
}
}
if (!this->recv_eom_) {
this->receiver_state_ = ReceiverState::RECEIVER_STATE_RECEIVING_BYTE;
break;
}
atomic_set(this->eom_received_, true); // publishes the tally above to loop()
// The application only gets frames that are not our own and not a polling message (header
// only), which the acknowledge answered already, and that are addressed to us unless
// promiscuous
const uint8_t destination = this->recv_header_ & 0xf;
const bool wanted = this->recv_length_ > 1 && !own_frame && (this->recv_header_ >> 4) != this->address_ &&
(this->promiscuous_mode_ || destination == 0xf || destination == this->address_);
if (!wanted) {
// nothing to deliver
} else if (this->recv_frame_ == nullptr) {
atomic_set<uint8_t>(this->dropped_frames_, atomic_get(this->dropped_frames_) + 1);
} else {
this->frames_queue_.push_back();
this->enable_loop_soon_any_context();
}
this->reset_receiver_();
break;
}
}
void IRAM_ATTR HDMICEC::reset_receiver_() {
this->receiver_state_ = ReceiverState::RECEIVER_STATE_IDLE;
this->recv_bit_counter_ = 0;
this->recv_length_ = 0;
this->recv_ack_queued_ = false;
this->recv_frame_ = nullptr;
}
} // namespace esphome::hdmi_cec
-218
View File
@@ -1,218 +0,0 @@
#pragma once
#include <array>
#include <atomic>
#include <initializer_list>
#include <span>
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32
#include "driver/gptimer.h"
#endif
#ifdef USE_RP2040
#include <hardware/timer.h>
#endif
namespace esphome::hdmi_cec {
// Release/acquire store and load. The read-modify-write forms of std::atomic are not available on
// every target (the ESP8266 has none), so a writer computes the new value itself; that is safe here
// because every atomic in this component has exactly one writer.
template<typename T> ESPHOME_ALWAYS_INLINE void atomic_set(std::atomic<T> &atom, T value) {
atom.store(value, std::memory_order_release);
}
template<typename T> ESPHOME_ALWAYS_INLINE T atomic_get(const std::atomic<T> &atom) {
return atom.load(std::memory_order_acquire);
}
/// One CEC bus message: a header byte with the initiator and destination addresses, then up to
/// 15 payload bytes of which the first is the opcode. Fixed storage, so the receive interrupt
/// never allocates.
class Frame {
public:
static constexpr size_t MAX_LENGTH = 16; // HDMI-CEC 1.4
static constexpr size_t MAX_PAYLOAD_LENGTH = MAX_LENGTH - 1;
Frame() = default;
/// The payload is at most MAX_PAYLOAD_LENGTH bytes; send() checks that
Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload);
ESPHOME_ALWAYS_INLINE uint8_t initiator_addr() const { return this->data_[0] >> 4; }
ESPHOME_ALWAYS_INLINE uint8_t destination_addr() const { return this->data_[0] & 0xf; }
ESPHOME_ALWAYS_INLINE bool is_broadcast() const { return this->destination_addr() == 0xf; }
ESPHOME_ALWAYS_INLINE size_t size() const { return this->size_; }
std::span<const uint8_t> bytes() const { return {this->data_.data(), this->size_}; }
/// The bytes after the header, starting with the opcode
std::span<const uint8_t> payload() const { return this->bytes().subspan(1); }
ESPHOME_ALWAYS_INLINE void clear() { this->size_ = 0; }
/// False when the frame is full already; the bus allows no more
ESPHOME_ALWAYS_INLINE bool push_back(uint8_t data) {
if (this->size_ == MAX_LENGTH) {
return false;
}
this->data_[this->size_++] = data;
return true;
}
protected:
std::array<uint8_t, MAX_LENGTH> data_{};
uint8_t size_{0};
};
/// A queue of frames between one producer and one consumer, for instance the receive interrupt and
/// loop(). The indices run freely and each is written by one side only, so no lock is needed.
/// back() hands out a slot to fill and push_back() commits it; front() and pop_front() do the same
/// on the reading side.
template<size_t SIZE> class FrameRingBuffer {
static_assert((SIZE & (SIZE - 1)) == 0, "SIZE must be a power of two");
public:
ESPHOME_ALWAYS_INLINE Frame *front() { return this->is_empty() ? nullptr : this->slot_(this->front_inx_); }
ESPHOME_ALWAYS_INLINE void pop_front() { this->advance_(this->front_inx_); }
ESPHOME_ALWAYS_INLINE Frame *back() { return this->count_() == SIZE ? nullptr : this->slot_(this->back_inx_); }
ESPHOME_ALWAYS_INLINE void push_back() { this->advance_(this->back_inx_); }
ESPHOME_ALWAYS_INLINE bool is_empty() const { return this->count_() == 0; }
protected:
ESPHOME_ALWAYS_INLINE uint8_t count_() const {
return static_cast<uint8_t>(atomic_get(this->back_inx_) - atomic_get(this->front_inx_));
}
ESPHOME_ALWAYS_INLINE Frame *slot_(const std::atomic<uint8_t> &inx) { return &this->store_[atomic_get(inx) % SIZE]; }
ESPHOME_ALWAYS_INLINE void advance_(std::atomic<uint8_t> &inx) {
atomic_set<uint8_t>(inx, static_cast<uint8_t>(atomic_get(inx) + 1));
}
std::array<Frame, SIZE> store_{};
std::atomic<uint8_t> front_inx_{0};
std::atomic<uint8_t> back_inx_{0};
};
/// The HDMI-CEC bus on one GPIO pin. The receive interrupt decodes the edges of every frame,
/// including those this device sends, and queues complete frames for loop(); loop() runs the
/// on_message automations and transmits queued frames, one blocking bit-banged attempt per pass.
class HDMICEC : public Component {
public:
HDMICEC(InternalGPIOPin *pin, uint8_t address) : pin_(pin), address_(address) {}
void setup() override;
void dump_config() override;
void loop() override;
void set_promiscuous_mode(bool promiscuous) { this->promiscuous_mode_ = promiscuous; }
void set_monitor_mode(bool monitor_mode) { this->monitor_mode_ = monitor_mode; }
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
/// Called with the initiator, the destination and the payload (opcode first) of every received
/// frame; the payload is only valid during the call
template<typename F> void add_on_message_callback(F &&callback) {
this->message_callbacks_.add(std::forward<F>(callback));
}
#endif
/// Queue a frame for transmission; false when the queue is full or in monitor mode
bool send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload);
bool send(uint8_t destination, std::span<const uint8_t> payload) {
return this->send(this->address_, destination, payload);
}
/// For lambdas: id(cec).send(0x0, {0x44, 0x41});
bool send(uint8_t destination, std::initializer_list<uint8_t> payload) {
return this->send(destination, std::span<const uint8_t>(payload.begin(), payload.size()));
}
protected:
enum class ReceiverState : uint8_t {
RECEIVER_STATE_IDLE,
RECEIVER_STATE_RECEIVING_BYTE,
RECEIVER_STATE_WAITING_FOR_EOM,
RECEIVER_STATE_WAITING_FOR_ACK,
};
// Receive side, in the interrupt
static void gpio_isr_s(HDMICEC *self);
void gpio_isr_();
void reset_receiver_();
/// Pull the line low for the acknowledge bit of a frame addressed to us
void send_ack_();
// A hardware one-shot timer releases the line after the acknowledge bit (gptimer on the ESP32,
// timer 1 on the ESP8266, a hardware alarm on the RP2040), so the interrupt returns at once;
// without one the interrupt holds the line for the 1.5 ms itself
bool setup_ack_timer_();
void start_ack_timer_();
#ifdef USE_ESP32
static bool ack_timer_isr_s(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *arg);
#endif
#ifdef USE_ESP8266
static void ack_timer_isr_s();
#endif
#ifdef USE_RP2040
static void ack_timer_isr_s(uint alarm_num);
#endif
// Transmit side, in loop()
void transmit_message_();
/// Keep the frame for another attempt, or drop it once the attempts are used up
void finish_attempt_(bool sent_ok);
/// Drive the frame onto the line; false when another initiator sent at the same time
bool transmit_frame_(const Frame *frame);
bool send_start_bit_();
/// Send one bit; false when a logical 1 found the line held low by another initiator
bool send_bit_(bool bit_value);
ESPHOME_ALWAYS_INLINE void set_pin_input_high_() { this->isr_pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
ESPHOME_ALWAYS_INLINE void set_pin_output_low_() {
this->isr_pin_.pin_mode(gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
this->isr_pin_.digital_write(false);
}
static constexpr size_t QUEUE_SIZE = 4;
// Each queue is 70 bytes, so two single-byte fields follow each to fill the alignment gap
FrameRingBuffer<QUEUE_SIZE> frames_queue_; // received, interrupt to loop()
uint8_t recv_bit_counter_{0};
uint8_t recv_byte_{0};
FrameRingBuffer<QUEUE_SIZE> send_queue_; // to send, both ends on loop()
uint8_t transmit_attempts_{0};
uint8_t reported_dropped_frames_{0};
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
StaticCallbackManager<HDMI_CEC_MESSAGE_CALLBACK_COUNT, void(uint8_t, uint8_t, std::span<const uint8_t>)>
message_callbacks_;
#endif
InternalGPIOPin *pin_;
ISRInternalGPIOPin isr_pin_;
#ifdef USE_ESP32
gptimer_handle_t ack_timer_{nullptr};
#endif
#ifdef USE_RP2040
int ack_alarm_{-1};
#endif
// The word-sized fields come first and the single-byte ones form one run, so nothing is padded
std::atomic<uint32_t> last_edge_us_{0}; // the bus is busy until this is some time ago; interrupt writes
Frame *recv_frame_{nullptr}; // interrupt only; null when the queue was full or the frame too long
uint32_t last_falling_edge_us_{0}; // interrupt only
uint32_t last_attempt_us_{0}; // loop() only; a refused start bit drives no edge, so it is timed here
uint16_t required_idle_period_{0}; // loop() only
// Shared with the interrupt. The tally of our own frame is written by the interrupt while
// transmitting_ is set and no end of message has passed yet; loop() reads it once eom_received_
// says the frame is over
std::atomic<bool> eom_received_{false}; // set for every frame, ours or not
std::atomic<uint8_t> dropped_frames_{0};
uint8_t n_bytes_received_{0};
uint8_t n_acks_received_{0};
bool transmitting_{false};
// Receive state, interrupt only
uint8_t recv_header_{0}; // kept outside the frame so a dropped frame can still be judged
uint8_t recv_length_{0};
ReceiverState receiver_state_{ReceiverState::RECEIVER_STATE_IDLE};
bool recv_eom_{false};
bool recv_ack_queued_{false};
bool prev_pin_level_{true};
uint8_t address_; // logical address, 0xf when only listening
bool promiscuous_mode_{false};
bool monitor_mode_{false};
};
} // namespace esphome::hdmi_cec
@@ -5,7 +5,6 @@
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
@@ -32,21 +31,22 @@ void HomeassistantButton::press_action() {
}
#endif
// In flash on the ESP8266 and encoded from there
static const char SERVICE_BUTTON[] PROGMEM = "button.press";
static const char SERVICE_INPUT_BUTTON[] PROGMEM = "input_button.press";
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
static constexpr auto SERVICE_BUTTON = StringRef::from_lit("button.press");
static constexpr auto SERVICE_INPUT_BUTTON = StringRef::from_lit("input_button.press");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr char INPUT_PREFIX[] = "input_";
api::HomeassistantActionRequest resp;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper = ESPHOME_strncmp_P(this->entity_id_, SERVICE_INPUT_BUTTON, sizeof("input_") - 1) == 0;
resp.service =
input_helper ? ProgmemStringRef::from_lit(SERVICE_INPUT_BUTTON) : ProgmemStringRef::from_lit(SERVICE_BUTTON);
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = SERVICE_INPUT_BUTTON;
} else {
resp.service = SERVICE_BUTTON;
}
resp.data.init(1);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
entity_id_kv.value = ProgmemStringRef(StringRef(this->entity_id_));
entity_id_kv.key = ENTITY_ID_KEY;
entity_id_kv.value = StringRef(this->entity_id_);
api::global_api_server->send_homeassistant_action(resp);
}
@@ -5,7 +5,6 @@
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
@@ -90,29 +89,31 @@ void HomeassistantNumber::control(float value) {
this->publish_state(value);
// In flash on the ESP8266 and encoded from there
static const char NUMBER_SERVICE_NAME[] PROGMEM = "number.set_value";
static const char INPUT_NUMBER_SERVICE_NAME[] PROGMEM = "input_number.set_value";
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
static const char VALUE_KEY[] PROGMEM = "value";
static constexpr auto NUMBER_SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto INPUT_NUMBER_SERVICE_NAME = StringRef::from_lit("input_number.set_value");
static constexpr char INPUT_PREFIX[] = "input_";
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
api::HomeassistantActionRequest resp;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper = ESPHOME_strncmp_P(this->entity_id_, INPUT_NUMBER_SERVICE_NAME, sizeof("input_") - 1) == 0;
resp.service = input_helper ? ProgmemStringRef::from_lit(INPUT_NUMBER_SERVICE_NAME)
: ProgmemStringRef::from_lit(NUMBER_SERVICE_NAME);
// input_number entities have their own set_value action
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = INPUT_NUMBER_SERVICE_NAME;
} else {
resp.service = NUMBER_SERVICE_NAME;
}
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
entity_id.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
entity_id.value = ProgmemStringRef(StringRef(this->entity_id_));
entity_id.key = ENTITY_ID_KEY;
entity_id.value = StringRef(this->entity_id_);
auto &entity_value = resp.data.emplace_back();
entity_value.key = ProgmemStringRef::from_lit(VALUE_KEY);
entity_value.key = VALUE_KEY;
// Stack buffer - no heap allocation; %g produces shortest representation
char value_buf[16];
buf_append_printf(value_buf, sizeof(value_buf), 0, "%g", value);
entity_value.value = ProgmemStringRef(StringRef(value_buf));
entity_value.value = StringRef(value_buf);
api::global_api_server->send_homeassistant_action(resp);
}
@@ -6,7 +6,6 @@
#include "esphome/components/api/api_server.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
@@ -238,26 +237,27 @@ void HomeassistantSelect::control(size_t index) {
}
#endif
// In flash on the ESP8266 and encoded from there
static const char SERVICE_SELECT[] PROGMEM = "select.select_option";
static const char SERVICE_INPUT_SELECT[] PROGMEM = "input_select.select_option";
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
static const char OPTION_KEY[] PROGMEM = "option";
static constexpr auto SERVICE_SELECT = StringRef::from_lit("select.select_option");
static constexpr auto SERVICE_INPUT_SELECT = StringRef::from_lit("input_select.select_option");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto OPTION_KEY = StringRef::from_lit("option");
static constexpr char INPUT_PREFIX[] = "input_";
api::HomeassistantActionRequest resp;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper = ESPHOME_strncmp_P(this->entity_id_, SERVICE_INPUT_SELECT, sizeof("input_") - 1) == 0;
resp.service =
input_helper ? ProgmemStringRef::from_lit(SERVICE_INPUT_SELECT) : ProgmemStringRef::from_lit(SERVICE_SELECT);
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = SERVICE_INPUT_SELECT;
} else {
resp.service = SERVICE_SELECT;
}
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
entity_id.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
entity_id.value = ProgmemStringRef(StringRef(this->entity_id_));
entity_id.key = ENTITY_ID_KEY;
entity_id.value = StringRef(this->entity_id_);
auto &option = resp.data.emplace_back();
option.key = ProgmemStringRef::from_lit(OPTION_KEY);
option.value = ProgmemStringRef(StringRef(this->option_at(index)));
option.key = OPTION_KEY;
option.value = StringRef(this->option_at(index));
api::global_api_server->send_homeassistant_action(resp);
}
@@ -1,7 +1,6 @@
#include "homeassistant_switch.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
@@ -48,18 +47,21 @@ void HomeassistantSwitch::write_state(bool state) {
}
#endif
// In flash on the ESP8266 and encoded from there
static const char SERVICE_ON[] PROGMEM = "homeassistant.turn_on";
static const char SERVICE_OFF[] PROGMEM = "homeassistant.turn_off";
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
static constexpr auto SERVICE_ON = StringRef::from_lit("homeassistant.turn_on");
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
api::HomeassistantActionRequest resp;
resp.service = state ? ProgmemStringRef::from_lit(SERVICE_ON) : ProgmemStringRef::from_lit(SERVICE_OFF);
if (state) {
resp.service = SERVICE_ON;
} else {
resp.service = SERVICE_OFF;
}
resp.data.init(1);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
entity_id_kv.value = ProgmemStringRef(StringRef(this->entity_id_));
entity_id_kv.key = ENTITY_ID_KEY;
entity_id_kv.value = StringRef(this->entity_id_);
api::global_api_server->send_homeassistant_action(resp);
}
@@ -6,7 +6,6 @@
#include "esphome/components/api/api_server.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
@@ -87,26 +86,27 @@ void HomeassistantText::control(const std::string &value) {
}
#endif
// In flash on the ESP8266 and encoded from there
static const char SERVICE_TEXT[] PROGMEM = "text.set_value";
static const char SERVICE_INPUT_TEXT[] PROGMEM = "input_text.set_value";
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
static const char VALUE_KEY[] PROGMEM = "value";
static constexpr auto SERVICE_TEXT = StringRef::from_lit("text.set_value");
static constexpr auto SERVICE_INPUT_TEXT = StringRef::from_lit("input_text.set_value");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
static constexpr char INPUT_PREFIX[] = "input_";
api::HomeassistantActionRequest resp;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper = ESPHOME_strncmp_P(this->entity_id_, SERVICE_INPUT_TEXT, sizeof("input_") - 1) == 0;
resp.service =
input_helper ? ProgmemStringRef::from_lit(SERVICE_INPUT_TEXT) : ProgmemStringRef::from_lit(SERVICE_TEXT);
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = SERVICE_INPUT_TEXT;
} else {
resp.service = SERVICE_TEXT;
}
resp.data.init(2);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
entity_id_kv.value = ProgmemStringRef(StringRef(this->entity_id_));
entity_id_kv.key = ENTITY_ID_KEY;
entity_id_kv.value = StringRef(this->entity_id_);
auto &value_kv = resp.data.emplace_back();
value_kv.key = ProgmemStringRef::from_lit(VALUE_KEY);
value_kv.value = ProgmemStringRef(value.data(), value.size());
value_kv.key = VALUE_KEY;
value_kv.value = StringRef(value.data(), value.size());
api::global_api_server->send_homeassistant_action(resp);
}
+38 -36
View File
@@ -1,42 +1,15 @@
#include "lsm6ds.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::lsm6ds {
ESPHOME_LOG_TAG(TAG, "lsm6ds");
// Indexed by the accel range field, which is not in ascending order (datasheet Table 3)
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±16g", "±4g", "±8g");
static const LogString *gyro_range_to_str(LSM6DSGyroRange range) {
switch (range) {
case LSM6DS_GYRO_RANGE_125:
return LOG_STR("±125dps");
case LSM6DS_GYRO_RANGE_250:
return LOG_STR("±250dps");
case LSM6DS_GYRO_RANGE_500:
return LOG_STR("±500dps");
case LSM6DS_GYRO_RANGE_1000:
return LOG_STR("±1000dps");
case LSM6DS_GYRO_RANGE_2000:
return LOG_STR("±2000dps");
default:
return LOG_STR("unknown");
}
}
static const LogString *chip_name_from_who_am_i(uint8_t who_am_i) {
switch (who_am_i) {
case 0x69:
return LOG_STR("LSMDSO");
case 0x6A:
return LOG_STR("LSM6DS3");
default:
return nullptr;
}
}
static const struct {
uint8_t who_am_i;
const char *const name;
} CHIP_IDS[] = {{0x69, "LSMDSO"}, {0x6A, "LSM6DS3"}};
void LSM6DSComponent::setup() {
MotionComponent::setup();
@@ -46,13 +19,19 @@ void LSM6DSComponent::setup() {
this->mark_failed();
return;
}
const LogString *chip_name = chip_name_from_who_am_i(who_am_i);
const char *chip_name = nullptr;
for (const auto &chip : CHIP_IDS) {
if (chip.who_am_i == who_am_i) {
chip_name = chip.name;
break;
}
}
if (chip_name == nullptr) {
ESP_LOGE(TAG, "Unknown WHO_AM_I: 0x%02X", who_am_i);
this->mark_failed(LOG_STR("Unknown WHO_AM_I value"));
return;
}
ESP_LOGD(TAG, "Found %s (WHO_AM_I = 0x%02X)", LOG_STR_ARG(chip_name), who_am_i);
ESP_LOGD(TAG, "Found %s (WHO_AM_I = 0x%02X)", chip_name, who_am_i);
this->chip_name_ = chip_name;
// 2. Software reset — clears all registers to defaults
@@ -106,10 +85,34 @@ void LSM6DSComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"LSM6DS IMU:\n"
" Chip type: %s\n",
LOG_STR_ARG(this->chip_name_ != nullptr ? this->chip_name_ : LOG_STR("Unknown")));
this->chip_name_);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
// Accel range — index into the sensitivity table (datasheet Table 3)
static const char *const ACCEL_RANGE_STR[] = {"±2g", "±16g", "±4g", "±8g"};
const char *gyro_str;
switch (this->gyro_range_) {
case LSM6DS_GYRO_RANGE_125:
gyro_str = "±125dps";
break;
case LSM6DS_GYRO_RANGE_250:
gyro_str = "±250dps";
break;
case LSM6DS_GYRO_RANGE_500:
gyro_str = "±500dps";
break;
case LSM6DS_GYRO_RANGE_1000:
gyro_str = "±1000dps";
break;
case LSM6DS_GYRO_RANGE_2000:
gyro_str = "±2000dps";
break;
default:
gyro_str = "unknown";
break;
}
auto accel_odr = this->accel_odr_ == 0 ? 0 : 13 * (1 << (this->accel_odr_ - 1));
auto gyro_odr = this->gyro_odr_ == 0 ? 0 : 13 * (1 << (this->gyro_odr_ - 1));
ESP_LOGCONFIG(TAG,
@@ -117,8 +120,7 @@ void LSM6DSComponent::dump_config() {
" Accel data rate : %dHz\n"
" Gyro range : %s\n"
" Gyro data rate : %dHz",
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_, AccelRangeStrings::LAST_INDEX)),
accel_odr, LOG_STR_ARG(gyro_range_to_str(this->gyro_range_)), gyro_odr);
ACCEL_RANGE_STR[this->accel_range_], accel_odr, gyro_str, gyro_odr);
}
// update_data()
+1 -2
View File
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/motion/motion_component.h"
@@ -98,7 +97,7 @@ class LSM6DSComponent final : public motion::MotionComponent, public i2c::I2CDev
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
protected:
const LogString *chip_name_{nullptr};
const char *chip_name_{"Unknown"};
bool update_data(motion::MotionData &data) override;
LSM6DSAccelRange accel_range_{LSM6DS_ACCEL_RANGE_4G};
@@ -13,7 +13,6 @@ from esphome.components.http_request import validate_url
import esphome.config_validation as cv
from esphome.const import (
CONF_FILE,
CONF_FRAMEWORK,
CONF_ID,
CONF_INTERNAL,
CONF_MICROPHONE,
@@ -27,12 +26,10 @@ from esphome.const import (
CONF_TYPE,
CONF_URL,
CONF_USERNAME,
PLATFORM_ESP32,
TYPE_GIT,
TYPE_LOCAL,
)
from esphome.core import CORE, HexInt
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -44,8 +41,6 @@ DEPENDENCIES = ["microphone"]
DOMAIN = "micro_wake_word"
CONF_TASK_CORE = "task_core"
CONF_FEATURE_STEP_SIZE = "feature_step_size"
CONF_MODELS = "models"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
@@ -412,27 +407,6 @@ def _download_http_models(config: ConfigType) -> ConfigType:
return config
def _validate_task_core(config: ConfigType) -> ConfigType:
if config[CONF_TASK_CORE] != 1:
return config
esp32.only_on_variant(
supported=[
esp32.VARIANT_ESP32,
esp32.VARIANT_ESP32S3,
esp32.VARIANT_ESP32P4,
esp32.VARIANT_ESP32H4,
esp32.VARIANT_ESP32S31,
],
msg_prefix="micro_wake_word task_core: 1",
)(config)
sdkconfig = fv.full_config.get()[PLATFORM_ESP32][CONF_FRAMEWORK][
esp32.CONF_SDKCONFIG_OPTIONS
]
if sdkconfig.get("CONFIG_FREERTOS_UNICORE") == "y":
raise cv.Invalid("task_core: 1 requires FreeRTOS dual-core mode")
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -453,7 +427,6 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_VAD): _maybe_empty_vad_schema,
cv.Optional(CONF_STOP_AFTER_DETECTION, default=True): cv.boolean,
cv.Optional(CONF_TASK_CORE, default=-1): cv.int_range(min=-1, max=1),
cv.Optional(CONF_PREFER_INTERNAL_RAM, default=False): cv.boolean,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_MODEL): cv.invalid(
@@ -536,7 +509,6 @@ FINAL_VALIDATE_SCHEMA = cv.All(
},
extra=cv.ALLOW_EXTRA,
),
_validate_task_core,
_feature_step_size_validate,
)
@@ -549,12 +521,6 @@ async def to_code(config):
cg.add(var.set_microphone_source(mic_source))
cg.add_define("USE_MICRO_WAKE_WORD")
cg.add_define(
"MICRO_WAKE_WORD_TASK_CORE",
cg.RawExpression("tskNO_AFFINITY")
if config[CONF_TASK_CORE] == -1
else config[CONF_TASK_CORE],
)
if config[CONF_PREFER_INTERNAL_RAM]:
cg.add_define("USE_MICRO_WAKE_WORD_INTERNAL_RAM")
ota.request_ota_state_listeners()
@@ -17,10 +17,6 @@
namespace esphome::micro_wake_word {
// Config validation checks the YAML, this checks the build: a unicore sdkconfig reached any other way
static_assert(MICRO_WAKE_WORD_TASK_CORE == tskNO_AFFINITY || MICRO_WAKE_WORD_TASK_CORE < portNUM_PROCESSORS,
"micro_wake_word task_core is not a core of this build");
ESPHOME_LOG_TAG(TAG, "micro_wake_word");
static const ssize_t DETECTION_QUEUE_LENGTH = 5;
@@ -88,12 +84,6 @@ static const LogString *micro_wake_word_state_to_string(State state) {
void MicroWakeWord::dump_config() {
ESP_LOGCONFIG(TAG, "microWakeWord:");
// tskNO_AFFINITY is a cast expression, so this cannot be a preprocessor test; the compiler folds it
if (MICRO_WAKE_WORD_TASK_CORE == tskNO_AFFINITY) {
ESP_LOGCONFIG(TAG, " Inference task core: any");
} else {
ESP_LOGCONFIG(TAG, " Inference task core: %d", MICRO_WAKE_WORD_TASK_CORE);
}
ESP_LOGCONFIG(TAG, " models:");
for (auto &model : this->wake_word_models_) {
model->log_model_config();
@@ -487,8 +477,7 @@ void MicroWakeWord::loop() {
}
if (!this->inference_task_.create(MicroWakeWord::inference_task, "mww", INFERENCE_TASK_STACK_SIZE,
(void *) this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_,
MICRO_WAKE_WORD_TASK_CORE)) {
(void *) this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_)) {
FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state
this->status_momentary_error(1000);
}
@@ -9,10 +9,6 @@ class RgbDriverChip(DriverChip):
def transforms(self) -> set[str]:
"""
Return the set of transformations supported by this driver chip.
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY.
A model with an empty init sequence has no command interface at all, so it
cannot apply any transform in hardware.
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY
"""
if self.initsequence is not None and len(self.initsequence) == 0:
return set()
return super().transforms - {CONF_SWAP_XY}
+4 -29
View File
@@ -27,8 +27,7 @@ _LOGGER = logging.getLogger(__name__)
# High performance networking tracking infrastructure
# Components can request high performance networking and this configures lwip and WiFi settings
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" # lwip tier
KEY_HIGH_PERFORMANCE_WIFI = "high_performance_wifi" # WiFi driver buffers
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
CONF_TCP_SEND_BUFFER = "tcp_send_buffer"
@@ -226,26 +225,9 @@ def require_high_performance_networking() -> None:
_request_high_performance_networking,
)
"""
CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True
require_high_performance_wifi()
def require_high_performance_wifi() -> None:
"""Request only the WiFi driver part of high performance networking.
For traffic that passes through the device below its own sockets, such as the
NAT of a WISP access point: more RX/TX buffers, AMPDU aggregation and a deeper
lwip input mailbox help every forwarded packet, while the larger TCP windows
would only serve the device's own connections and cost internal RAM for nothing.
``network: enable_high_performance: false`` turns the mailbox part off like the
lwip tier; the WiFi driver settings follow the component request alone.
"""
CORE.data[KEY_HIGH_PERFORMANCE_WIFI] = True
def has_high_performance_wifi() -> bool:
"""True when a component asked for the high performance WiFi driver settings."""
return CORE.data.get(KEY_HIGH_PERFORMANCE_WIFI, False)
# Only set up once (idempotent - multiple components can call this)
if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False):
CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True
def has_high_performance_networking() -> bool:
@@ -537,13 +519,6 @@ async def to_code(config: ConfigType) -> None:
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", tcp_window)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", tcpip_mailbox)
elif (
CORE.is_esp32 and enable_high_perf is not False and has_high_performance_wifi()
):
# Forwarded frames still queue through tcpip_input, so the WiFi-only tier deepens that mailbox too
add_idf_sdkconfig_option(
"CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", TCPIP_RECVMBOX_OPTIMIZED
)
# After the high performance block so an explicit size wins over the
# bundle's 65534 (last write wins in the sdkconfig store).
@@ -58,9 +58,6 @@ void OpenThreadComponent::on_state_changed(otChangedFlags flags, void *context)
// Gets the off-mesh routable address
std::optional<otIp6Address> OpenThreadComponent::get_omr_address() {
// A failed setup never creates the lock, and acquire() would abort waiting for it
if (this->is_failed())
return {};
InstanceLock lock = InstanceLock::acquire();
return this->get_omr_address_(lock);
}
@@ -125,10 +122,6 @@ void OpenThreadSrpComponent::srp_factory_reset_callback(otError err, const otSrp
}
void OpenThreadSrpComponent::setup() {
if (global_openthread_component->is_failed()) {
this->mark_failed();
return;
}
otError error;
InstanceLock lock = InstanceLock::acquire();
otInstance *instance = lock.get_instance();
@@ -235,9 +228,6 @@ void *OpenThreadSrpComponent::pool_alloc_(size_t size) {
}
bool OpenThreadComponent::teardown() {
// Nothing was started
if (this->is_failed())
return true;
switch (this->teardown_stage_) {
case TeardownStage::TEARDOWN_STAGE_NOT_STARTED: {
{
@@ -282,11 +272,6 @@ bool OpenThreadComponent::teardown() {
}
void OpenThreadComponent::on_factory_reset(std::function<void()> callback) {
if (this->is_failed()) {
// No SRP registration to remove; reset right away
callback();
return;
}
this->factory_reset_external_callback_ = std::move(callback);
ESP_LOGD(TAG, "Start Removal SRP Host and Services");
otError error;
@@ -10,12 +10,12 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include "esp_err.h"
#include "esp_netif.h"
#include "esp_openthread_netif_glue.h"
#include "esp_vfs_eventfd.h"
#include "nvs_flash.h"
ESPHOME_LOG_TAG(TAG, "openthread");
@@ -29,14 +29,8 @@ void OpenThreadComponent::setup() {
esp_vfs_eventfd_config_t eventfd_config = {
.max_fds = 3,
};
// Network interface setup handled by network component.
// Preferences opens NVS in app_main(); OpenThread keeps its dataset there, and esp_openthread_start()
// aborts without it, so fail this component instead.
if (global_preferences->nvs_handle == 0) {
ESP_LOGE(TAG, "NVS unavailable");
this->mark_failed();
return;
}
// Network interface setup handled by network component
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
esp_openthread_config_t config = {.netif_config = ESP_NETIF_DEFAULT_OPENTHREAD(),
@@ -159,11 +153,6 @@ InstanceLock InstanceLock::acquire() {
ESP_LOGE(TAG, "OpenThread component torn down, cannot acquire instance lock");
abort();
}
// A failed setup() never creates the lock; say so now instead of after the wait below
if (global_openthread_component->is_failed()) {
ESP_LOGE(TAG, "Setup failed, cannot acquire instance lock");
abort();
}
// Wait for the lock to be created before attempting to acquire it.
// esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_start().
constexpr uint32_t lock_init_timeout_ms = 10000;
+6 -9
View File
@@ -1,16 +1,11 @@
#include "qmi8658.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::qmi8658 {
ESPHOME_LOG_TAG(TAG, "qmi8658");
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±4g", "±8g", "±16g");
PROGMEM_STRING_TABLE(GyroRangeStrings, "±16°/s", "±32°/s", "±64°/s", "±128°/s", "±256°/s", "±512°/s", "±1024°/s",
"±2048°/s");
// Acceleration scale (g per LSB), indexed by accel_range_ >> 4.
// Full-scale = range_g, mapped over a signed 16-bit value (2^15 counts).
static constexpr float ACCEL_SCALE[] = {
@@ -91,10 +86,12 @@ void QMI8658Component::dump_config() {
return;
}
ESP_LOGCONFIG(TAG, " Accel range : %s",
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_ >> 4, AccelRangeStrings::LAST_INDEX)));
ESP_LOGCONFIG(TAG, " Gyro range : %s",
LOG_STR_ARG(GyroRangeStrings::get_log_str(this->gyro_range_ >> 4, GyroRangeStrings::LAST_INDEX)));
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±16°/s", "±32°/s", "±64°/s", "±128°/s",
"±256°/s", "±512°/s", "±1024°/s", "±2048°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[this->accel_range_ >> 4]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[this->gyro_range_ >> 4]);
MotionComponent::dump_config();
}
@@ -243,14 +243,6 @@ std::optional<std::string> SendspinHub::get_pairing_token() const {
return this->client_->pairing_token();
}
// THREAD CONTEXT: Main loop
StringRef SendspinHub::get_client_id() const {
if (this->client_ == nullptr) {
return StringRef();
}
return StringRef(this->client_->client_id());
}
// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
void SendspinHub::set_unpaired_access_enabled(bool enabled) {
this->unpaired_access_ = enabled;
@@ -198,12 +198,6 @@ class SendspinHub final : public Component,
/// started once; it stays available after the client stops. Main loop only.
std::optional<std::string> get_pairing_token() const;
/// @brief Returns the Sendspin client_id: base64url of the client's static public key.
///
/// Empty until the client has started once; it stays set after the client stops. The returned StringRef points into
/// the client's string, which start() may rewrite, so do not keep it past the current call. Main loop only.
StringRef get_client_id() const;
/// @brief Sets the device information reported to the server in the `client/hello` message.
///
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
+7 -153
View File
@@ -18,9 +18,8 @@ from esphome.components.esp32 import (
from esphome.components.network import (
add_use_address,
get_network_priority,
has_high_performance_wifi,
has_high_performance_networking,
ip_address_literal,
require_high_performance_wifi,
)
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
from esphome.config_helpers import filter_source_files_from_platform
@@ -31,7 +30,6 @@ from esphome.const import (
CONF_CERTIFICATE,
CONF_CERTIFICATE_AUTHORITY,
CONF_CHANNEL,
CONF_CHANNELS,
CONF_DNS1,
CONF_DNS2,
CONF_DOMAIN,
@@ -159,15 +157,9 @@ def has_native_wifi(
CONF_SAVE = "save"
CONF_BAND_MODE = "band_mode"
CONF_DEVICE_INFO = "device_info"
CONF_DPP = "dpp"
CONF_MIN_AUTH_MODE = "min_auth_mode"
CONF_ON_DPP_URI = "on_dpp_uri"
CONF_PHY_MODE = "phy_mode"
CONF_POST_CONNECT_ROAMING = "post_connect_roaming"
CONF_PRIVATE_KEY = "private_key"
CONF_AP_MODE = "ap_mode"
CONF_DNS_SERVERS = "dns_servers"
# Maximum number of WiFi networks that can be configured
# Limited to 127 because selected_sta_index_ is int8_t in C++
@@ -178,15 +170,10 @@ MAX_WIFI_NETWORKS = 127
# get best-effort connection attempts. Longer timeout ensures we exhaust all options
# before falling back to AP mode. Aligned with improv wifi_timeout default.
DEFAULT_AP_TIMEOUT = "90s"
DEFAULT_AP_TIMEOUT_PERIOD = cv.time_period(DEFAULT_AP_TIMEOUT)
DEFAULT_REBOOT_TIMEOUT = "15min"
# Both defaults also match the C++ initializers in wifi_component.h; codegen skips
# the setter when the config equals them.
# Similar to AP timeout, but reduced to allow repetitive enrollment attempts
# in the case of failure. Does not affect AP or scanning.
DEFAULT_DPP_TIMEOUT = "1min"
wifi_ns = cg.esphome_ns.namespace("wifi")
EAPAuth = wifi_ns.struct("EAPAuth")
ManualIP = wifi_ns.struct("ManualIP")
@@ -222,32 +209,6 @@ WIFI_8266_PHY_MODES = {
"11G": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11G,
"11N": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11N,
}
WIFI_AP_MODE_FALLBACK = "FALLBACK"
WIFI_AP_MODE_WISP = "WISP"
def _validate_ap_mode(value: str) -> str:
value = cv.one_of(WIFI_AP_MODE_FALLBACK, WIFI_AP_MODE_WISP, upper=True)(value)
if value == WIFI_AP_MODE_WISP:
# esp_netif_napt_enable() exists from ESP-IDF 5.1
cv.require_framework_version(
esp_idf=cv.Version(5, 1, 0), extra_message="WISP mode routes through NAT"
)(value)
return value
def _validate_dns_servers(value: list) -> list:
if any(ip.is_unspecified for ip in value):
raise cv.Invalid("0.0.0.0 is not a DNS server the access point can offer")
if len(value) > 1:
cv.require_framework_version(
esp_idf=cv.Version(6, 0, 0),
extra_message="The access point offers a second DNS server from ESP-IDF 6.0",
)(value)
return value
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition)
@@ -259,14 +220,6 @@ WiFiConfigureAction = wifi_ns.class_(
)
def _validate_dpp_device_info(value: object) -> str:
"""The supplicant puts the text on a space separated command line and reads it up to the next space."""
value = cv.string_strict(value)
if any(c.isspace() for c in value):
raise cv.Invalid("device_info cannot contain whitespace")
return value
def validate_password(value):
value = cv.string_strict(value)
if not value:
@@ -333,26 +286,6 @@ EAP_AUTH_SCHEMA = cv.All(
cv.has_at_least_one_key(CONF_IDENTITY, CONF_CERTIFICATE),
)
# Wi-Fi Easy Connect (DPP) enrollee; the ESP-IDF supplicant supports the QR code method
DPP_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PRIVATE_KEY): cv.sensitive(cv.string_strict),
cv.Optional(CONF_DEVICE_INFO): _validate_dpp_device_info,
cv.Optional(CONF_CHANNELS, default=[6]): cv.ensure_list(
cv.int_range(min=1, max=14)
),
cv.Optional(
CONF_TIMEOUT, default=DEFAULT_DPP_TIMEOUT
): cv.positive_time_period_milliseconds,
}
)
def _dpp_schema(value: ConfigType | None) -> ConfigType:
# A bare `dpp:` takes every default
return DPP_SCHEMA({} if value is None else value)
WIFI_NETWORK_BASE = cv.Schema(
{
cv.GenerateID(): cv.declare_id(WiFiAP),
@@ -371,12 +304,6 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend(
cv.Optional(
CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_AP_MODE, default=WIFI_AP_MODE_FALLBACK): _validate_ap_mode,
cv.Optional(CONF_DNS_SERVERS): cv.All(
cv.ensure_list(cv.ipv4address),
cv.Length(min=1, max=2),
_validate_dns_servers,
),
}
)
@@ -385,19 +312,6 @@ def wifi_network_ap(value):
if value is None:
value = {}
config = WIFI_NETWORK_AP(value)
if config[CONF_AP_MODE] == WIFI_AP_MODE_WISP:
if not config.get(CONF_PASSWORD):
raise cv.Invalid(
"WISP mode requires a password for the access point",
path=[CONF_PASSWORD],
)
if config[CONF_AP_TIMEOUT].total_milliseconds == 0:
raise cv.Invalid(
"ap_timeout is how long the bridge waits before a failed start is tried again; it cannot be 0",
path=[CONF_AP_TIMEOUT],
)
elif CONF_DNS_SERVERS in config:
raise cv.Invalid("dns_servers requires ap_mode: WISP", path=[CONF_DNS_SERVERS])
if CONF_MANUAL_IP in config and CORE.is_rp2:
raise cv.Invalid(
"Manual AP IP configuration is not supported on RP2040. "
@@ -447,42 +361,16 @@ def _apply_min_auth_mode_default(config):
def final_validate(config):
has_sta = bool(config.get(CONF_NETWORKS, True))
has_ap = CONF_AP in config
has_dpp = CONF_DPP in config
full_config = fv.full_config.get()
has_improv = "improv_ble" in full_config
has_improv_serial = "improv_serial" in full_config
has_captive_portal = "captive_portal" in full_config
has_web_server = "web_server" in full_config
if not (has_sta or has_ap or has_dpp or has_improv or has_improv_serial):
if not (has_sta or has_ap or has_improv or has_improv_serial):
raise cv.Invalid(
"Please specify at least an SSID or an Access Point to create, or configure DPP (Easy Connect)."
"Please specify at least an SSID or an Access Point to create."
)
if CONF_ON_DPP_URI in config and not has_dpp:
raise cv.Invalid(f"'{CONF_ON_DPP_URI}' requires '{CONF_DPP}'")
ap_is_wisp = has_ap and config[CONF_AP][CONF_AP_MODE] == WIFI_AP_MODE_WISP
if ap_is_wisp:
if not has_sta:
raise cv.Invalid(
"WISP mode requires at least one STA network (wifi.ssid or wifi.networks)"
)
if has_captive_portal:
raise cv.Invalid(
"captive_portal is not supported together with ap_mode: WISP"
)
# Every client packet passes through the WiFi driver, none through the device's own sockets
require_high_performance_wifi()
if CONF_DNS_SERVERS not in config[CONF_AP]:
# The access point passes the station's resolvers on, which a static address has to name
for network in [config, *config.get(CONF_NETWORKS, [])]:
static = network.get(CONF_MANUAL_IP)
if static and static[CONF_DNS1].is_unspecified:
raise cv.Invalid(
"manual_ip needs dns1 in WISP mode unless ap.dns_servers is set",
path=[CONF_MANUAL_IP, CONF_DNS1],
)
if has_ap and not has_captive_portal and not has_web_server and not ap_is_wisp:
if has_ap and not has_captive_portal and not has_web_server:
_LOGGER.warning(
"WiFi AP is configured but neither captive_portal nor web_server is enabled. "
"The AP will not be usable for configuration or monitoring. "
@@ -655,7 +543,6 @@ CONFIG_SCHEMA = cv.All(
): STA_MANUAL_IP_SCHEMA,
cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
cv.Optional(CONF_AP): wifi_network_ap,
cv.Optional(CONF_DPP): cv.All(cv.only_on_esp32, _dpp_schema),
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(
CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT
@@ -702,7 +589,6 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_DPP_URI): automation.validate_automation(single=True),
cv.Optional(CONF_USE_PSRAM): cv.All(
cv.only_on_esp32, cv.requires_component("psram"), cv.boolean
),
@@ -822,45 +708,19 @@ async def to_code(config):
lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))),
)
# Skip the setter when the config matches the C++ initializer.
if (ap_timeout := conf[CONF_AP_TIMEOUT]) != DEFAULT_AP_TIMEOUT_PERIOD:
if (ap_timeout := conf[CONF_AP_TIMEOUT]) != cv.time_period(DEFAULT_AP_TIMEOUT):
cg.add(var.set_ap_timeout(ap_timeout))
if conf[CONF_AP_MODE] == WIFI_AP_MODE_WISP:
cg.add_define("USE_WIFI_AP_WISP")
add_idf_sdkconfig_option("CONFIG_LWIP_IP_FORWARD", True)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV4_NAPT", True)
if dns_servers := conf.get(CONF_DNS_SERVERS):
cg.add_define("USE_WIFI_AP_WISP_STATIC_DNS")
# As lwIP stores an IPv4 address: the four bytes read as one little-endian word
for i, ip in enumerate(dns_servers):
cg.add_define(
f"WIFI_AP_WISP_DNS_{i}", int.from_bytes(ip.packed, "little")
)
cg.add_define("USE_WIFI_AP")
# The LN882H radio cannot run the AP and STA together; the fallback AP
# takes turns with the networks instead.
if CORE.is_ln882x:
cg.add_define("USE_WIFI_AP_EXCLUSIVE")
# The Pico W driver drops the AP station events; wifi_component_pico_w.cpp counts them
if CORE.is_rp2:
cg.add_build_flag("-Wl,--wrap=cyw43_cb_process_async_event")
# ESP32: register the WiFi stack with the esp32 sdkconfig reconciler, which
# drops SoftAP support / the LWIP DHCP server when AP mode is unused.
if CORE.is_esp32:
request_wifi(ap=CONF_AP in config)
if (dpp := config.get(CONF_DPP)) is not None:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DPP_SUPPORT", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_DPP", True)
if (key := dpp.get(CONF_PRIVATE_KEY)) is not None:
cg.add(var.set_dpp_key(key))
if (info := dpp.get(CONF_DEVICE_INFO)) is not None:
cg.add(var.set_dpp_device_info(info))
cg.add(var.set_dpp_channels(",".join(str(c) for c in dpp[CONF_CHANNELS])))
if (timeout := dpp[CONF_TIMEOUT]) != cv.time_period(DEFAULT_DPP_TIMEOUT):
cg.add(var.set_dpp_timeout(timeout))
cg.add_define("USE_WIFI_DPP")
# Disable Enterprise WiFi support if no EAP is configured
if CORE.is_esp32:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
@@ -963,8 +823,8 @@ async def to_code(config):
if config.get(CONF_USE_PSRAM):
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
# Apply high performance WiFi settings if a component asked for them
if CORE.is_esp32 and has_high_performance_wifi():
# Apply high performance WiFi settings if high performance networking is enabled
if CORE.is_esp32 and has_high_performance_networking():
# Check if PSRAM is guaranteed (set by psram component during final validation)
psram_guaranteed = psram_is_guaranteed()
@@ -1031,12 +891,6 @@ async def to_code(config):
var.get_disconnect_trigger(), [], on_disconnect_config
)
if (on_dpp_uri := config.get(CONF_ON_DPP_URI)) is not None:
cg.add_define("USE_WIFI_DPP_URI_TRIGGER")
await automation.build_callback_automation(
var, "add_on_dpp_uri_callback", [(cg.const_char_ptr, "uri")], on_dpp_uri
)
CORE.add_job(final_step)
+56 -167
View File
@@ -187,11 +187,11 @@ bool CompactString::operator==(const StringRef &other) const {
/// │ ↓ │
/// │ 5. FAILED → RESTARTING_ADAPTER │
/// │ - Normal: restart adapter, clear state │
/// │ - AP/DPP/improv active: skip restart, just disconnect │
/// │ - AP/improv active: skip restart, just disconnect │
/// │ ↓ │
/// │ 6. Loop back to start: │
/// │ - If first network is hidden → EXPLICIT_HIDDEN (retry cycle) │
/// │ - If AP/DPP/improv active → RETRY_HIDDEN (blind retry, see below)│
/// │ - If AP/improv active → RETRY_HIDDEN (blind retry, see below) │
/// │ - Otherwise → SCAN_CONNECTING (rescan) │
/// │ ↓ │
/// │ 7. RESCAN → Apply stored priorities, sort again │
@@ -655,17 +655,12 @@ void WiFiComponent::setup() {
this->configured_power_save_ = this->power_save_;
#endif
#if defined(USE_PROVISIONING) && (defined(USE_WIFI_DPP) || (defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)))
// The access point, the portals on it and the DPP listen are provisioning surfaces: once the
// window has closed, end them (mirrors the teardown on a successful connection). The blocks in
// loop() that start them are gated so none of them is started again afterwards. A bridge AP
// carries no portal and stays up.
#if defined(USE_PROVISIONING) && defined(USE_WIFI_AP)
// The access point and the portals on it are provisioning surfaces: once the window has
// closed, end them (mirrors the teardown on a successful connection). The fallback block in
// loop() is gated so none of them is started again afterwards.
if (provisioning::global_provisioning_manager != nullptr) {
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
#ifdef USE_WIFI_DPP
this->stop_dpp_listen_();
#endif
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)
this->end_ap_portal_(); // no-op when nothing is active, so not tied to ap_setup_
if (this->ap_setup_) {
ESP_LOGD(TAG, "Provisioning window closed; disabling AP");
@@ -677,9 +672,8 @@ void WiFiComponent::setup() {
return;
}
#endif
this->disable_ap_();
this->wifi_mode_({}, false);
}
#endif // USE_WIFI_AP
});
}
#endif
@@ -796,11 +790,6 @@ void WiFiComponent::start() {
this->start_ap_portal_();
#endif // USE_WIFI_AP
}
#ifdef USE_WIFI_DPP
// Without networks there is nothing to try first, so listen for a configurator right away
if (!this->has_sta() && this->is_dpp_active_() && this->wifi_mode_(true, {}))
this->start_dpp_listen_(millis());
#endif
#ifdef USE_IMPROV_BLE
if (!this->has_sta() && improv_ble::global_improv_component != nullptr) {
if (this->wifi_mode_(true, {}))
@@ -860,7 +849,7 @@ void WiFiComponent::loop() {
break;
}
// Use longer cooldown when a portal/improv is active to avoid disrupting a user on the AP
bool portal_active = this->is_provisioning_active_();
bool portal_active = this->is_ap_portal_active_() || this->is_improv_ble_active_();
uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
if (now - this->action_started_ > cooldown_duration) {
// After cooldown we either restarted the adapter because of
@@ -922,8 +911,8 @@ void WiFiComponent::loop() {
// The networks are not tried while the AP is up, so it pauses for
// them once nobody has used it for a while. Without networks the AP
// is all there is and stays.
if (this->has_sta() && now - this->ap_changed_ > WIFI_AP_EXCLUSIVE_DWELL_MS &&
(this->ap_clients_ == 0 || now - this->ap_changed_ > WIFI_AP_EXCLUSIVE_MAX_DWELL_MS))
if (this->has_sta() && now - this->ap_exclusive_changed_ > WIFI_AP_EXCLUSIVE_DWELL_MS &&
(this->ap_clients_ == 0 || now - this->ap_exclusive_changed_ > WIFI_AP_EXCLUSIVE_MAX_DWELL_MS))
this->pause_exclusive_ap_();
break;
#endif
@@ -933,35 +922,31 @@ void WiFiComponent::loop() {
return;
}
#ifdef USE_WIFI_AP_WISP
// The bridge comes up once it has a resolver to offer; a start that failed is tried again after ap_timeout
if (!this->ap_setup_ && this->ap_dns_known_() &&
(this->ap_changed_ == 0 || now - this->ap_changed_ > this->ap_timeout_)) {
ESP_LOGI(TAG, "Starting AP");
this->ap_changed_ = now;
this->setup_ap_config_();
}
#elif defined(USE_WIFI_AP)
#ifdef USE_WIFI_AP
bool provisioning_closed = false;
#ifdef USE_PROVISIONING
// Once the provisioning window has closed, don't bring up the fallback AP (or
// the captive portal on it) - the device must stay unprovisionable until it is
// power-cycled.
if (this->has_ap() && !this->ap_setup_ && !this->is_provisioning_closed_()) {
provisioning_closed =
provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed();
#endif
if (this->has_ap() && !this->ap_setup_ && !provisioning_closed) {
if (this->ap_timeout_ != 0 &&
(now - this->last_connected_ > this->ap_timeout_)
#ifdef USE_WIFI_AP_EXCLUSIVE
// After a pause, or a start that failed, the networks get a full
// ap_timeout before the AP is tried again.
&& now - this->ap_changed_ > this->ap_timeout_
&& now - this->ap_exclusive_changed_ > this->ap_timeout_
#endif
) {
ESP_LOGI(TAG, "Starting fallback AP");
#ifdef USE_WIFI_AP_EXCLUSIVE
this->ap_changed_ = now;
this->ap_exclusive_changed_ = now;
#endif
this->setup_ap_config_();
// A portal is only started behind an AP that came up: the portal keeps a reference to the web server
// on every start, and a failed start is tried again later
if (this->ap_setup_) {
// Where the AP runs on its own, a portal with no AP behind it would only stretch the cooldowns.
if (!WIFI_AP_EXCLUSIVE || this->ap_setup_) {
#ifdef USE_CAPTIVE_PORTAL
// Reset so we force one full scan after captive portal starts
// (previous scans were filtered because captive portal wasn't active yet)
@@ -981,43 +966,29 @@ void WiFiComponent::loop() {
improv_ble::global_improv_component->start();
}
}
#endif
if (this->reboot_timeout_ != 0 && now - this->last_connected_ > this->reboot_timeout_) {
bool suppress = false;
#if defined(USE_WIFI_AP) && (defined(USE_RTL87XX) || defined(USE_LN882X))
// The Realtek driver reports no stations and the Lightning one is unverified, so a running fallback AP
// holds the reboot there as it always did
suppress = this->ap_setup_;
#elif defined(USE_WIFI_AP)
// A station on the fallback AP is using the device
suppress = this->ap_setup_ && this->ap_clients_ != 0;
#endif
if (!this->has_ap() && this->reboot_timeout_ != 0) {
if (now - this->last_connected_ > this->reboot_timeout_) {
bool suppress = false;
#ifdef USE_PROVISIONING
// Don't reboot while a provisioning window is pending (device unprovisioned).
// The device is legitimately waiting to be onboarded (Wi-Fi must come up
// before the controller can set credentials), and an auto-reboot would reopen
// the window without the deliberate power cycle / reset that is meant to be
// required. Resumes normal reboot behavior once provisioned.
suppress |= provisioning::global_provisioning_manager != nullptr &&
provisioning::global_provisioning_manager->window_pending();
// Don't reboot while a provisioning window is pending (device unprovisioned).
// The device is legitimately waiting to be onboarded (Wi-Fi must come up
// before the controller can set credentials), and an auto-reboot would reopen
// the window without the deliberate power cycle / reset that is meant to be
// required. Resumes normal reboot behavior once provisioned.
suppress = provisioning::global_provisioning_manager != nullptr &&
provisioning::global_provisioning_manager->window_pending();
#endif
if (!suppress) {
ESP_LOGE(TAG, "Can't connect; rebooting");
App.reboot();
if (!suppress) {
ESP_LOGE(TAG, "Can't connect; rebooting");
App.reboot();
}
}
}
}
#ifdef USE_WIFI_DPP
// Without networks no cooldown starts the listen, so a failed or abandoned enrollment is listened for again
// here, at most once per timeout
if (!this->has_sta() && !this->dpp_listening_ && this->state_ != WIFI_COMPONENT_STATE_DISABLED &&
now - this->dpp_listen_started_ > this->dpp_timeout_ && this->is_dpp_active_() && this->wifi_mode_(true, {})) {
this->start_dpp_listen_(now);
}
#endif
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Check if power save mode needs to be updated based on high-performance requests
if (this->high_performance_semaphore_ != nullptr) {
@@ -1091,12 +1062,10 @@ void WiFiComponent::setup_ap_config_() {
this->ap_.set_ssid(ssid_buf);
}
}
// Reset before the AP exists: afterwards only the platform's event path writes the counter
this->ap_clients_ = 0;
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
#if defined(USE_WIFI_AP_EXCLUSIVE) || defined(USE_WIFI_AP_WISP)
// A failed start must not leave the radio in AP mode: on an exclusive radio it would refuse
// the STA attempts until the next try, and a bridge would take clients it cannot serve.
#ifdef USE_WIFI_AP_EXCLUSIVE
// A failed start must not leave the radio in AP mode, which would refuse
// the STA attempts until the next try.
if (!this->ap_setup_)
this->wifi_mode_({}, false);
#endif
@@ -1136,7 +1105,10 @@ void WiFiComponent::pause_exclusive_ap_() {
ESP_LOGI(TAG, "Pausing AP to try the networks");
// Scanning and connecting turn STA back on.
this->disable_ap_();
this->ap_changed_ = App.get_loop_component_start_time();
this->ap_setup_ = false;
// Clients drop with the AP without a disconnect event for each.
this->ap_clients_ = 0;
this->ap_exclusive_changed_ = App.get_loop_component_start_time();
// A fresh connection cycle, as at boot, rather than resuming the one the
// AP cut short.
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
@@ -1145,22 +1117,10 @@ void WiFiComponent::pause_exclusive_ap_() {
}
#endif
#ifdef USE_WIFI_AP_WISP
void WiFiComponent::fail_bridge_() {
this->ap_setup_ = false;
this->ap_clients_ = 0; // dropped with the AP, without a disconnect event for each
this->wifi_mode_({}, false);
}
#endif
void WiFiComponent::disable_ap_() {
this->end_ap_portal_();
ESP_LOGD(TAG, "Disabling AP");
this->wifi_mode_({}, false);
// The fallback block may bring the AP back after a later disconnect; its clients dropped with it without
// a disconnect event for each
this->ap_setup_ = false;
this->ap_clients_ = 0;
}
void WiFiComponent::set_ap(const WiFiAP &ap) {
@@ -1370,23 +1330,13 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
// Enter cooldown to allow WiFi hardware to stabilize
// (immediate failure suggests hardware not ready, different from connection timeout)
this->start_cooldown_();
return;
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
} else {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
}
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
this->action_started_ = millis();
}
void WiFiComponent::start_cooldown_() {
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
#ifdef USE_WIFI_DPP
// The cooldown is the configurator's window; the attempt that ends it stops the listen again
if (!this->dpp_listening_ && this->is_dpp_active_() && this->wifi_mode_(true, {}))
this->start_dpp_listen_(this->action_started_);
#endif
}
const LogString *get_signal_bars(int8_t rssi) {
// LOWER ONE QUARTER BLOCK
// Unicode: U+2582, UTF-8: E2 96 82
@@ -1499,21 +1449,9 @@ void WiFiComponent::disable() {
this->state_ = WIFI_COMPONENT_STATE_DISABLED;
#ifdef USE_WIFI_LEASE_CACHE
this->cancel_timeout(LEASE_RENEW_TIMEOUT_ID);
#endif
#ifdef USE_WIFI_DPP
this->stop_dpp_listen_();
#endif
this->wifi_disconnect_();
this->wifi_mode_(false, false);
#ifdef USE_WIFI_AP
// The AP went down with the radio; enable() has to configure it, and its portal, again
this->end_ap_portal_();
this->ap_setup_ = false;
this->ap_clients_ = 0;
#endif
#ifdef USE_WIFI_AP_WISP
this->ap_changed_ = 0; // at once, not after ap_timeout
#endif
}
void WiFiComponent::start_scanning() {
@@ -1758,12 +1696,6 @@ void WiFiComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Disabled");
return;
}
#ifdef USE_WIFI_DPP
ESP_LOGCONFIG(TAG,
" DPP channels: %s\n"
" DPP timeout: %" PRIu32 " ms",
this->dpp_channels_, this->dpp_timeout_);
#endif
#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
const char *band_mode_s;
switch (this->band_mode_) {
@@ -1810,22 +1742,17 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
// Reset to initial phase on successful connection (don't log transition, just reset state)
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
this->num_retried_ = 0;
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP) // the bridge AP stays up for its clients
#ifdef USE_WIFI_AP
if (this->has_ap()) {
this->disable_ap_();
}
#endif
#ifdef USE_WIFI_DPP
this->stop_dpp_listen_();
#endif
#ifdef USE_IMPROV_BLE
if (this->is_improv_ble_active_()) {
improv_ble::global_improv_component->stop();
}
#endif
// The fallback and reboot checks later in this pass must see the connection, not the old timestamp
this->last_connected_ = now;
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
// Refresh is_connected() cache; loop()'s refresh ran before this transition.
this->update_connected_state_();
@@ -1995,7 +1922,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
}
// No hidden networks - always go through RESTARTING_ADAPTER phase
// This ensures num_retried_ gets reset and a fresh scan is triggered
// The actual adapter restart will be skipped if DPP/captive portal/improv is active
// The actual adapter restart will be skipped if captive portal/improv is active
return WiFiRetryPhase::RESTARTING_ADAPTER;
case WiFiRetryPhase::RETRY_HIDDEN:
@@ -2018,7 +1945,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
}
// Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
// This ensures num_retried_ gets reset and a fresh scan is triggered
// The actual adapter restart will be skipped if DPP/captive portal/improv is active
// The actual adapter restart will be skipped if captive portal/improv is active
return WiFiRetryPhase::RESTARTING_ADAPTER;
case WiFiRetryPhase::RESTARTING_ADAPTER:
@@ -2048,14 +1975,14 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
// attempting to connect to all configured networks in sequence.
// Captive portal needs scan results to show available networks.
// If captive portal is active, only skip scanning if we've done a scan after it started.
// If only DPP or improv is active (no captive portal), skip scanning since neither needs results.
// If only improv is active (no captive portal), skip scanning since improv doesn't need results.
if (this->is_captive_portal_active_()) {
if (this->has_completed_scan_after_captive_portal_start_) {
return WiFiRetryPhase::RETRY_HIDDEN;
}
// Need to scan for captive portal
} else if (this->is_improv_ble_active_() || this->is_dpp_active_()) {
// Improv and DPP don't need scan results
} else if (this->is_improv_ble_active_()) {
// Improv doesn't need scan results
return WiFiRetryPhase::RETRY_HIDDEN;
}
return WiFiRetryPhase::SCAN_CONNECTING;
@@ -2145,7 +2072,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
// Skip actual adapter restart if a portal/improv is active
// This allows state machine to reset num_retried_ and trigger fresh scan
// without disrupting the portal/improv connection
if (!this->is_provisioning_active_()) {
if (!this->is_ap_portal_active_() && !this->is_improv_ble_active_()) {
this->restart_adapter();
} else {
// Even when skipping full restart, disconnect to clear driver state
@@ -2153,14 +2080,15 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
this->wifi_disconnect_();
}
// Clear scan flag - we're starting a new retry cycle
// This is critical for DPP/captive portal/improv flow: when determine_next_phase_()
// This is critical for captive portal/improv flow: when determine_next_phase_()
// returns RETRY_HIDDEN (because scanning is skipped), find_next_hidden_sta_()
// will see BLIND_RETRY mode and treat ALL networks as candidates,
// effectively cycling through all configured networks without scan results.
this->retry_hidden_mode_ = RetryHiddenMode::BLIND_RETRY;
// Always enter cooldown after restart (or skip-restart) to allow stabilization
// Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
this->start_cooldown_();
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
return true;
@@ -2396,32 +2324,6 @@ void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) {
#endif
}
#ifdef USE_WIFI_DPP
void WiFiComponent::start_dpp_listen_(uint32_t now) {
this->dpp_listen_started_ = now;
this->dpp_listening_ = this->wifi_dpp_start_listen_();
}
void WiFiComponent::stop_dpp_listen_() {
if (!this->dpp_listening_)
return;
this->wifi_dpp_stop_listen_();
this->dpp_listening_ = false;
}
#endif
bool WiFiComponent::is_dpp_active_() {
#ifdef USE_WIFI_DPP
// A configurator is waited for once the networks have had their timeout, or from boot without any; the retry
// machine then treats DPP like a portal (blind retries, a long cooldown) whether the supplicant is listening at
// this moment or not
return !this->is_provisioning_closed_() &&
(!this->has_sta() || App.get_loop_component_start_time() - this->last_connected_ > this->dpp_timeout_);
#else
return false;
#endif
}
bool WiFiComponent::is_captive_portal_active_() {
#ifdef USE_CAPTIVE_PORTAL
return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active();
@@ -2467,19 +2369,6 @@ bool WiFiComponent::is_improv_ble_active_() {
#endif
}
bool WiFiComponent::is_provisioning_active_() {
return this->is_ap_portal_active_() || this->is_improv_ble_active_() || this->is_dpp_active_();
}
bool WiFiComponent::is_provisioning_closed_() {
#ifdef USE_PROVISIONING
// The device stays unprovisionable until it is power-cycled
return provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed();
#else
return false;
#endif
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
bool WiFiComponent::request_high_performance() {
// Already configured for high performance - request satisfied
+4 -80
View File
@@ -19,7 +19,6 @@
#include <atomic>
#include <limits>
#include <span>
#include <memory>
#include <string>
#include <type_traits>
#include <utility>
@@ -51,13 +50,6 @@ extern "C" {
}
#include <WiFi.h>
#ifdef USE_WIFI_AP
// The linker routes the driver's firmware events here; wifi/__init__.py adds the wrap flag
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" void __wrap_cyw43_cb_process_async_event(void *cb_data, const cyw43_async_event_t *ev);
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif
#endif
#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
@@ -487,21 +479,6 @@ class WiFiComponent final : public Component {
void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; }
#endif // USE_WIFI_AP
#ifdef USE_WIFI_DPP
// Code generation passes string literals that live in flash
void set_dpp_key(const char *key) { this->dpp_key_ = key; }
void set_dpp_device_info(const char *info) { this->dpp_device_info_ = info; }
void set_dpp_channels(const char *channels) { this->dpp_channels_ = channels; }
void set_dpp_timeout(uint32_t timeout) { this->dpp_timeout_ = timeout; }
#ifdef USE_WIFI_DPP_URI_TRIGGER
template<typename F> void add_on_dpp_uri_callback(F &&callback) {
this->dpp_uri_callback_.add(std::forward<F>(callback));
}
#endif
/// The Easy Connect URI to show as a QR code, nullptr until the supplicant has generated it
const char *get_dpp_uri() const { return this->dpp_uri_.get(); }
#endif
void enable();
void disable();
bool is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; }
@@ -744,19 +721,6 @@ class WiFiComponent final : public Component {
#endif
#endif // USE_WIFI_AP
#ifdef USE_WIFI_AP_WISP
/// Whether there is an IPv4 resolver to offer AP clients: a configured one, or the station's
bool ap_dns_known_();
/// Hand the AP DHCP server its resolvers; false when one could not be set
bool write_ap_dns_();
/// Take the bridge down so loop() runs its setup again after ap_timeout
void fail_bridge_();
#ifndef USE_WIFI_AP_WISP_STATIC_DNS
/// Pass the station's resolvers on to AP clients when they changed
void apply_ap_dns_();
#endif
#endif
void print_connect_params_();
WiFiAP build_params_for_current_phase_();
@@ -867,23 +831,6 @@ class WiFiComponent final : public Component {
network::IPAddress wifi_gateway_ip_();
network::IPAddress wifi_dns_ip_(int num);
#ifdef USE_WIFI_DPP
// Starts listening for a DPP configurator; the platform half reports whether the driver accepted
void start_dpp_listen_(uint32_t now);
void stop_dpp_listen_();
#endif
/// Wait before the next attempt; while a configurator is waited for, the wait is its listen window
void start_cooldown_();
#ifdef USE_WIFI_DPP
void wifi_dpp_init_();
bool wifi_dpp_start_listen_();
void wifi_dpp_stop_listen_();
#endif
bool is_dpp_active_();
/// A portal, improv or DPP is waiting for credentials, so the retry machine must not disturb the radio
bool is_provisioning_active_();
/// The provisioning window has closed, so no new provisioning surface may start
bool is_provisioning_closed_();
bool is_captive_portal_active_();
/// captive_portal or the web_server AP mode is serving a user on the access point
bool is_ap_portal_active_();
@@ -959,11 +906,6 @@ class WiFiComponent final : public Component {
#ifdef USE_RP2
static int s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result);
void wifi_scan_result_(void *env, const cyw43_ev_scan_result_t *result);
#ifdef USE_WIFI_AP
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
friend void ::__wrap_cyw43_cb_process_async_event(void *cb_data, const cyw43_async_event_t *ev);
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif
#endif
#ifdef USE_LIBRETINY
@@ -1029,8 +971,8 @@ class WiFiComponent final : public Component {
float output_power_{NAN};
uint32_t action_started_;
uint32_t last_connected_{0};
#if defined(USE_WIFI_AP_EXCLUSIVE) || defined(USE_WIFI_AP_WISP)
uint32_t ap_changed_{0}; // When the AP was last started or paused
#ifdef USE_WIFI_AP_EXCLUSIVE
uint32_t ap_exclusive_changed_{0}; // When the AP was last started or paused
#endif
uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py
uint32_t roaming_last_check_{0};
@@ -1040,17 +982,6 @@ class WiFiComponent final : public Component {
// WiFiAP is byte aligned unless manual IP or EAP is enabled; placed before the
// 1-byte members so they pack into its trailing bytes instead of padding after it
WiFiAP ap_;
#endif // USE_WIFI_AP
#ifdef USE_WIFI_DPP
const char *dpp_key_{nullptr};
const char *dpp_device_info_{nullptr};
const char *dpp_channels_{nullptr};
std::unique_ptr<char[]> dpp_uri_; // written by the event task before the URI event is queued
#ifdef USE_WIFI_DPP_URI_TRIGGER
CallbackManager<void(const char *)> dpp_uri_callback_;
#endif
uint32_t dpp_timeout_{60000}; // Keep in sync with DEFAULT_DPP_TIMEOUT in __init__.py
uint32_t dpp_listen_started_{0};
#endif
// 1-byte enums and integers
@@ -1135,15 +1066,8 @@ class WiFiComponent final : public Component {
bool scan_done_{false};
bool ap_setup_{false};
bool ap_started_{false};
#ifdef USE_WIFI_DPP
bool dpp_listening_{false};
#endif
#ifdef USE_WIFI_AP
// Stations joined to the AP; they hold the reboot timeout and keep an exclusive AP from pausing.
// Counted on the main loop on ESP32 and LibreTiny (queued events), in the SDK callback on ESP8266 and
// in the driver's interrupt context on the Pico W. The main loop only reads the byte while the AP is up
// and only writes it (a reset) while the AP is down, so no lock is needed.
uint8_t ap_clients_{0};
#ifdef USE_WIFI_AP_EXCLUSIVE
uint8_t ap_clients_{0}; // Devices joined to the AP, which keep it from pausing
#endif
bool passive_scan_{false};
bool has_saved_wifi_settings_{false};
@@ -572,9 +572,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
break;
}
case EVENT_SOFTAPMODE_STACONNECTED: {
#ifdef USE_WIFI_AP
global_wifi_component->ap_clients_++;
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
auto it = event->event_info.sta_connected;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
@@ -584,10 +581,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
break;
}
case EVENT_SOFTAPMODE_STADISCONNECTED: {
#ifdef USE_WIFI_AP
if (global_wifi_component->ap_clients_ > 0)
global_wifi_component->ap_clients_--;
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
auto it = event->event_info.sta_disconnected;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
@@ -24,10 +24,6 @@
#endif
#endif
#ifdef USE_WIFI_DPP
#include <esp_dpp.h>
#endif
#ifdef USE_WIFI_AP
#include "dhcpserver/dhcpserver.h"
#endif // USE_WIFI_AP
@@ -68,15 +64,6 @@ static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_WIFI_DPP
/// Terminated copies of the received station credentials; the supplicant's wifi_config_t would grow every event
static void copy_terminated(char *dst, const uint8_t *src, size_t len) {
memcpy(dst, src, len);
dst[len] = '\0';
}
#endif
struct IDFWiFiEvent {
esp_event_base_t event_base;
int32_t event_id;
@@ -89,10 +76,6 @@ struct IDFWiFiEvent {
wifi_event_ap_stadisconnected_t ap_stadisconnected;
wifi_event_ap_probe_req_rx_t ap_probe_req_rx;
wifi_event_bss_rssi_low_t bss_rssi_low;
#ifdef USE_WIFI_DPP
SavedWifiSettings *dpp_credentials; // owned by the event; the rare 98 byte record does not widen the union
wifi_event_dpp_failed_t dpp_failed;
#endif
ip_event_got_ip_t ip_got_ip;
#if USE_NETWORK_IPV6
ip_event_got_ip6_t ip_got_ip6;
@@ -142,23 +125,6 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
memcpy(&event.data.ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t));
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
memcpy(&event.data.ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t));
#ifdef USE_WIFI_DPP
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_URI_READY) {
// Kept on the component for the whole boot; the queued event only announces it
const auto *uri = static_cast<const wifi_event_dpp_uri_ready_t *>(event_data);
const size_t len = strnlen(uri->uri, uri->uri_data_len);
auto copy = std::make_unique<char[]>(len + 1);
copy_terminated(copy.get(), reinterpret_cast<const uint8_t *>(uri->uri), len);
global_wifi_component->dpp_uri_ = std::move(copy);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_CFG_RECVD) {
const auto &sta = static_cast<const wifi_event_dpp_config_received_t *>(event_data)->wifi_cfg.sta;
auto *credentials = new SavedWifiSettings; // NOLINT(cppcoreguidelines-owning-memory)
copy_terminated(credentials->ssid, sta.ssid, sizeof(sta.ssid));
copy_terminated(credentials->password, sta.password, sizeof(sta.password));
event.data.dpp_credentials = credentials;
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_FAILED) {
memcpy(&event.data.dpp_failed, event_data, sizeof(wifi_event_dpp_failed_t));
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
memcpy(&event.data.ip_assigned_ip_to_client, event_data, sizeof(ip_event_assigned_ip_to_client_t));
@@ -175,10 +141,6 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
auto *to_send = new IDFWiFiEvent; // NOLINT(cppcoreguidelines-owning-memory)
memcpy(to_send, &event, sizeof(IDFWiFiEvent));
if (!global_wifi_component->event_queue_.push(to_send)) {
#ifdef USE_WIFI_DPP
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_CFG_RECVD)
delete to_send->data.dpp_credentials; // NOLINT(cppcoreguidelines-owning-memory)
#endif
delete to_send; // NOLINT(cppcoreguidelines-owning-memory)
}
}
@@ -262,9 +224,6 @@ void WiFiComponent::wifi_lazy_init_() {
}
s_wifi_started = true;
this->wifi_initialized_ = true;
#ifdef USE_WIFI_DPP
this->wifi_dpp_init_();
#endif
return;
}
@@ -283,32 +242,9 @@ void WiFiComponent::wifi_lazy_init_() {
ESP_LOGE(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err));
return;
}
this->wifi_initialized_ = true;
#ifdef USE_WIFI_DPP
this->wifi_dpp_init_();
#endif
}
#ifdef USE_WIFI_DPP
void WiFiComponent::wifi_dpp_init_() {
// With no callback the supplicant posts WIFI_EVENT_DPP_* events, handled with the other events
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
esp_err_t err = esp_supp_dpp_init();
#else
esp_err_t err = esp_supp_dpp_init(nullptr);
#endif
if (err != ESP_OK) {
ESP_LOGW(TAG, "%s failed: %s", LOG_STR_LITERAL("esp_supp_dpp_init"), esp_err_to_name(err));
return;
}
err = esp_supp_dpp_bootstrap_gen(this->dpp_channels_, DPP_BOOTSTRAP_QR_CODE, this->dpp_key_, this->dpp_device_info_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "%s failed: %s", LOG_STR_LITERAL("esp_supp_dpp_bootstrap_gen"), esp_err_to_name(err));
}
}
#endif
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
esp_err_t err;
wifi_mode_t current_mode = WIFI_MODE_NULL;
@@ -418,10 +354,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
#ifdef USE_WIFI_DPP
// A connect attempt ends the supplicant's listen; the next cooldown starts it again
this->stop_dpp_listen_();
#endif
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
wifi_config_t conf;
@@ -996,15 +928,6 @@ const char *get_disconnect_reason_str(uint8_t reason) {
}
}
#ifdef USE_WIFI_AP
// The driver's own station list cannot drift, so the count is read from it rather than kept by arithmetic
static void read_ap_clients(uint8_t &count) {
wifi_sta_list_t sta_list;
if (esp_wifi_ap_get_sta_list(&sta_list) == ESP_OK)
count = sta_list.num;
}
#endif
bool WiFiComponent::wifi_loop_() {
// Use pop() directly instead of empty() — pop() costs 1 memw (acquire on tail_),
// while empty() costs 2 memw (acquire on both head_ and tail_) on Xtensa.
@@ -1022,11 +945,6 @@ bool WiFiComponent::wifi_loop_() {
uint16_t dropped = this->event_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped);
#ifdef USE_WIFI_AP
// A dropped join or leave is made up for by reading the list again
if (this->ap_setup_)
read_ap_clients(this->ap_clients_);
#endif
}
return true;
}
@@ -1136,9 +1054,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
const auto &it = data->data.ip_got_ip;
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
this->got_ipv4_address_ = true;
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
this->apply_ap_dns_();
#endif
#ifdef USE_WIFI_LEASE_CACHE
this->save_lease_settings_(); // events are drained on the main loop
#endif
@@ -1257,14 +1172,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) {
ESP_LOGV(TAG, "AP start");
this->ap_started_ = true;
#ifdef USE_WIFI_AP_WISP
// NAPT needs the interface up, so it cannot be part of the setup. Without it the clients have no way out, so
// the AP goes down and its setup runs again, which brings it back up through this event.
if (esp_err_t err = esp_netif_napt_enable(s_ap_netif); err != ESP_OK) {
ESP_LOGE(TAG, "Failed to enable NAT: %s", esp_err_to_name(err));
this->fail_bridge_();
}
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) {
ESP_LOGV(TAG, "AP stop");
@@ -1279,9 +1186,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) {
#ifdef USE_WIFI_AP
read_ap_clients(this->ap_clients_);
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
const auto &it = data->data.ap_staconnected;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
@@ -1290,9 +1194,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) {
#ifdef USE_WIFI_AP
read_ap_clients(this->ap_clients_);
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
const auto &it = data->data.ap_stadisconnected;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
@@ -1300,31 +1201,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf);
#endif
#ifdef USE_WIFI_DPP
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_DPP_URI_READY) {
ESP_LOGD(TAG, "DPP URI: %s", this->dpp_uri_.get());
#ifdef USE_WIFI_DPP_URI_TRIGGER
this->dpp_uri_callback_.call(this->dpp_uri_.get());
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_DPP_CFG_RECVD) {
const std::unique_ptr<SavedWifiSettings> credentials(data->data.dpp_credentials);
this->dpp_listening_ = false;
ESP_LOGI(TAG, "DPP configured network " LOG_SECRET("'%s'"), credentials->ssid);
// The reboot timer starts over for the connect attempt, as after a boot with saved credentials
this->last_connected_ = App.get_loop_component_start_time();
// Saved like portal credentials; from a cooldown this already restarts the retries
this->save_wifi_sta(credentials->ssid, credentials->password);
if (this->state_ == WIFI_COMPONENT_STATE_OFF || this->state_ == WIFI_COMPONENT_STATE_AP) {
// Without networks the station machine was never started; an AP without a portal sits in AP
this->start_connecting(*this->get_selected_sta_());
}
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_DPP_FAILED) {
this->dpp_listening_ = false;
ESP_LOGW(TAG, "DPP failed: %s", esp_err_to_name(data->data.dpp_failed.failure_reason));
#endif // USE_WIFI_DPP
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
const auto &it = data->data.ip_assigned_ip_to_client;
@@ -1473,19 +1349,6 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
}
#endif
#ifdef USE_WIFI_AP_WISP
// Offer resolvers through DHCP: the configured ones, otherwise the station's. Without them the server would
// offer the AP's own address, so a failure fails the setup and it is tried again.
uint8_t dns_offer = 1;
err =
esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_DOMAIN_NAME_SERVER, &dns_offer, sizeof(dns_offer));
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to enable DHCP DNS option: %s", esp_err_to_name(err));
}
if (err != ESP_OK || !this->write_ap_dns_())
return false;
#endif
err = esp_netif_dhcps_start(s_ap_netif);
if (err != ESP_OK) {
@@ -1556,20 +1419,6 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
}
#endif // USE_WIFI_AP
#ifdef USE_WIFI_DPP
bool WiFiComponent::wifi_dpp_start_listen_() {
esp_err_t err = esp_supp_dpp_start_listen();
if (err != ESP_OK) {
ESP_LOGW(TAG, "%s failed: %s", LOG_STR_LITERAL("esp_supp_dpp_start_listen"), esp_err_to_name(err));
return false;
}
ESP_LOGD(TAG, "DPP listening for a configurator");
return true;
}
void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
#endif
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
@@ -1640,81 +1489,6 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
return network::IPAddress(dns_ip);
}
#ifdef USE_WIFI_AP_WISP
// A DHCP server interface takes a backup resolver from ESP-IDF 6.0
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
static constexpr size_t AP_DNS_COUNT = 2;
#else
static constexpr size_t AP_DNS_COUNT = 1;
#endif
#ifdef USE_WIFI_AP_WISP_STATIC_DNS
// The configured resolvers as lwIP stores them, from codegen
static constexpr uint32_t AP_DNS[] = {
WIFI_AP_WISP_DNS_0,
#ifdef WIFI_AP_WISP_DNS_1
WIFI_AP_WISP_DNS_1,
#endif
};
#endif
static uint32_t ap_dns_to_offer(size_t i) {
#ifdef USE_WIFI_AP_WISP_STATIC_DNS
return i < std::size(AP_DNS) ? AP_DNS[i] : 0;
#else
return ip_addr_get_ip4_u32(dns_getserver(i));
#endif
}
bool WiFiComponent::ap_dns_known_() { return ap_dns_to_offer(0) != 0; }
bool WiFiComponent::write_ap_dns_() {
bool ok = true;
for (size_t i = 0; i < AP_DNS_COUNT; i++) {
const uint32_t addr = ap_dns_to_offer(i);
if (addr == 0)
continue;
esp_netif_dns_info_t info{};
info.ip.type = ESP_IPADDR_TYPE_V4;
info.ip.u_addr.ip4.addr = addr;
esp_err_t err = esp_netif_set_dns_info(s_ap_netif, static_cast<esp_netif_dns_type_t>(i), &info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to set DHCP DNS server %u: %s", (unsigned) i, esp_err_to_name(err));
ok = false;
} else {
ESP_LOGV(TAG, "DHCP DNS server: " IPSTR, IP2STR(&info.ip.u_addr.ip4));
}
}
return ok;
}
#ifndef USE_WIFI_AP_WISP_STATIC_DNS
void WiFiComponent::apply_ap_dns_() {
if (!this->ap_setup_)
return; // loop() brings the bridge up once the station has a resolver, and its setup writes them
// The DHCP server holds what it offers, so there is nothing to cache; a failed update is noticed again with the
// next lease. A resolver the station lost stays offered: set_dns_info rejects an empty address.
bool changed = false;
for (size_t i = 0; i < AP_DNS_COUNT && !changed; i++) {
const uint32_t addr = ap_dns_to_offer(i);
esp_netif_dns_info_t current{};
changed =
addr != 0 && (esp_netif_get_dns_info(s_ap_netif, static_cast<esp_netif_dns_type_t>(i), &current) != ESP_OK ||
current.ip.u_addr.ip4.addr != addr);
}
if (!changed)
return;
// The IP setup programs the DHCP server from scratch without touching the soft AP, so clients stay joined
#ifdef USE_WIFI_MANUAL_IP
const bool ok = this->wifi_ap_ip_config_(this->ap_.get_manual_ip());
#else
const bool ok = this->wifi_ap_ip_config_({});
#endif
if (!ok)
this->fail_bridge_();
}
#endif // USE_WIFI_AP_WISP_STATIC_DNS
#endif // USE_WIFI_AP_WISP
} // namespace esphome::wifi
#endif // USE_ESP32
#endif
@@ -629,7 +629,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
#ifdef USE_WIFI_AP
#ifdef USE_WIFI_AP_EXCLUSIVE
this->ap_clients_++;
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
@@ -641,7 +641,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
#ifdef USE_WIFI_AP
#ifdef USE_WIFI_AP_EXCLUSIVE
if (this->ap_clients_ > 0)
this->ap_clients_--;
#endif
@@ -881,7 +881,6 @@ bool WiFiComponent::wifi_loop_() {
// so if pop() returned nullptr above we can skip this check.
uint16_t dropped = this->event_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
// A dropped station event cannot be made up for: the BK72xx core has no station query
ESP_LOGW(TAG, "Dropped %" PRIu16 " WiFi events due to buffer overflow", dropped);
}
return true;
@@ -59,11 +59,6 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
if (ap.value()) {
cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, CYW43_COUNTRY_WORLDWIDE);
} else {
#ifdef USE_WIFI_AP
// Takes down the Arduino AP interface and its DHCP server; a later beginAP() is refused
// while they are still up
WiFi.disconnectAP();
#endif
cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, false, CYW43_COUNTRY_WORLDWIDE);
}
this->ap_started_ = ap.value();
@@ -243,16 +238,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// Pass nullptr for empty password — CYW43 uses the password pointer (not length)
// to choose between OPEN and WPA2 auth mode.
const char *ap_password = ap.password_.empty() ? nullptr : ap.password_.c_str();
if (WiFi.beginAP(ap.ssid_.c_str(), ap_password, ap.has_channel() ? ap.get_channel() : 1) != WL_CONNECTED) {
ESP_LOGW(TAG, "beginAP failed");
return false;
}
WiFi.beginAP(ap.ssid_.c_str(), ap_password, ap.has_channel() ? ap.get_channel() : 1);
return true;
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.softAPIP()}; }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() {
@@ -381,36 +372,5 @@ bool WiFiComponent::wifi_loop_() {
void WiFiComponent::wifi_pre_setup_() {}
} // namespace esphome::wifi
#ifdef USE_WIFI_AP
// Firmware event ids the driver header leaves out
static constexpr uint32_t AP_STATION_ASSOC_IND = 8;
// The driver hands every firmware event to cyw43_cb_process_async_event and ignores the AP station
// ones, so they are counted here before it sees them. Runs in the driver's context. Verified with
// arduino-pico 6.1.1 on a Pico 2W: a join gives AUTH_IND then ASSOC_IND, a leave gives DISASSOC_IND.
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
// Names are mandated by the --wrap linker mechanism.
extern "C" void __real_cyw43_cb_process_async_event(void *cb_data, const cyw43_async_event_t *ev);
extern "C" void __wrap_cyw43_cb_process_async_event(void *cb_data, const cyw43_async_event_t *ev) {
if (ev->interface == CYW43_ITF_AP) {
switch (ev->event_type) {
case AP_STATION_ASSOC_IND:
// A refused association carries its status here and has no leave event to balance it. A
// reassociation comes from a station that is already counted, whether or not its leave was seen.
if (ev->status == CYW43_STATUS_SUCCESS)
esphome::wifi::global_wifi_component->ap_clients_++;
break;
case CYW43_EV_DISASSOC_IND:
case CYW43_EV_DEAUTH_IND:
if (esphome::wifi::global_wifi_component->ap_clients_ > 0)
esphome::wifi::global_wifi_component->ap_clients_--;
break;
}
}
__real_cyw43_cb_process_async_event(cb_data, ev);
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_WIFI_AP
#endif
#endif
+1 -8
View File
@@ -154,7 +154,6 @@
#define USE_LVGL_TOUCHSCREEN
#define USE_MDNS
#define USE_MDNS_STORE_SERVICES
#define HDMI_CEC_MESSAGE_CALLBACK_COUNT 2
#define I2C_PORT_FREQUENCY_COUNT 1
#define MDNS_SERVICE_COUNT 3
#define USE_MDNS_DYNAMIC_TXT
@@ -323,6 +322,7 @@
#define USE_API_USER_DEFINED_ACTION_RESPONSES
#define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#define API_MAX_SEND_QUEUE 8
#define API_USER_ACTION_STRINGS_SCRATCH_SIZE 64
#define API_WIZARD_DATA_SIZE 1
#define API_WIZARD_INPUT_COUNT 1
#define MAX_API_CONNECTIONS 6
@@ -496,16 +496,10 @@
#define USE_CAPTIVE_PORTAL_GZIP
#endif
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_DPP
#define USE_WIFI_DPP_URI_TRIGGER
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_FAST_CONNECT_IN_FLASH
#define USE_WIFI_LEASE_CACHE
#define USE_WIFI_LEASE_CACHE_IN_FLASH
#define USE_WIFI_AP_WISP
#define USE_WIFI_AP_WISP_STATIC_DNS
#define WIFI_AP_WISP_DNS_0 0x08080808
#define WIFI_AP_WISP_DNS_1 0x04040808
#define USE_WIFI_PHY_MODE
#define USE_WIFI_IP_STATE_LISTENERS
#define USE_WIFI_SCAN_RESULTS_LISTENERS
@@ -568,7 +562,6 @@
#define ESPHOME_ETHERNET_PHY_REGISTER_COUNT 2
#ifdef USE_ESP32
#define MICRO_WAKE_WORD_TASK_CORE 0
#define USE_MICRO_WAKE_WORD
#define USE_MICRO_WAKE_WORD_INTERNAL_RAM
#define USE_MICRO_WAKE_WORD_VAD
+3 -3
View File
@@ -7,7 +7,7 @@
namespace esphome {
bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority,
bool use_psram, BaseType_t core_id) {
bool use_psram) {
if (this->handle_ != nullptr) {
// Task is already created; must call destroy() first
return false;
@@ -38,8 +38,8 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size
return false;
}
this->handle_ = xTaskCreateStaticPinnedToCore(fn, name, this->stack_size_, param, priority, this->stack_buffer_.get(),
this->tcb_.get(), core_id);
this->handle_ =
xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_.get(), this->tcb_.get());
if (this->handle_ == nullptr) {
this->deallocate();
return false;
+1 -2
View File
@@ -37,10 +37,9 @@ class StaticTask {
/// @param param Parameter passed to task function
/// @param priority FreeRTOS task priority
/// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM
/// @param core_id CPU core, or tskNO_AFFINITY to leave scheduling unrestricted
/// @return true on success; false if either allocation fails or the task cannot be created
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority,
bool use_psram, BaseType_t core_id = tskNO_AFFINITY);
bool use_psram);
/// @brief Delete the task, keeping the TCB and stack buffer allocated for reuse by a subsequent create() call.
/// The task must have finished its work and parked itself, either suspended or blocked indefinitely: it is
+18 -44
View File
@@ -1,6 +1,5 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <iterator>
@@ -190,6 +189,24 @@ inline bool operator!=(const StringRef &lhs, const __FlashStringHelper *rhs) { r
inline bool operator!=(const __FlashStringHelper *lhs, const StringRef &rhs) { return !(rhs == lhs); }
/// Codegen use only: a flash string literal with its byte length, compared without a runtime strlen.
struct ProgmemStringRef {
PGM_P str;
size_t len;
};
inline bool operator==(const std::string &lhs, ProgmemStringRef rhs) {
return lhs.size() == rhs.len && (rhs.len == 0 || memcmp_P(lhs.data(), rhs.str, rhs.len) == 0);
}
inline bool operator!=(const std::string &lhs, ProgmemStringRef rhs) { return !(lhs == rhs); }
inline bool operator==(const StringRef &lhs, ProgmemStringRef rhs) {
// A StringRef may be {nullptr, 0}, and memcmp_P with a null pointer is undefined even for 0 bytes
return lhs.size() == rhs.len && (rhs.len == 0 || memcmp_P(lhs.c_str(), rhs.str, rhs.len) == 0);
}
inline bool operator!=(const StringRef &lhs, ProgmemStringRef rhs) { return !(lhs == rhs); }
#endif // USE_ESP8266
inline bool operator<(const StringRef &lhs, const StringRef &rhs) {
@@ -224,49 +241,6 @@ inline std::string operator+(const std::string &lhs, const StringRef &rhs) {
str.append(rhs.c_str(), rhs.size());
return str;
}
/// A string that may live in flash (on ESP8266): pointer and length, with no byte-reading operations.
/// progmem_ptr() is safe as a "%s" log argument; compare the contents with equals().
class ProgmemStringRef {
public:
constexpr ProgmemStringRef() = default;
constexpr ProgmemStringRef(const char *s, size_t len) : ref_(s, len) {}
constexpr explicit ProgmemStringRef(const StringRef &s) : ref_(s) {}
/// A PROGMEM char array, with its length known at compile time
template<size_t N> static constexpr ProgmemStringRef from_lit(const char (&s)[N]) { return {s, N - 1}; }
constexpr const char *progmem_ptr() const { return this->ref_.c_str(); }
constexpr size_t size() const { return this->ref_.size(); }
constexpr bool empty() const { return this->ref_.empty(); }
bool equals(const StringRef &other) const {
#ifdef USE_ESP8266
// A StringRef may be {nullptr, 0}, and memcmp_P with a null pointer is undefined even for 0 bytes
return other.size() == this->ref_.size() &&
(other.empty() || memcmp_P(other.c_str(), this->ref_.c_str(), other.size()) == 0);
#else
return other == this->ref_;
#endif
}
// Remove before 2027.5.0
[[deprecated(
"May point to flash; use progmem_ptr() or the owner's accessors. Removed in 2027.5.0")]] constexpr const char *
c_str() const {
return this->ref_.c_str();
}
private:
StringRef ref_;
};
#ifdef USE_ESP8266
inline bool operator==(const StringRef &lhs, const ProgmemStringRef &rhs) { return rhs.equals(lhs); }
inline bool operator!=(const StringRef &lhs, const ProgmemStringRef &rhs) { return !rhs.equals(lhs); }
inline bool operator==(const std::string &lhs, const ProgmemStringRef &rhs) { return rhs.equals(StringRef(lhs)); }
inline bool operator!=(const std::string &lhs, const ProgmemStringRef &rhs) { return !rhs.equals(StringRef(lhs)); }
#endif
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
namespace internal {
+104 -503
View File
@@ -70,13 +70,11 @@ CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02
CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04
CLIENT_FEATURE_SUPPORTS_NOISE = 0x08
CLIENT_FEATURE_SUPPORTS_DEFLATE = 0x10
CLIENT_FEATURE_SUPPORTS_UDP = 0x20
SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01
SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02
SERVER_FEATURE_SUPPORTS_NOISE = 0x04
# Binding once offered: the device then expects the image size and a deflate stream
SERVER_FEATURE_SUPPORTS_DEFLATE = 0x08
SERVER_FEATURE_SUPPORTS_UDP = 0x10
# Wire constant: the deflate bit promises a 4 KB window (OTA_INFLATE_WINDOW_SIZE)
DEFLATE_WINDOW_BITS = 12
@@ -105,64 +103,17 @@ COMPRESS_LEVEL = 9
UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8
# Flaky Wi-Fi links often drop the first OTA attempt, and the device may need time
# to clean up a half-open connection (its handshake watchdog runs at 60s) before it
# to clean up a half-open connection (its handshake watchdog runs at 20s) before it
# accepts a new one, so wait between attempts instead of failing the upload outright.
# Every resolved address is tried once, and this many extra attempts are shared
# across the addresses on top of that.
EXTRA_UPLOAD_ATTEMPTS = 4
# Once a connection has reached the device, a lossy link (not a dead device) is
# the likely cause, so keep trying like an API reconnect would
EXTRA_UPLOAD_ATTEMPTS_REACHED = 8
EXTRA_UPLOAD_ATTEMPTS = 2
UPLOAD_RETRY_DELAY = 5.0
# A SYN getting through is the cheap step; an unreachable address still fails fast
CONNECT_TIMEOUT = 20.0
# Handshake waits; as patient as an API connect on a lossy link, and matching the
# device's OTA_SOCKET_TIMEOUT_HANDSHAKE
SETUP_TIMEOUT = 60.0
# Data phase timeout; must stay longer than the device's OTA_SOCKET_TIMEOUT_DATA
# (105 s) so a stalled session is gone before a retry, and long enough for lwIP
# to get a lost chunk ack through after the retransmit run seen in practice
DATA_PHASE_TIMEOUT = 160.0
# UDP data phase; wire format in ota_esphome_udp.cpp
UDP_MSG_DATA = 0x01
UDP_MSG_PING = 0x02
UDP_MSG_PROBE = 0x03
UDP_FLAG_COMMITTED = 0x80
UDP_MSG_ACK = 0x40
UDP_HEADER_SIZE = 9
UDP_ACK_HEADER_SIZE = 12
# Stream bytes per datagram, within one Ethernet frame
UDP_MAX_PAYLOAD = 1400
# Messages the device queues unconsumed (UDP_WINDOW in ota_esphome.h); as many
# as TCP's window holds, so UDP costs the device no more memory than TCP
UDP_WINDOW = 4
# Probe this long before falling back to TCP, enough to ride out a radio busy
# with BLE; the device waits 80 s
UDP_PROBE_TIMEOUT = 15.0
UDP_PROBE_INTERVAL = 0.1
# Resend timeout bounds around srtt + 4 * rttvar; random loss never grows it
UDP_RESEND_INITIAL = 0.5
UDP_RESEND_MIN = 0.2
UDP_RESEND_MAX = 2.0
# Acks still asking for the head after later messages arrived; resend it once
UDP_DUP_ACKS = 2
# Progress acks in a row with no round trip to time: the link is slower than
# the resend timer, so back it off (random loss still yields samples)
UDP_UNTIMED_ACKS = 8
# Refusals in a row with no ACK between: the device's UDP port is closed
UDP_REFUSALS = 3
UDP_FALLBACK = b"\x00"
# The UDP format this client speaks; devices decline values they do not know
UDP_REQUEST = b"\x01"
# A plain handshake exchange slower than this hid a retransmit (the device's
# lwIP waits about a second before resending); UDP is used from then on
UDP_SLOW_EXCHANGE = 0.75
# getsockopt(IPPROTO_TCP) option, size, offset and format of the retransmit
# count: tcp_info.tcpi_total_retrans (Linux), tcp_connection_info.
# tcpi_txretransmitbytes (macOS)
_TCP_RETRANS_OPTION = {"linux": (11, 104, 100, "I"), "darwin": (0x106, 80, 72, "Q")}
_LOGGER = logging.getLogger(__name__)
# Authentication method lookup table: response -> (hash_func, nonce_size, name)
@@ -560,10 +511,6 @@ class NoiseSocketWrapper:
self._decrypt = None
self._buffer = b""
def use_transport(self, transport: UdpChannel) -> None:
"""Continue the session over another byte stream."""
self._sock = transport
# Only harmless socket controls pass through; byte-moving methods are
# deliberately absent so plaintext cannot leak past the transport.
def settimeout(self, timeout: float | None) -> None:
@@ -666,316 +613,19 @@ class NoiseSocketWrapper:
return data
class _UdpMessage:
"""One datagram of the UDP stream and its send history."""
__slots__ = ("held", "last_send", "payload", "resend", "sends", "seq")
def __init__(self, seq: int, payload: bytes) -> None:
self.seq = seq
self.payload = payload
self.last_send = 0.0
self.sends = 0
self.resend = False # duplicate acks asked for it
self.held = False # the device keeps it, waiting for an earlier one
class UdpChannel:
"""Byte stream over numbered UDP datagrams, standing in for the socket.
The device acks every datagram on arrival and says how many it has room
for; each is resent after TCP's srtt + 4 * rttvar, so loss slows the
transfer instead of ending it. It backs off only while acks come back
untimed, i.e. the link is slower than the timer, never on random loss.
"""
def __init__(self, sock: socket.socket, token: bytes) -> None:
self._sock = sock
self._token = token
self._timeout = DATA_PHASE_TIMEOUT
self._next_seq = 0
self._queue: list[_UdpMessage] = [] # not yet received, oldest first
self._rx = bytearray()
self._device_bytes = 0
self._committed = False
self._srtt: float | None = None
self._rttvar = 0.0
# Doubles while acks come back untimed, so a link slower than the
# timer still yields messages sent once to time (Karn, RFC 6298)
self._backoff = 1
self._untimed = 0
self._dup_acks = 0
self._free = UDP_WINDOW # the device's empty slots, as of its last ACK
self._last_ping = 0.0
self._last_progress = time.monotonic()
self._sends = 0
# Last socket error, e.g. refused once the device's socket is gone
self._last_error: OSError | None = None
self._refusals = 0
def settimeout(self, timeout: float) -> None:
self._timeout = timeout
def setsockopt(self, level: int, optname: int, value: int) -> None:
"""TCP tuning does not apply."""
def close(self) -> None:
_LOGGER.debug(
"UDP: %d messages in %d datagrams, smoothed RTT %.0f ms",
self._next_seq,
self._sends,
(self._srtt or 0) * 1000,
)
self._sock.close()
def probe(self) -> bool:
"""True once the device answers over UDP within UDP_PROBE_TIMEOUT."""
deadline = time.monotonic() + UDP_PROBE_TIMEOUT
try:
while (now := time.monotonic()) < deadline:
self._send(UDP_MSG_PROBE, 0, b"")
self._receive(min(UDP_PROBE_INTERVAL, deadline - now))
if self._committed:
return True
except OTANetworkError:
pass # rejected, e.g. by a firewall
return False
def sendall(self, data: bytes) -> None:
for offset in range(0, len(data), UDP_MAX_PAYLOAD):
self._queue.append(
_UdpMessage(self._next_seq, data[offset : offset + UDP_MAX_PAYLOAD])
)
self._next_seq = (self._next_seq + 1) & 0xFFFF
# Return once everything is at least in flight, or the device spoke (an error)
while len(self._queue) > UDP_WINDOW and not self._rx:
self._pump()
def pending(self) -> bool:
"""Whether the device sent bytes nobody has read yet."""
return bool(self._rx)
def recv(self, amount: int) -> bytes:
while not self._rx:
self._pump()
data = bytes(self._rx[:amount])
del self._rx[:amount]
return data
def flush(self, timeout: float) -> None:
"""Best effort: wait until the device has every message."""
deadline = time.monotonic() + timeout
with contextlib.suppress(OTANetworkError):
while self._queue and time.monotonic() < deadline:
self._pump()
def _rto(self) -> float:
if self._srtt is None:
rto = UDP_RESEND_INITIAL
else:
rto = max(self._srtt + 4 * self._rttvar, UDP_RESEND_MIN)
return min(rto * self._backoff, UDP_RESEND_MAX)
def _send(self, msg_type: int, seq: int, payload: bytes) -> None:
flags = UDP_FLAG_COMMITTED if self._committed else 0
header = (
bytes([msg_type | flags])
+ self._token
+ seq.to_bytes(2, "big")
+ (self._device_bytes & 0xFFFF).to_bytes(2, "big")
)
self._sends += 1
try:
self._sock.send(header + payload)
except OSError as err:
# A full buffer or ICMP error is just more loss, but worth reporting
self._socket_error(err)
def _pump(self) -> None:
now = time.monotonic()
if now - self._last_progress > self._timeout:
reason = f" (last error: {self._last_error})" if self._last_error else ""
raise OTANetworkError(
f"no progress over UDP for {self._timeout:.0f}s{reason}"
)
rto = self._rto()
wake = now + rto
# At least the head, which doubles as the probe while the device is busy
in_flight = self._queue[: max(self._free, 1)]
for message in in_flight:
if message.held:
continue
if message.sends == 0 or message.resend or now - message.last_send >= rto:
message.last_send = now
message.sends += 1
message.resend = False
self._send(UDP_MSG_DATA, message.seq, message.payload)
wake = min(wake, message.last_send + rto)
if all(message.held for message in in_flight):
# Nothing to send: ask for the device's responses
if now - self._last_ping >= rto:
self._last_ping = now
self._send(UDP_MSG_PING, 0, b"")
wake = self._last_ping + rto
self._receive(wake - now)
def _receive(self, wait: float) -> None:
import select
# Wait for the first datagram, then drain whatever else arrived
while select.select([self._sock], [], [], max(wait, 0))[0]:
wait = 0
try:
data = self._sock.recv(2048)
except OSError as err:
self._socket_error(err)
continue
self._handle_ack(data)
def _socket_error(self, err: OSError) -> None:
if self._last_error is None or err.errno != self._last_error.errno:
_LOGGER.debug("UDP socket error: %s", err)
self._last_error = err
# Windows reports the ICMP port unreachable as a reset
if isinstance(err, (ConnectionRefusedError, ConnectionResetError)):
self._refusals += 1
if self._refusals >= UDP_REFUSALS:
raise OTANetworkError(f"device closed its UDP port: {err}")
def _sample_rtt(self, sample: float) -> None:
# RFC 6298 smoothing
self._backoff = 1
self._untimed = 0
if self._srtt is None:
self._srtt, self._rttvar = sample, sample / 2
else:
self._rttvar = 0.75 * self._rttvar + 0.25 * abs(self._srtt - sample)
self._srtt = 0.875 * self._srtt + 0.125 * sample
def _handle_ack(self, data: bytes) -> None:
if (
len(data) < UDP_ACK_HEADER_SIZE
or data[0] != UDP_MSG_ACK
or data[1:5] != self._token
):
return
now = time.monotonic()
self._committed = True
self._refusals = 0
received = int.from_bytes(data[5:7], "big")
prompted = int.from_bytes(data[7:9], "big")
self._free = data[11] >> 4
if self._queue:
head = self._queue[0].seq
index = (prompted - head) & 0xFFFF
# This datagram's own round trip; only from messages sent once (Karn)
timed = index < len(self._queue) and self._queue[index].sends == 1
if timed:
self._sample_rtt(now - self._queue[index].last_send)
done = (received - head) & 0xFFFF
if 0 < done <= len(self._queue):
del self._queue[:done]
self._last_progress = now
self._dup_acks = 0
if not timed:
self._untimed += 1
if self._untimed >= UDP_UNTIMED_ACKS:
self._untimed = 0
if self._rto() < UDP_RESEND_MAX:
self._backoff *= 2
elif done == 0 and self._queue[0].sends:
# A later message arrived while the head is missing; resend it once
self._dup_acks += 1
if self._dup_acks == UDP_DUP_ACKS:
self._queue[0].resend = True
# Rebuilt from every ACK, so a reordered one cannot leave a stale mark
for k, message in enumerate(self._queue[:UDP_WINDOW]):
message.held = (received + k - message.seq) & 0xFFFF == 0 and bool(
data[11] & (1 << k)
)
skip = (self._device_bytes - int.from_bytes(data[9:11], "big")) & 0xFFFF
fresh = data[UDP_ACK_HEADER_SIZE + skip :]
if skip <= len(data) - UDP_ACK_HEADER_SIZE and fresh:
self._rx += fresh
self._device_bytes += len(fresh)
self._last_progress = now
def _tcp_retransmits(sock: socket.socket) -> int | None:
"""Segments this side retransmitted, where the OS reports it."""
import struct
import sys
if (option := _TCP_RETRANS_OPTION.get(sys.platform)) is None:
return None
optname, size, offset, fmt = option
try:
info = sock.getsockopt(socket.IPPROTO_TCP, optname, size)
except (OSError, TypeError):
return None
if len(info) < offset + struct.calcsize(fmt):
return None
return struct.unpack_from(fmt, info, offset)[0]
def _link_lossy(
tcp_sock: socket.socket, slowest_exchange: float, prefer_udp: bool
) -> bool:
"""Whether the handshake showed loss, so the data phase should use UDP."""
return (
prefer_udp
or slowest_exchange > UDP_SLOW_EXCHANGE
or bool(_tcp_retransmits(tcp_sock))
)
def _start_udp(
sock: socket.socket | NoiseSocketWrapper, tcp_sock: socket.socket, lossy: bool
) -> UdpChannel | None:
"""Move the data phase to UDP; None keeps it on TCP."""
# A clean link asks to stay on TCP
send_check(sock, UDP_REQUEST if lossy else UDP_FALLBACK, "UDP request")
token = receive_exactly(sock, 4, "UDP token", None, decode=False)
if not lossy:
_LOGGER.debug("Link looks clean, staying on TCP")
return None
if token == bytes(4):
_LOGGER.debug("Device declined UDP")
return None
udp_sock = socket.socket(tcp_sock.family, socket.SOCK_DGRAM)
try:
udp_sock.connect(tcp_sock.getpeername())
except OSError as err:
udp_sock.close()
_LOGGER.info("UDP unavailable (%s), using TCP", err)
else:
channel = UdpChannel(udp_sock, token)
if channel.probe():
_LOGGER.info("Using UDP")
return channel
channel.close()
_LOGGER.info("UDP blocked, using TCP")
# Raw, outside any Noise frame: forging it only forces the TCP path
send_check(tcp_sock, UDP_FALLBACK, "UDP fallback")
return None
def _negotiate_session(
sock: socket.socket,
noise_psk: str | None,
plaintext_fallback: bool = False,
allow_plaintext_upload: bool = False,
) -> tuple[socket.socket | NoiseSocketWrapper, int, int, bool, float]:
) -> tuple[socket.socket | NoiseSocketWrapper, int, int, bool]:
"""Negotiate through the Noise handshake; returns the socket to go on with
(wrapped once encrypted), version, features, extended and the slowest
plaintext exchange in seconds."""
(wrapped once encrypted), version, features, extended."""
# Enable nodelay, we need it for phase 1
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
exchange_start = time.monotonic()
send_check(sock, MAGIC_BYTES, "magic bytes")
_, version = receive_exactly(sock, 2, "version", RESPONSE_OK)
slowest_exchange = time.monotonic() - exchange_start
_LOGGER.debug("Device supports OTA version %s", version)
supported_versions = (OTA_VERSION_1_0, OTA_VERSION_2_0)
if version not in supported_versions:
@@ -989,11 +639,9 @@ def _negotiate_session(
| CLIENT_FEATURE_SUPPORTS_SHA256_AUTH
| CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL
| CLIENT_FEATURE_SUPPORTS_DEFLATE
| CLIENT_FEATURE_SUPPORTS_UDP
)
if noise_psk:
features_to_send |= CLIENT_FEATURE_SUPPORTS_NOISE
exchange_start = time.monotonic()
send_check(sock, features_to_send, "features")
features = receive_exactly(
sock,
@@ -1001,7 +649,6 @@ def _negotiate_session(
"features",
None, # Accept any response
)[0]
slowest_exchange = max(slowest_exchange, time.monotonic() - exchange_start)
extended_proto = False
if features == RESPONSE_FEATURE_FLAGS:
@@ -1058,7 +705,7 @@ def _negotiate_session(
_LOGGER.debug("Encrypted connection established")
if allow_plaintext_upload:
_LOGGER.warning(ALLOW_PLAINTEXT_UPLOAD_REMOVE_WARNING)
return sock, version, features, extended_proto, slowest_exchange
return sock, version, features, extended_proto
def perform_ota(
@@ -1071,7 +718,6 @@ def perform_ota(
plaintext_fallback: bool = False,
allow_plaintext_upload: bool = False,
on_upload_start: Callable[[], None] | None = None,
prefer_udp: bool = False,
) -> None:
# Validate up front; an out-of-range value would only surface as a
# ValueError deep inside send_check, bypassing OTAError handling
@@ -1094,9 +740,7 @@ def perform_ota(
file_size = len(file_contents)
_LOGGER.info("Uploading %s (%s bytes)", filename, file_size)
# sock may become a Noise wrapper or a UDP channel; UDP needs the peer
tcp_sock = sock
sock, version, features, extended_proto, slowest_exchange = _negotiate_session(
sock, version, features, extended_proto = _negotiate_session(
sock, noise_psk, plaintext_fallback, allow_plaintext_upload
)
_LOGGER.info("Connection established; device supports OTA version %s", version)
@@ -1188,153 +832,117 @@ def perform_ota(
if on_upload_start is not None:
on_upload_start() # the device may commit anything sent from here
udp: UdpChannel | None = None
# Still under SETUP_TIMEOUT, which the device's commit wait is sized for
if extended_proto and features & SERVER_FEATURE_SUPPORTS_UDP:
udp = _start_udp(
sock, tcp_sock, _link_lossy(tcp_sock, slowest_exchange, prefer_udp)
)
if udp is not None:
# The device drops TCP once it hears the committed channel
if isinstance(sock, NoiseSocketWrapper):
sock.use_transport(udp)
else:
sock = udp
sock.settimeout(DATA_PHASE_TIMEOUT)
if extended_proto:
send_check(sock, ota_type, "ota type")
upload_size = len(upload_contents)
# The device erases flash between receiving the size and acking the
# prepare, so this window shows the erase cost (near zero when the
# device erases lazily during the upload)
prepare_start = time.perf_counter()
send_check(sock, upload_size.to_bytes(SIZE_FIELD_BYTES, "big"), "binary size")
if deflate:
# Own frame: an encrypted session carries one field per frame
send_check(sock, file_size.to_bytes(SIZE_FIELD_BYTES, "big"), "image size")
receive_exactly(sock, 1, "update prepare result", RESPONSE_UPDATE_PREPARE_OK)
prepare_duration = time.perf_counter() - prepare_start
_LOGGER.info("Preparing for upload took %.2f seconds", prepare_duration)
# The device hashes what it writes: the inflated image, else the received bytes
upload_md5 = hashlib.md5(file_contents if deflate else upload_contents).hexdigest()
_LOGGER.debug("MD5 of upload is %s", upload_md5)
send_check(sock, upload_md5, "file checksum")
receive_exactly(sock, 1, "file checksum result", RESPONSE_BIN_MD5_OK)
# Disable nodelay for transfer
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)
# Limit send buffer (usually around 100kB) in order to have progress bar
# show the actual progress
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_BUFFER_SIZE)
start_time = time.perf_counter()
offset = 0
progress = ProgressBar("Uploading")
try:
if extended_proto:
send_check(sock, ota_type, "ota type")
upload_size = len(upload_contents)
# The device erases flash between receiving the size and acking the
# prepare, so this window shows the erase cost (near zero when the
# device erases lazily during the upload)
prepare_start = time.perf_counter()
send_check(sock, upload_size.to_bytes(SIZE_FIELD_BYTES, "big"), "binary size")
if deflate:
# Own frame: an encrypted session carries one field per frame
send_check(sock, file_size.to_bytes(SIZE_FIELD_BYTES, "big"), "image size")
receive_exactly(sock, 1, "update prepare result", RESPONSE_UPDATE_PREPARE_OK)
prepare_duration = time.perf_counter() - prepare_start
_LOGGER.info("Preparing for upload took %.2f seconds", prepare_duration)
# The device hashes what it writes: the inflated image, else the received bytes
upload_md5 = hashlib.md5(
file_contents if deflate else upload_contents
).hexdigest()
_LOGGER.debug("MD5 of upload is %s", upload_md5)
send_check(sock, upload_md5, "file checksum")
receive_exactly(sock, 1, "file checksum result", RESPONSE_BIN_MD5_OK)
# Disable nodelay for transfer
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)
# Limit send buffer (usually around 100kB) in order to have progress bar
# show the actual progress
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_BUFFER_SIZE)
start_time = time.perf_counter()
offset = 0
progress = ProgressBar("Uploading")
try:
while True:
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
if not chunk:
break
offset += len(chunk)
while True:
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
if not chunk:
break
offset += len(chunk)
try:
sock.sendall(chunk)
except OSError as err:
# A send failure can hide an error byte the device reported
# just before dropping the connection; surface that as the
# real, non-retryable cause when it is available
try:
sock.sendall(chunk)
except OSError as err:
# A send failure can hide an error byte the device reported
# just before dropping the connection; surface that as the
# real, non-retryable cause when it is available
try:
sock.settimeout(1.0)
check_error(recv_decode(sock, 1), None)
except (OSError, OTANetworkError) as probe_err:
_LOGGER.debug(
"No device error behind the send failure: %s", probe_err
)
raise OTANetworkError(f"sending data: {err}") from err
sock.settimeout(1.0)
check_error(recv_decode(sock, 1), None)
except (OSError, OTANetworkError) as probe_err:
_LOGGER.debug(
"No device error behind the send failure: %s", probe_err
)
raise OTANetworkError(f"sending data: {err}") from err
if version >= OTA_VERSION_2_0:
try:
receive_exactly(sock, 1, "chunk result", RESPONSE_CHUNK_OK)
except OTANetworkError as err:
if udp is None:
raise
# Over UDP: surface a device error code if one arrived, and
# treat a failure on the last chunk like a lost final ack
if udp.pending():
receive_exactly(sock, 1, "data", [])
if offset < upload_size:
raise
# The device already had the complete image when this ack
# was lost, so it may be committing; do not retry
raise _committed_error(err) from err
# The UDP channel acks every message itself, so anything the device
# sends mid upload is an error code
if udp is not None and udp.pending() and offset < upload_size:
receive_exactly(sock, 1, "data", [])
if version >= OTA_VERSION_2_0 and udp is None:
try:
receive_exactly(sock, 1, "chunk result", RESPONSE_CHUNK_OK)
except OTANetworkError as err:
if offset < upload_size:
raise
# The device already had the complete image when this ack
# was lost, so it may be committing; do not retry
raise _committed_error(err) from err
progress.update(offset / upload_size)
except OTAError:
# Terminate the progress bar line before the error is logged
progress.done()
raise
progress.update(offset / upload_size)
except OTAError:
# Terminate the progress bar line before the error is logged
progress.done()
raise
progress.done()
# Enable nodelay for last checks
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
duration = time.perf_counter() - start_time
# Enable nodelay for last checks
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
duration = time.perf_counter() - start_time
_LOGGER.info("Upload took %.2f seconds, waiting for result...", duration)
_LOGGER.info("Upload took %.2f seconds, waiting for result...", duration)
# Once the device has the complete image it commits the update and
# reboots on its own; the exact commit point is not observable from
# here, so treat everything past the data phase as non-retryable. A
# re-upload could flash a device that already updated successfully.
commit_start = time.perf_counter()
try:
receive_exactly(sock, 1, "update receive result", RESPONSE_RECEIVE_OK)
receive_exactly(sock, 1, "update end result", RESPONSE_UPDATE_END_OK)
except OTANetworkError as err:
raise _committed_error(err) from err
commit_duration = time.perf_counter() - commit_start
# Once the device has the complete image it commits the update and
# reboots on its own; the exact commit point is not observable from
# here, so treat everything past the data phase as non-retryable. A
# re-upload could flash a device that already updated successfully.
commit_start = time.perf_counter()
try:
receive_exactly(sock, 1, "update receive result", RESPONSE_RECEIVE_OK)
receive_exactly(sock, 1, "update end result", RESPONSE_UPDATE_END_OK)
except OTANetworkError as err:
raise _committed_error(err) from err
commit_duration = time.perf_counter() - commit_start
# Sum of the named windows so the breakdown is self consistent; connect,
# handshake, auth, and the one MD5 round trip are not included
_LOGGER.info(
"Update took %.2f seconds (prepare %.2f, upload %.2f, commit %.2f)",
prepare_duration + duration + commit_duration,
prepare_duration,
duration,
commit_duration,
)
# Sum of the named windows so the breakdown is self consistent; connect,
# handshake, auth, and the one MD5 round trip are not included
_LOGGER.info(
"Update took %.2f seconds (prepare %.2f, upload %.2f, commit %.2f)",
prepare_duration + duration + commit_duration,
prepare_duration,
duration,
commit_duration,
)
try:
send_check(sock, RESPONSE_OK, "end acknowledgement")
except OTANetworkError as err:
# The device treats a missing end acknowledgement as non-fatal and is
# already rebooting into the new firmware, so the update succeeded
_LOGGER.warning("Failed sending end acknowledgement: %s", err)
_LOGGER.info("OTA successful (end acknowledgement not delivered)")
else:
_LOGGER.info("OTA successful")
if udp is not None:
# Our end acknowledgement also tells the device we have its
# responses, ending its wait before it reboots
udp.flush(2.0)
finally:
if udp is not None:
udp.close()
try:
send_check(sock, RESPONSE_OK, "end acknowledgement")
except OTANetworkError as err:
# The device treats a missing end acknowledgement as non-fatal and is
# already rebooting into the new firmware, so the update succeeded
_LOGGER.warning("Failed sending end acknowledgement: %s", err)
_LOGGER.info("OTA successful (end acknowledgement not delivered)")
else:
_LOGGER.info("OTA successful")
# Do not connect logs until it is fully on
time.sleep(1)
@@ -1513,7 +1121,7 @@ def run_ota_impl_(
# are shared across the addresses, cycling through them. Wait before an
# attempt when the previous one actually reached the device, or when
# revisiting an address, so a flaky link can recover and the device can
# clean up a half-open connection (its handshake watchdog runs at 60s);
# clean up a half-open connection (its handshake watchdog runs at 20s);
# moving on to the next address family stays immediate. Known limitation:
# a silent mid-transfer drop with no reset can wedge the device until its
# 105s data timeout, which outlasts this budget; the retries target the
@@ -1531,8 +1139,6 @@ def run_ota_impl_(
on_upload_start()
on_upload_start = None # once per upload, whatever the retries do
# A network failure means a lossy link, so later attempts send data over UDP
prefer_udp = False
while attempt < total_attempts:
af, socktype, _, _, sa = res[attempt % len(res)]
if not reconnect and (reached_device or attempt >= len(res)):
@@ -1547,7 +1153,7 @@ def run_ota_impl_(
reconnect = False
_LOGGER.info("Connecting to %s port %s...", sa[0], sa[1])
sock = socket.socket(af, socktype)
sock.settimeout(CONNECT_TIMEOUT)
sock.settimeout(20.0)
try:
sock.connect(sa)
except OSError as err:
@@ -1558,9 +1164,7 @@ def run_ota_impl_(
continue
_LOGGER.info("Connected to %s", sa[0])
sock.settimeout(SETUP_TIMEOUT)
reached_device = True
total_attempts = max(total_attempts, len(res) + EXTRA_UPLOAD_ATTEMPTS_REACHED)
with contextlib.closing(sock), Path(filename).open("rb") as file_handle:
try:
perform_ota(
@@ -1573,7 +1177,6 @@ def run_ota_impl_(
encryption.plaintext_fallback,
allow_plaintext_upload=allow_plaintext_upload,
on_upload_start=upload_started,
prefer_udp=prefer_udp,
)
except OTAKeyRejected as err:
# The device sent a reject and closed on its own, so reconnect
@@ -1593,7 +1196,6 @@ def run_ota_impl_(
raise
except OTAHandshakeNetworkError as err:
last_error = str(err)
prefer_udp = True
if encryption.handshake_fault_falls_back():
encryption.downgrade(last_error)
continue
@@ -1603,7 +1205,6 @@ def run_ota_impl_(
except OTANetworkError as err:
# Transient network failure; retry
last_error = str(err)
prefer_udp = True
_LOGGER.warning("%s", last_error)
attempt += 1
continue
-4
View File
@@ -85,10 +85,6 @@ dependencies:
version: "1.1.0"
rules:
- if: "idf_version >=6.0.0"
espressif/ksz8851snl:
version: "1.2.0"
rules:
- if: "idf_version >=6.0.0"
# Keep in sync with esphome/components/ethernet/__init__.py
espressif/ch390:
version: "0.3.0"
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.2.0
esptool==5.5.0
click==8.5.0
aioesphomeapi==46.12.0
aioesphomeapi==46.11.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.5
puremagic==2.2.0
+18 -43
View File
@@ -536,19 +536,16 @@ def create_field_type_info(
f"track_presence on field '{field.name}' has no effect; it requires "
"a non-repeated message field in a message that is decoded"
)
if get_field_opt(field, pb.progmem, False):
is_pointer_bytes = (
field.type == FieldDescriptorProto.TYPE_BYTES
and get_field_opt(field, pb.pointer_to_buffer, False)
and get_field_opt(field, pb.fixed_array_size) is None
if get_field_opt(field, pb.progmem, False) and (
field.label == FieldDescriptorProto.LABEL_REPEATED
or field.type != 12
or not get_field_opt(field, pb.pointer_to_buffer, False)
or get_field_opt(field, pb.fixed_array_size) is not None
):
raise ValueError(
f"progmem on field '{field.name}' requires a non-repeated bytes field "
"with pointer_to_buffer"
)
if field.label == FieldDescriptorProto.LABEL_REPEATED or not (
field.type == FieldDescriptorProto.TYPE_STRING or is_pointer_bytes
):
raise ValueError(
f"progmem on field '{field.name}' requires a non-repeated string field, "
"or a bytes field with pointer_to_buffer"
)
if field.label == FieldDescriptorProto.LABEL_REPEATED:
# Check if this is a packed_buffer field (zero-copy packed repeated)
if get_field_opt(field, pb.packed_buffer, False):
@@ -1158,11 +1155,6 @@ class PointerToBufferTypeBase(TypeInfo):
"""Get the wire type for this field."""
return WireType.LENGTH_DELIMITED # Uses wire type 2
@property
def progmem(self) -> bool:
"""Whether the data is in flash, so encode and dump copy it with progmem_memcpy."""
return get_field_opt(self._field, pb.progmem, False)
def get_estimated_size(self) -> int:
# field ID + length varint + typical data (assume small for pointer fields)
return self.calculate_field_id_size() + 2 + 16
@@ -1176,6 +1168,11 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
reference_type = "const uint8_t*"
const_reference_type = "const uint8_t*"
@property
def progmem(self) -> bool:
"""Whether the data is in flash, so encode and dump copy it with progmem_memcpy."""
return get_field_opt(self._field, pb.progmem, False)
@property
def public_content(self) -> list[str]:
# Use uint16_t for length - max packet size is well below 65535
@@ -1255,8 +1252,6 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
@property
def public_content(self) -> list[str]:
if self.progmem:
return [f"ProgmemStringRef {self.field_name}{{}};"]
if self._starts_null:
return [
f"StringRef {self.field_name}{{nullptr, 0}}; // null until set, encode only"
@@ -1272,19 +1267,9 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
)
tag = self.calculate_tag()
if tag < 128:
encode_fn = (
"encode_short_progmem_string_force"
if self.progmem
else "encode_short_string_force"
return _encode_call(
"encode_short_string_force", str(tag), f"this->{self.field_name}"
)
return _encode_call(encode_fn, str(tag), f"this->{self.field_name}")
if self.progmem:
return _encode_call(
"encode_progmem_string",
str(self.number),
f"this->{self.field_name}",
force=self.force,
)
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}.c_str()",
f"this->{self.field_name}.size()",
@@ -1312,8 +1297,6 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
@property
def dump_content(self) -> str:
if self.progmem:
return f'dump_progmem_string_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});'
return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});'
def get_size_calculation(self, name: str, force: bool = False) -> str:
@@ -3263,9 +3246,8 @@ namespace esphome::api {
#ifdef USE_ESP8266
// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site
void DumpBuffer::append_p_esp8266(const char *str) { this->append_p_esp8266(str, strlen_P(str)); }
void DumpBuffer::append_p_esp8266(const char *str, size_t len) {
void DumpBuffer::append_p_esp8266(const char *str) {
size_t len = strlen_P(str);
size_t space = CAPACITY - 1 - pos_;
if (len > space)
len = space;
@@ -3386,13 +3368,6 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
progmem_memcpy(data_buf, data, len);
dump_bytes_field(out, field_name, data_buf, len, indent);
}
// Helper for string fields in flash
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append_p(value.progmem_ptr(), value.size()).append("'\\n");
}
#pragma GCC diagnostic pop
"""
+2 -3
View File
@@ -1258,9 +1258,8 @@ def lint_no_std_bind(fname, match):
)
def lint_no_std_nothrow(fname, match):
return (
f"{highlight('new (std::nothrow)')} aborts on ESP-IDF when the allocation fails, exceptions are disabled "
f"there, so it never returns nullptr.\n"
f"Please use {highlight('RAMAllocator')} from esphome/core/helpers.h, which does.\n"
f"Use {highlight('RAMAllocator')} from esphome/core/helpers.h instead of {highlight('new (std::nothrow)')}; "
f"it returns nullptr on failure on every platform and can choose PSRAM or internal RAM.\n"
f" Before: {highlight('auto *buf = new (std::nothrow) uint8_t[n];')}\n"
f" After: {highlight('auto buf = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(n);')}\n"
f"For one object use {highlight('RAMAllocator<T>().make_unique(args...)')}; both return empty on failure.\n"
@@ -8,13 +8,18 @@ import pytest
from esphome.components.api import (
_action_strings,
_action_strings_size,
_has_action_metadata,
_validate_esp8266_action_strings,
validate_variable,
)
from esphome.config_validation import Invalid
from esphome.const import PlatformFramework
from esphome.core import CORE
from esphome.cpp_generator import safe_exp
from esphome.helpers import fnv1_hash
from tests.component_tests.helpers import get_define_value
from tests.component_tests.types import SetCoreConfigCallable
CONFIG = "tests/component_tests/api/test_action_metadata.yaml"
CONFIG_ESP8266 = "tests/component_tests/api/test_action_metadata_esp8266.yaml"
@@ -40,6 +45,7 @@ def test_metadata_is_emitted_as_progmem_table(
)
assert f"(api_action0_strings, {safe_exp(fnv1_hash('play_buzzer'))});" in main_cpp
assert "USE_API_USER_DEFINED_ACTION_METADATA" in {d.name for d in CORE.defines}
assert get_define_value("API_USER_ACTION_STRINGS_SCRATCH_SIZE") is None
def test_esp8266_strings_are_shared_progmem_arrays(
@@ -63,13 +69,15 @@ def test_esp8266_strings_are_shared_progmem_arrays(
)
def test_esp8266_encodes_action_strings_from_flash(
def test_esp8266_sizes_scratch_buffer_for_largest_action(
generate_main: Callable[[str | Path], str],
) -> None:
"""ESP8266 uses the same flash string table."""
main_cpp = generate_main(CONFIG_ESP8266)
"""ESP8266 gets a scratch buffer define equal to the byte total of the largest action."""
generate_main(CONFIG_ESP8266)
assert "api_action0_strings[] PROGMEM" in main_cpp
# play_buzzer: name, description, two variable names, one description, one example,
# each with a terminator
assert get_define_value("API_USER_ACTION_STRINGS_SCRATCH_SIZE") == "117"
def test_shorthand_variables_emit_no_metadata(
@@ -105,8 +113,36 @@ def test_variable_rejects_invalid(value: object) -> None:
validate_variable(value)
def test_empty_metadata_is_unset() -> None:
"""An empty description or example emits nullptr."""
def _oversized_action_config() -> dict:
return {
"actions": [
{
"action": "big",
"description": "x" * 300,
"variables": {"a": {"type": "string", "example": "y" * 300}},
}
]
}
def test_esp8266_rejects_actions_over_string_budget(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(PlatformFramework.ESP8266_ARDUINO)
with pytest.raises(Invalid, match="ESP8266 allows at most 384 bytes"):
_validate_esp8266_action_strings(_oversized_action_config())
def test_other_platforms_have_no_string_budget(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
config = _oversized_action_config()
assert _validate_esp8266_action_strings(config) is config
def test_empty_metadata_is_unset_and_not_counted() -> None:
"""An empty description or example emits nullptr and takes no scratch space."""
conf = {
"action": "a",
"description": "",
@@ -114,6 +150,8 @@ def test_empty_metadata_is_unset() -> None:
}
strings = _action_strings(conf, has_metadata=True)
assert strings == ["a", None, "b", None, "ex"]
# Every emitted string counts its terminator: "a" + "b" + "ex"
assert _action_strings_size(strings) == 2 + 2 + 3
def test_empty_metadata_does_not_enable_the_define() -> None:
@@ -93,12 +93,12 @@ def test_transform_matches_model_support(
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
# The full-height ST7701S supports both mirror axes but not swap_xy (RGB displays
# ESP32-8048S070 supports both mirror axes but not swap_xy (RGB displays
# never support axis swapping).
model = MODELS["ST7701S"]
model = MODELS["ESP32-8048S070"]
assert model.transforms == {CONF_MIRROR_X, CONF_MIRROR_Y}
base = {"model": "ST7701S", "data_pins": DATA_PINS, "pclk_pin": 21}
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": False}})
# An unsupported axis may be explicitly disabled (a harmless no-op)...
@@ -139,35 +139,6 @@ def test_st7701s_only_supports_mirror_x(
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": True}})
def test_pure_rgb_model_has_no_hardware_transform(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A model with no init sequence has no command interface, so no transform
can be applied in hardware and every rotation must be done in software.
Previously such models claimed mirror_x/mirror_y support, so a rotation of
180 was handed to the (nonexistent) hardware and the display was not rotated.
"""
_set_s3(set_core_config)
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
model = MODELS["ESP32-8048S070"]
assert model.initsequence == ()
assert model.transforms == set()
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}
for rotation in (90, 180, 270):
config = CONFIG_SCHEMA({**base, "id": f"rot_{rotation}", "rotation": rotation})
assert not model.rotation_as_transform(config)
metadata = get_display_metadata(config["id"])
assert metadata.rotation == rotation
assert not metadata.has_hardware_rotation
with pytest.raises(cv.Invalid, match="'mirror_x' is not supported by this model"):
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True}})
def test_metadata_records_rotation(
set_core_config: SetCoreConfigCallable,
) -> None:
@@ -1,11 +0,0 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
@@ -9,10 +9,7 @@ from pathlib import Path
import pytest
from esphome.components.network import (
require_high_performance_networking,
require_high_performance_wifi,
)
from esphome.components.network import require_high_performance_networking
from tests.component_tests.network import sdkconfig_option
@@ -55,15 +52,3 @@ def test_lwip_tier(
assert sdkconfig_option("CONFIG_LWIP_WND_SCALE") is window_scale
# Wifi RX buffers really go to PSRAM, so the wifi tier is never downgraded
assert sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM") == wifi_rx_buffers
def test_wifi_only_request_leaves_lwip_alone(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A WiFi-only request sizes the driver buffers but not the TCP windows."""
require_high_performance_wifi()
generate_main(component_config_path("high_perf_wifi_only.yaml"))
assert sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT") is None
assert sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE") == 64
assert sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM") == 64
@@ -1,30 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::core::testing {
TEST(ProgmemStringRef, EqualsComparesLengthAndContent) {
ProgmemStringRef ref("Kitchen", 7);
EXPECT_TRUE(ref.equals(StringRef("Kitchen")));
EXPECT_FALSE(ref.equals(StringRef("Kitche")));
EXPECT_FALSE(ref.equals(StringRef("Kitchens")));
EXPECT_FALSE(ref.equals(StringRef("Bedroom")));
}
TEST(ProgmemStringRef, FromLitTakesTheLengthAtCompileTime) {
static const char NAME[] PROGMEM = "entity_id";
constexpr ProgmemStringRef ref = ProgmemStringRef::from_lit(NAME);
static_assert(ref.size() == 9);
EXPECT_TRUE(ref.equals(StringRef("entity_id")));
}
TEST(ProgmemStringRef, DefaultIsEmpty) {
ProgmemStringRef ref;
EXPECT_TRUE(ref.empty());
EXPECT_EQ(ref.size(), 0u);
EXPECT_TRUE(ref.equals(StringRef("")));
}
} // namespace esphome::core::testing
@@ -121,7 +121,7 @@ struct Rig {
DaikinClimate climate;
Rig() {
// A second fixture in the same test would otherwise restore what the first saved
// All instances share one preference key; clear it so setup starts from defaults
if (global_preferences != nullptr)
global_preferences->reset();
static const bool APP_CONSTRUCTED = (new (&App) Application(), true);
+1 -1
View File
@@ -143,7 +143,7 @@ struct Fixture {
/// Pass keep_preferences to restore what an earlier fixture saved
explicit Fixture(bool keep_preferences = false) {
// A second fixture in the same test would otherwise restore what the first saved
// Every fixture shares one preference key, so clear what an earlier test saved or setup restores it
if (!keep_preferences && global_preferences != nullptr)
global_preferences->reset();
// The test main does not construct App as generated code does; the boost timer needs its scheduler
@@ -1,16 +0,0 @@
esphome:
on_boot:
then:
- audio_dac.mute_off:
- audio_dac.mute_on:
- audio_dac.set_volume:
volume: 50%
audio_dac:
- platform: es9038q2m_katana
i2c_id: i2c_bus
filter_shape: hybrid
bits_per_sample: 24bit
sample_rate: 96000
deemphasis: 48khz
dop: true
@@ -1,3 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
es9038q2m_katana: !include common.yaml
@@ -1,3 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
es9038q2m_katana: !include common.yaml
@@ -8,6 +8,7 @@
#include <string>
#include "esphome/components/exponential_moving_average/exponential_moving_average_sensor.h"
#include "esphome/core/preferences.h"
#ifdef USE_HOST
#include "esphome/components/host/preferences.h"
#endif
@@ -32,9 +33,11 @@ class ExponentialMovingAverageTest : public ::testing::Test {
#ifdef USE_HOST
host::setup_preferences();
#endif
global_preferences->reset();
}
void TearDown() override {
global_preferences->reset();
if (this->saved_prefdir_.has_value()) {
setenv("ESPHOME_PREFDIR", this->saved_prefdir_->c_str(), 1);
} else {
@@ -4,6 +4,7 @@
#include "esphome/components/friedrich/friedrich.h"
#include "esphome/components/remote_base/aeha_protocol.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
namespace esphome::friedrich::testing {
@@ -41,7 +42,12 @@ struct Fixture {
TestFriedrich sut;
CapturingTransmitter transmitter;
Fixture() { this->sut.set_transmitter(&this->transmitter); }
Fixture() {
// Climate tests share one preference key, so clear what an earlier test saved or setup restores it
if (global_preferences != nullptr)
global_preferences->reset();
this->sut.set_transmitter(&this->transmitter);
}
// The last transmission decoded, or an empty frame if it could not be decoded
AEHAData sent() { return AEHAProtocol().decode(rx_of(this->transmitter.last)).value_or(AEHAData{}); }
-4
View File
@@ -1,4 +0,0 @@
fusb302b:
id: pd
interrupt_pin: ${interrupt_pin}
request_voltage: 20
@@ -1,6 +0,0 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
fusb302b: !include common.yaml
-18
View File
@@ -1,18 +0,0 @@
hdmi_cec:
id: cec
pin: ${cec_pin} # HDMI pin 13, with HDMI pin 17 as ground
address: 0x5
promiscuous_mode: true
on_message:
- opcode: 0x8F # Give Device Power Status
then:
- hdmi_cec.send:
destination: !lambda return source;
data: [0x90, 0x01] # Report Power Status: standby
- opcode: 0x8C # Give Device Vendor ID
then:
- lambda: |-
id(cec).send(0xf, {0x87, 0x00, 0x09, 0xb0});
- data: [0x44, 0x41] # User Control Pressed: Volume Up
then:
- logger.log: "HDMI-CEC Volume Up"
@@ -1,5 +0,0 @@
substitutions:
cec_pin: P6
packages:
hdmi_cec: !include common.yaml
@@ -1,5 +0,0 @@
substitutions:
cec_pin: GPIO4
packages:
hdmi_cec: !include common.yaml
@@ -1,5 +0,0 @@
substitutions:
cec_pin: GPIO14
packages:
hdmi_cec: !include common.yaml
@@ -1,5 +0,0 @@
substitutions:
cec_pin: GPIO14
packages:
hdmi_cec: !include common.yaml
+1 -7
View File
@@ -2,7 +2,6 @@
#include "esphome/components/logger/logger.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
/*
This special main.cpp replaces the default one.
@@ -23,12 +22,7 @@ void original_setup() {
// The loop clock and current component one test moves must not leak into the next, like CORE.reset() in Python
class ResetAppListener : public ::testing::EmptyTestEventListener {
void OnTestStart(const ::testing::TestInfo &) override {
esphome::LoopBlockingGuard reset{nullptr, nullptr, 0};
// Entities of the same kind share preference keys, so a value an earlier test saved would be restored
if (esphome::global_preferences != nullptr)
esphome::global_preferences->reset();
}
void OnTestStart(const ::testing::TestInfo &) override { esphome::LoopBlockingGuard reset{nullptr, nullptr, 0}; }
};
void setup() {
@@ -3,4 +3,5 @@ substitutions:
packages:
i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-idf.yaml
micro_wake_word: !include common.yaml
<<: !include common.yaml
@@ -6,5 +6,4 @@ packages:
micro_wake_word: !include common.yaml
micro_wake_word:
task_core: 1
prefer_internal_ram: true
@@ -81,7 +81,7 @@ struct Fixture {
MistralIR climate;
Fixture() {
// A second fixture in the same test would otherwise restore what the first saved
// Every fixture shares one preference key, so clear what an earlier test saved or setup restores it
if (global_preferences != nullptr)
global_preferences->reset();
// The test main does not construct App as generated code does
@@ -69,7 +69,7 @@ struct Fixture {
PDPioneerIR climate;
Fixture() {
// A second fixture in the same test would otherwise restore what the first saved
// Every fixture shares one preference key, so clear what an earlier test saved or setup restores it
if (global_preferences != nullptr)
global_preferences->reset();
// The test main does not construct App as generated code does
@@ -7,6 +7,7 @@
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/components/whirlpool/whirlpool.h"
#include "esphome/core/preferences.h"
namespace esphome::whirlpool::testing {
@@ -121,6 +122,9 @@ struct Fixture {
WhirlpoolClimate climate;
explicit Fixture(Model model) {
// Every fixture shares one preference key, so clear what an earlier test saved or setup restores it
if (global_preferences != nullptr)
global_preferences->reset();
this->climate.set_model(model);
this->climate.set_transmitter(&this->transmitter);
this->climate.setup();
@@ -1,12 +0,0 @@
wifi:
dpp:
channels: [1, 6, 11]
device_info: ESPHome
timeout: 30s
on_dpp_uri:
- logger.log:
format: "DPP URI %s"
args: [uri]
packages:
wifi: !include common.yaml
@@ -1,8 +0,0 @@
wifi:
ap:
ssid: WISP AP
password: wisp12345678
ap_mode: wisp
packages:
wifi: !include common.yaml

Some files were not shown because too many files have changed in this diff Show More