mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2890c82080 | ||
|
|
7abab6cca9 | ||
|
|
75dd7b1a94 | ||
|
|
4cb335934e | ||
|
|
736f6b53d2 | ||
|
|
a715bdae7d | ||
|
|
fe151ba802 | ||
|
|
5da9cbdefa | ||
|
|
016ee8a62d | ||
|
|
51a8a56335 |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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), ¤t) != 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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{}); }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user