Compare commits

..
334 changed files with 1843 additions and 13588 deletions
+11 -25
View File
@@ -81,8 +81,8 @@ jobs:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
packages: libsdl2-dev ccache
version: 1.1
determine-jobs:
name: Determine which jobs to run
@@ -415,8 +415,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
packages: libsdl2-dev ccache
version: 1.1
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -680,7 +680,7 @@ jobs:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino
@@ -693,16 +693,12 @@ jobs:
ignore_errors: false
- id: clang-tidy
name: Run script/clang-tidy for RP2
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
options: --environment rp2-tidy --grep USE_RP2
pio_cache_key: tidyrp2
- id: clang-tidy
name: Run script/clang-tidy for host
options: --environment host-tidy --grep USE_HOST
pio_cache_key: tidyhost
- id: clang-tidy
name: Run script/clang-tidy for LibreTiny
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
pio_cache_key: tidylibretiny
steps:
@@ -711,16 +707,6 @@ jobs:
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
# exactly.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Restore Python
id: restore-python
@@ -1003,14 +989,14 @@ jobs:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 S3
# yamllint disable-line rule:line-length
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
tidy_environment: esp32s3-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
tidy_environment: esp32p4-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
@@ -1116,8 +1102,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
packages: libsdl2-dev ccache
version: 1.1
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
-5
View File
@@ -183,7 +183,6 @@ esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81
esphome/components/es8311/* @kahrendt @kroimon
esphome/components/es8388/* @P4uLT
esphome/components/es9038q2m_katana/* @GioMez
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
esphome/components/esp32_ble_client/* @bdraco @jesserockz
@@ -220,7 +219,6 @@ esphome/components/friedrich/* @rwrozelle
esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/fusb302b/* @remcom
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
@@ -242,7 +240,6 @@ esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/select/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
@@ -257,7 +254,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
@@ -389,7 +385,6 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/midea_portasplit/* @Fexiven
esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_rgb/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow
+3 -2
View File
@@ -338,8 +338,9 @@ class ApplyField:
Double a literal brace in a template. ``conf_key`` may be a path into nested sections.
``type_`` may be a C++ type string using ``{parent}`` when the type is only known per
instance. ``const_fn(config, value)`` renders a constant's argument text when ``cg.safe_exp``
is not the right spelling (unit conversion belongs in the validator); a lambda or an id
bypasses it, so the target must also take a plain ``type_``. An absent key emits nothing.
is not the right spelling, or the value depends on another entity's config, such as the unit
a climate reports in; a lambda or an id bypasses it, so the target must also take a plain
``type_``. An absent key emits nothing.
"""
conf_key: str | tuple[str, ...]
+33
View File
@@ -251,6 +251,9 @@ def _validate_supports_response(value: Any) -> str:
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
VARIABLE_SCHEMA = cv.Schema(
{
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
@@ -481,6 +484,29 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
return strings
def _action_strings_size(strings: list[str | None]) -> int:
"""Bytes needed to copy every string out of flash, each with its terminator."""
return sum(
len(string.encode("utf-8")) + 1 for string in strings if string is not None
)
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
if not CORE.is_esp8266:
return config
actions = config.get(CONF_ACTIONS, [])
has_metadata = _has_action_metadata(actions)
for conf in actions:
size = _action_strings_size(_action_strings(conf, has_metadata))
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
raise cv.Invalid(
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
f"description and example text; ESP8266 allows at most "
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
)
return config
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
if (
@@ -505,6 +531,7 @@ def _validate_wizard(config: ConfigType) -> ConfigType:
FINAL_VALIDATE_SCHEMA = cv.All(
_validate_esp8266_action_strings,
_validate_outgoing_host_ipv6,
_validate_wizard,
)
@@ -560,6 +587,7 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
scratch_size = 0
if actions:
# Metadata is compiled in for every action once any action declares it, because the
# string table layout is fixed by the define rather than per action
@@ -611,6 +639,8 @@ async def to_code(config: ConfigType) -> None:
func_args.append((native, name))
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
# Key is hashed here because the name is not readable at runtime on ESP8266
@@ -637,6 +667,9 @@ async def to_code(config: ConfigType) -> None:
cg.add(auto.add_action(unregister_action))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
await wizard.to_code(wizard_config)
+10 -16
View File
@@ -403,9 +403,6 @@ message WizardCapabilities {
message SendspinCapabilities {
// Bitmask of the Sendspin features this device supports
uint32 feature_flags = 1;
// The Sendspin client_id: base64url of the client's static public key, 43 characters.
// Empty until the Sendspin client has started once since boot.
string client_id = 2 [(max_data_length) = 43];
}
message DeviceCapabilitiesResponse {
@@ -1029,8 +1026,8 @@ message SubscribeHomeassistantServicesRequest {
}
message HomeassistantServiceMap {
string key = 1 [(progmem) = true];
string value = 2 [(progmem) = true];
string key = 1;
string value = 2;
}
message HomeassistantActionRequest {
@@ -1039,7 +1036,7 @@ message HomeassistantActionRequest {
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
string service = 1 [(progmem) = true];
string service = 1;
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
@@ -1160,21 +1157,21 @@ enum SupportsResponseType {
}
message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1 [(progmem) = true];
string name = 1;
ServiceArgType type = 2;
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ListEntitiesServicesResponse {
option (id) = 41;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1 [(progmem) = true];
string name = 1;
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
@@ -3046,15 +3043,12 @@ enum SerialProxyIdentityFlag {
// IDs below are empty
}
// The descriptor fields of a USB device as seen by the host stack, and the port's index
// among those a vendor driver exposes on the interface
// The descriptor fields of a USB device as seen by the host stack
message UsbDeviceDescriptor {
uint32 vendor_id = 1;
uint32 product_id = 2;
uint32 bcd_device = 3;
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
uint32 interface_class = 5; // bInterfaceClass of that interface
uint32 port_number = 6;
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to
}
// The identity of the device behind a port. Identifies one physical device among others of
-13
View File
@@ -1,7 +1,4 @@
#include "api_connection.h"
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
#include "esphome/components/logger/logger.h"
#endif
#ifdef USE_API
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
#ifdef USE_API_NOISE
@@ -454,15 +451,6 @@ bool APIConnection::send_disconnect_response_() {
DisconnectResponse resp;
return this->send_message(resp);
}
void APIConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
logger::log_crash_to_client();
#endif
}
void APIConnection::on_disconnect_response() {
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
@@ -2131,7 +2119,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);
}
+24 -1
View File
@@ -15,6 +15,15 @@
#include "subscribe_state.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "esphome/components/esp32/crash_handler.h"
#endif
#ifdef USE_RP2_CRASH_HANDLER
#include "esphome/components/rp2/crash_handler.h"
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
#include "esphome/components/esp8266/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
@@ -289,7 +298,21 @@ class APIConnection final : public APIServerConnectionBase {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
}
void on_subscribe_logs_request(const SubscribeLogsRequest &msg);
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
esp32::crash_handler_clear();
#endif
#ifdef USE_RP2_CRASH_HANDLER
rp2::crash_handler_log();
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
esp8266::crash_handler_log();
#endif
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
#endif
+1 -1
View File
@@ -123,7 +123,7 @@ extend google.protobuf.FieldOptions {
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
optional bool track_presence = 50020 [default=false];
// progmem: The string field, or pointer_to_buffer bytes field, points into flash (PROGMEM).
// progmem: The pointer_to_buffer bytes field points into flash (PROGMEM).
// The generated encode and dump copy it with progmem_memcpy, as ESP8266
// cannot read flash with ordinary loads.
optional bool progmem = 50021 [default=false];
+8 -14
View File
@@ -310,14 +310,12 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.client_id);
return pos;
}
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.feature_flags);
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
return size;
}
#endif
@@ -1142,8 +1140,8 @@ uint32_t NoiseEncryptionSetKeyResponse::calc_size_msg(const void *self) {
#ifdef USE_API_HOMEASSISTANT_SERVICES
uint8_t *HomeassistantServiceMap::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const HomeassistantServiceMap *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
return pos;
}
uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
@@ -1155,7 +1153,7 @@ uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
}
uint8_t *HomeassistantActionRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const HomeassistantActionRequest *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
for (auto &it : msg.data) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 2, it);
}
@@ -1326,13 +1324,13 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
uint8_t *ListEntitiesServicesArgument::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const ListEntitiesServicesArgument *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(msg.type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
#endif
return pos;
}
@@ -1352,14 +1350,14 @@ uint32_t ListEntitiesServicesArgument::calc_size_msg(const void *self) {
uint8_t *ListEntitiesServicesResponse::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const ListEntitiesServicesResponse *>(self);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, msg.key);
for (auto &it : msg.args) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 3, it);
}
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
#endif
return pos;
}
@@ -4177,8 +4175,6 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
return pos;
}
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
@@ -4188,8 +4184,6 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.product_id);
size += ProtoSize::calc_uint32(1, msg.bcd_device);
size += ProtoSize::calc_uint32(1, msg.interface_number);
size += ProtoSize::calc_uint32(1, msg.interface_class);
size += ProtoSize::calc_uint32(1, msg.port_number);
return size;
}
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
+8 -11
View File
@@ -750,7 +750,6 @@ class WizardCapabilities final : public ProtoMessage {
class SendspinCapabilities final : public ProtoMessage {
public:
uint32_t feature_flags{0};
StringRef client_id{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -1389,8 +1388,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
#ifdef USE_API_HOMEASSISTANT_SERVICES
class HomeassistantServiceMap final : public ProtoMessage {
public:
ProgmemStringRef key{};
ProgmemStringRef value{};
StringRef key{nullptr, 0}; // null until set, encode only
StringRef value{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -1410,7 +1409,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); }
#endif
ProgmemStringRef service{};
StringRef service{nullptr, 0}; // null until set, encode only
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
@@ -1577,13 +1576,13 @@ class GetTimeResponse final : public ProtoDecodableMessage {
#ifdef USE_API_USER_DEFINED_ACTIONS
class ListEntitiesServicesArgument final : public ProtoMessage {
public:
ProgmemStringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProgmemStringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProgmemStringRef example{};
StringRef example{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1604,12 +1603,12 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
ProgmemStringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProgmemStringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -4084,8 +4083,6 @@ class UsbDeviceDescriptor final : public ProtoMessage {
uint32_t product_id{0};
uint32_t bcd_device{0};
uint32_t interface_number{0};
uint32_t interface_class{0};
uint32_t port_number{0};
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);
+10 -21
View File
@@ -12,9 +12,8 @@ namespace esphome::api {
#ifdef USE_ESP8266
// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site
void DumpBuffer::append_p_esp8266(const char *str) { this->append_p_esp8266(str, strlen_P(str)); }
void DumpBuffer::append_p_esp8266(const char *str, size_t len) {
void DumpBuffer::append_p_esp8266(const char *str) {
size_t len = strlen_P(str);
size_t space = CAPACITY - 1 - pos_;
if (len > space)
len = space;
@@ -135,13 +134,6 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
progmem_memcpy(data_buf, data, len);
dump_bytes_field(out, field_name, data_buf, len, indent);
}
// Helper for string fields in flash
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append_p(value.progmem_ptr(), value.size()).append("'\n");
}
#pragma GCC diagnostic pop
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
@@ -1127,7 +1119,6 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
return out.c_str();
}
#endif
@@ -1520,13 +1511,13 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
#ifdef USE_API_HOMEASSISTANT_SERVICES
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap"));
dump_progmem_string_field(out, ESPHOME_PSTR("key"), this->key);
dump_progmem_string_field(out, ESPHOME_PSTR("value"), this->value);
dump_field(out, ESPHOME_PSTR("key"), this->key);
dump_field(out, ESPHOME_PSTR("value"), this->value);
return out.c_str();
}
const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest"));
dump_progmem_string_field(out, ESPHOME_PSTR("service"), this->service);
dump_field(out, ESPHOME_PSTR("service"), this->service);
for (const auto &it : this->data) {
out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": ");
it.dump_to(out);
@@ -1621,19 +1612,19 @@ const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
#ifdef USE_API_USER_DEFINED_ACTIONS
const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_progmem_string_field(out, ESPHOME_PSTR("example"), this->example);
dump_field(out, ESPHOME_PSTR("example"), this->example);
#endif
return out.c_str();
}
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse"));
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("key"), this->key);
for (const auto &it : this->args) {
out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": ");
@@ -1642,7 +1633,7 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
}
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
return out.c_str();
}
@@ -2951,8 +2942,6 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
return out.c_str();
}
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
+1 -1
View File
@@ -547,7 +547,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.progmem_ptr(),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected"));
}
+8 -8
View File
@@ -247,7 +247,7 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(service_name));
resp.service = StringRef(service_name);
global_api_server->send_homeassistant_action(resp);
}
@@ -267,12 +267,12 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(service_name));
resp.service = StringRef(service_name);
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = ProgmemStringRef(StringRef(it.first));
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
}
global_api_server->send_homeassistant_action(resp);
}
@@ -289,7 +289,7 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &event_name) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(event_name));
resp.service = StringRef(event_name);
resp.is_event = true;
global_api_server->send_homeassistant_action(resp);
}
@@ -309,13 +309,13 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = ProgmemStringRef(StringRef(service_name));
resp.service = StringRef(service_name);
resp.is_event = true;
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = ProgmemStringRef(StringRef(it.first));
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
}
global_api_server->send_homeassistant_action(resp);
}
+28 -4
View File
@@ -116,18 +116,42 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
const Field *fields = this->fields_;
const size_t total = 1 + this->data_count_ + this->data_template_count_ + this->variables_count_;
// Lambda results must live until the send; the constant strings are encoded from flash where they are
// Lambda results, and on ESP8266 the RAM copies of the flash strings, must live until the send
size_t lambda_count = 0;
for (size_t i = 0; i < total; i++)
#ifdef USE_ESP8266
size_t flash_len = 0;
#endif
for (size_t i = 0; i < total; i++) {
lambda_count += fields[i].fn != nullptr;
#ifdef USE_ESP8266
if (fields[i].fn == nullptr)
flash_len += ESPHOME_strlen_P(fields[i].value);
if (fields[i].key != nullptr)
flash_len += ESPHOME_strlen_P(fields[i].key);
#endif
}
FixedVector<std::string> results;
results.init(lambda_count);
auto string_ref = [](const char *str) { return ProgmemStringRef(str, ESPHOME_strlen_P(str)); };
#ifdef USE_ESP8266
SmallBufferWithHeapFallback<128, char> flash_copy(flash_len);
char *cursor = flash_copy.get();
#endif
auto string_ref = [&](const char *str) {
#ifdef USE_ESP8266
size_t len = ESPHOME_strlen_P(str);
memcpy_P(cursor, str, len);
StringRef ref(cursor, len);
cursor += len;
return ref;
#else
return StringRef(str);
#endif
};
auto value_ref = [&](const Field &field) {
if (field.fn == nullptr)
return string_ref(field.value);
results.push_back(field.fn(x...));
return ProgmemStringRef(StringRef(results.back()));
return StringRef(results.back());
};
auto fill = [&](FixedVector<HomeassistantServiceMap> &dest, uint8_t count) {
dest.init(count);
+2 -1
View File
@@ -99,7 +99,8 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
UserActionScratch scratch;
auto resp = service->encode_list_service_response(scratch);
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
-34
View File
@@ -418,18 +418,6 @@ class ProtoEncode {
std::memcpy(pos + 2, ref.c_str(), ref.size());
return pos + 2 + ref.size();
}
/// encode_short_string_force for a string that may be in flash (PROGMEM).
[[nodiscard]] static inline uint8_t *encode_short_progmem_string_force(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, const ProgmemStringRef &ref) {
#ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_progmem_string_force: string exceeds max_data_length < 128");
#endif
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size());
progmem_memcpy(pos + 2, ref.progmem_ptr(), ref.size());
return pos + 2 + ref.size();
}
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
@@ -493,17 +481,6 @@ class ProtoEncode {
progmem_memcpy(pos, data, len);
return pos + len;
}
/// encode_progmem_bytes for a string field that may be in flash.
[[nodiscard]] static inline uint8_t *encode_progmem_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const ProgmemStringRef &ref) {
return encode_progmem_bytes(pos PROTO_ENCODE_DEBUG_ARG, field_id,
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
}
[[nodiscard]] static inline uint8_t *encode_progmem_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const ProgmemStringRef &ref) {
return encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, field_id,
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
}
[[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
@@ -703,20 +680,9 @@ class DumpBuffer {
return *this;
}
/// Append len bytes of a PROGMEM string
DumpBuffer &append_p(const char *str, size_t len) {
#ifdef USE_ESP8266
append_p_esp8266(str, len);
#else
append_impl_(str, len);
#endif
return *this;
}
#ifdef USE_ESP8266
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
void append_p_esp8266(const char *str);
void append_p_esp8266(const char *str, size_t len);
#endif
const char *c_str() const { return buf_; }
+20 -9
View File
@@ -1,36 +1,47 @@
#include "user_services.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
ProgmemStringRef UserServiceStatic::str_(size_t idx) const {
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
const char *s = progmem_read_ptr(&this->strings_[idx]);
if (s == nullptr)
return {};
return ProgmemStringRef(s, ESPHOME_strlen_P(s));
#ifdef USE_ESP8266
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
if (scratch.empty())
return {};
size_t len = strnlen_P(s, scratch.size() - 1);
progmem_memcpy(scratch.data(), s, len);
scratch[len] = '\0';
StringRef ref(scratch.data(), len);
scratch = scratch.subspan(len + 1);
return ref;
#else
return StringRef(s);
#endif
}
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
std::span<const enums::ServiceArgType> arg_types) const {
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
ListEntitiesServicesResponse msg;
msg.name = this->str_(0);
msg.name = this->str_(0, scratch);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
msg.description = this->str_(1);
msg.description = this->str_(1, scratch);
#endif
msg.args.init(arg_types.size());
for (size_t i = 0; i < arg_types.size(); i++) {
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = this->str_(base);
arg.name = this->str_(base, scratch);
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
arg.description = this->str_(base + 1);
arg.example = this->str_(base + 2);
arg.description = this->str_(base + 1, scratch);
arg.example = this->str_(base + 2, scratch);
#endif
}
return msg;
+21 -10
View File
@@ -20,8 +20,9 @@ class APIServer;
class UserServiceDescriptor {
public:
/// Build the list-entities message; its strings may point into flash (PROGMEM).
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -36,6 +37,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type();
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
#ifdef USE_ESP8266
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
#else
using UserActionScratch = std::array<char, 0>;
#endif
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
@@ -55,9 +63,12 @@ class UserServiceStatic : public UserServiceDescriptor {
: strings_(strings), key_(key), supports_response_(supports_response) {}
protected:
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types) const;
/// Table entry `idx` as it sits in flash; nullptr gives an empty string.
ProgmemStringRef str_(size_t idx) const;
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
std::span<char> scratch) const;
/// Reference table entry `idx`; nullptr gives an empty StringRef.
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
/// is advanced past the copy.
StringRef str_(size_t idx, std::span<char> &scratch) const;
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
uint32_t key_;
@@ -68,9 +79,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
public:
using UserServiceStatic::UserServiceStatic;
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
return this->encode_list_service_response_(arg_types);
return this->encode_list_service_response_(arg_types, scratch);
}
bool execute_service(const ExecuteServiceRequest &req) override {
@@ -114,9 +125,9 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
ListEntitiesServicesResponse msg;
msg.name = ProgmemStringRef(StringRef(this->name_));
msg.name = StringRef(this->name_);
msg.key = this->key_;
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
@@ -124,7 +135,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = ProgmemStringRef(StringRef(this->arg_names_[i]));
arg.name = StringRef(this->arg_names_[i]);
}
return msg;
}
+1 -1
View File
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
data = _get_data()
@@ -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 -3
View File
@@ -100,8 +100,7 @@ void BM8563::get_date_(ESPTime &time) {
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
// The chip counts weekdays 0-6 from Sunday (not BCD), ESPTime counts 1-7
time.day_of_week = (buf[1] & 0x07) + 1;
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
@@ -116,7 +115,7 @@ void BM8563::get_date_(ESPTime &time) {
void BM8563::set_date_(const ESPTime &time) {
uint8_t buf[4] = {
this->byte_to_bcd2_(time.day_of_month),
static_cast<uint8_t>(time.day_of_week - 1),
this->byte_to_bcd2_(time.day_of_week),
this->byte_to_bcd2_(time.month),
this->byte_to_bcd2_(time.year % 100),
};
+7 -11
View File
@@ -2,7 +2,6 @@
#include "bmi270_config.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::bmi270 {
@@ -12,9 +11,6 @@ using namespace bmm150; // NOLINT - sole consumer of the BMM150 aux-device defi
ESPHOME_LOG_TAG(TAG, "bmi270");
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±4g", "±8g", "±16g");
PROGMEM_STRING_TABLE(GyroRangeStrings, "±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s");
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
static constexpr size_t MAX_I2C_BUFFER_SIZE = 32;
#else
@@ -277,10 +273,11 @@ void BMI270Component::dump_config() {
return;
}
ESP_LOGCONFIG(TAG, " Accel range : %s",
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_, AccelRangeStrings::LAST_INDEX)));
ESP_LOGCONFIG(TAG, " Gyro range : %s",
LOG_STR_ARG(GyroRangeStrings::get_log_str(this->gyro_range_, GyroRangeStrings::LAST_INDEX)));
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
#ifdef USE_BMI270_BMM150
if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) {
ESP_LOGCONFIG(TAG, " Aux device : BMM150 magnetometer (%s)",
@@ -318,8 +315,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
}
// Scale factor: LSB/g depends on range
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
// In PROGMEM; each read is a 32-bit load, which is safe from flash on ESP8266
alignas(4) static constexpr float ACCEL_SCALE[] PROGMEM = {
static constexpr float ACCEL_SCALE[] = {
2.0f / 32768.0f,
4.0f / 32768.0f,
8.0f / 32768.0f,
@@ -333,7 +329,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
// Gyroscope: registers 0x12–0x17 (6 bytes)
// Scale: full-scale range / 2^15
alignas(4) static constexpr float GYRO_SCALE[] PROGMEM = {
static constexpr float GYRO_SCALE[] = {
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
};
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
+61 -20
View File
@@ -46,12 +46,15 @@ from esphome.const import (
CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC,
CONF_TARGET_TEMPERATURE_STATE_TOPIC,
CONF_TEMPERATURE_STEP,
CONF_UNIT_OF_MEASUREMENT,
CONF_VISUAL,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
entity_temperature_unit,
entity_temperature_unit_of_id,
queue_entity_register,
setup_entity,
)
@@ -137,13 +140,10 @@ CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity"
visual_temperature = cv.float_with_unit("visual_temperature", "(°|(° ?)?[CKF])?")
VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
{
cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature,
cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature,
cv.Required(CONF_TARGET_TEMPERATURE): cv.temperature_with_unit,
cv.Required(CONF_CURRENT_TEMPERATURE): cv.temperature_with_unit,
}
)
@@ -154,13 +154,30 @@ def visual_temperature_step(value: Any) -> ConfigType:
return VISUAL_TEMPERATURE_STEP_SCHEMA(value)
# Otherwise, use the single value for both properties
value = visual_temperature(value)
return VISUAL_TEMPERATURE_STEP_SCHEMA(
{
CONF_TARGET_TEMPERATURE: value,
CONF_CURRENT_TEMPERATURE: value,
}
)
value = cv.temperature_with_unit(value)
return {CONF_TARGET_TEMPERATURE: value, CONF_CURRENT_TEMPERATURE: value}
def temperature_in_entity_unit(
config: ConfigType, value: tuple[float, str | None]
) -> str:
"""Render a static action temperature in the unit of the climate the action targets."""
unit = entity_temperature_unit_of_id(config[CONF_ID])
return str(cg.safe_exp(cv.convert_temperature(*value, unit)))
def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType:
"""Convert the visual temperatures into the entity's unit."""
unit = entity_temperature_unit(config)
visual = config[CONF_VISUAL]
for key in (CONF_MIN_TEMPERATURE, CONF_MAX_TEMPERATURE):
if isinstance(value := visual.get(key), tuple):
visual[key] = cv.convert_temperature(*value, unit)
if (step := visual.get(CONF_TEMPERATURE_STEP)) is not None:
for key in (CONF_TARGET_TEMPERATURE, CONF_CURRENT_TEMPERATURE):
if isinstance(value := step[key], tuple):
step[key] = cv.convert_temperature(*value, unit, delta=True)
return config
_CLIMATE_SCHEMA = (
@@ -171,8 +188,8 @@ _CLIMATE_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature_with_unit,
cv.Optional(CONF_TEMPERATURE_STEP): visual_temperature_step,
cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int,
cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int,
@@ -249,6 +266,7 @@ _CLIMATE_SCHEMA = (
_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate"))
_CLIMATE_SCHEMA.add_extra(_visual_temperatures_in_entity_unit)
def climate_schema(
@@ -256,10 +274,18 @@ def climate_schema(
*,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
temperature_unit: bool = False,
) -> cv.Schema:
"""Build the schema of a climate platform.
``temperature_unit`` adds ``unit_of_measurement`` for platforms whose temperatures are all
in the unit the user picks; the class then needs ``set_temperature_unit(TemperatureUnit)``.
"""
schema = {
cv.GenerateID(): cv.declare_id(class_),
}
if temperature_unit:
schema[cv.Optional(CONF_UNIT_OF_MEASUREMENT)] = cv.temperature_unit
for key, default, validator in [
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
@@ -460,9 +486,13 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(Climate),
cv.Optional(CONF_MODE): cv.templatable(validate_climate_mode),
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature_with_unit),
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(
cv.temperature_with_unit
),
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(
cv.temperature_with_unit
),
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
@@ -482,12 +512,23 @@ automation.register_apply_action(
"climate.control",
CLIMATE_CONTROL_ACTION_SCHEMA,
automation.ApplyField(CONF_MODE, "set_mode", ClimateMode),
automation.ApplyField(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
automation.ApplyField(
CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_
CONF_TARGET_TEMPERATURE,
"set_target_temperature",
cg.float_,
temperature_in_entity_unit,
),
automation.ApplyField(
CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_
CONF_TARGET_TEMPERATURE_LOW,
"set_target_temperature_low",
cg.float_,
temperature_in_entity_unit,
),
automation.ApplyField(
CONF_TARGET_TEMPERATURE_HIGH,
"set_target_temperature_high",
cg.float_,
temperature_in_entity_unit,
),
automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
automation.ApplyField(CONF_FAN_MODE, "set_fan_mode", ClimateFanMode),
+4 -22
View File
@@ -8,9 +8,6 @@ namespace esphome::cse7766 {
ESPHOME_LOG_TAG(TAG, "cse7766");
// The chip only sends the time since the last pulse once a cycle is longer than 1 s (in us)
static constexpr uint32_t MIN_HELD_POWER_CYCLE = 1000000;
void CSE7766Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) {
@@ -157,33 +154,18 @@ void CSE7766Component::parse_data_() {
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
have_power = true;
this->last_power_cycle_ = 0;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
} else if (have_power) {
power = power_coeff / float(power_cycle);
this->last_power_cycle_ = power_cycle;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
} else if (this->last_power_cycle_ > MIN_HELD_POWER_CYCLE && power_cycle <= 2 * this->last_power_cycle_) {
// No new measurement in this frame. With cycles over 1 s the register holds the last
// cycle or the time elapsed since the last pulse. While that time is within twice the
// last cycle, the next pulse is not overdue, so the last measurement still applies.
// Shorter cycles never get here in normal operation and keep the previous behaviour.
have_power = true;
power = power_coeff / float(this->last_power_cycle_);
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
} else {
// The next pulse is overdue: the load has dropped, report 0 W. A load that drops to less
// than half also reads 0 W here until its first new pulse arrives.
this->last_power_cycle_ = 0;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
} else if (this->power_sensor_ != nullptr) {
// No valid power measurement from chip - publish 0W to avoid stale readings
// This typically happens when current is below the measurable threshold (~50mA)
this->power_sensor_->publish_state(0.0f);
}
float current = 0.0f;
+1 -3
View File
@@ -36,7 +36,6 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
uint8_t raw_data_index_{0};
uint16_t cf_pulses_last_{0};
uint32_t last_transmission_{0};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
@@ -46,8 +45,7 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
uint32_t cf_pulses_total_{0};
// Power cycle of the last frame that carried a new power measurement, 0 if none
uint32_t last_power_cycle_{0};
uint16_t cf_pulses_last_{0};
};
} // namespace esphome::cse7766
-36
View File
@@ -250,42 +250,6 @@ def add_metadata(
)
KEY_EXTERNAL_WRITERS = "external_writers"
def _get_external_writer_list() -> list[tuple[ID, str]]:
"""Get the raw external writer list. Each entry is (display id, owner)."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
def register_external_writer(display_id: ID, owner: str) -> None:
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
resolves it in place before final validation.
"""
_get_external_writer_list().append((display_id, owner))
def get_external_writers(display_id: ID) -> list[str]:
"""Get the owners registered for a display. Only valid once IDs are resolved."""
return [
owner
for id_, owner in _get_external_writer_list()
if id_.id is not None and id_.id == display_id.id
]
def has_external_writer(display_id: ID) -> bool:
"""Tell a driver's final validation that something else draws into the display.
LVGL anywhere in the config also counts, as the drivers assumed before.
"""
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
+29 -55
View File
@@ -5,7 +5,6 @@
#include "display_color_utils.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::display {
@@ -53,62 +52,37 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
this->line(x1, y1, x2, y2, color);
}
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
// -Os stops inlining it once there are two call sites, which costs a call and
// spills per pixel. Without PSRAM there is one call site and it changes
// nothing, and draw_pixel_at() stays a virtual call either way.
template<ColorBitness BITNESS>
static void __attribute__((flatten))
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
bool big_endian, int x_offset, int y_offset, int x_pad) {
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
// BITNESS is a constant here, so to_color()'s three per pixel divisions
// become a multiply and shift and its bitness switch folds.
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
display->draw_pixel_at(
x + x_start, y + y_start,
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
};
// The source and frame buffers are row-major, so walking rows keeps the read
// and the write sequential. When the display swaps the axes before writing,
// walking columns keeps the write sequential instead; with the frame buffer
// in PSRAM a strided write costs more than a strided read.
#ifdef USE_PSRAM
if (display->pixel_axes_swapped()) {
size_t column_idx = y_offset * line_stride + x_offset;
for (int x = 0; x != w; x++, column_idx++) {
size_t source_idx = column_idx;
for (int y = 0; y != h; y++, source_idx += line_stride) {
draw_source_pixel(x, y, source_idx);
}
}
return;
}
#endif
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
for (int x = 0; x != w; x++, source_idx++) {
draw_source_pixel(x, y, source_idx);
}
}
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
switch (bitness) {
case COLOR_BITNESS_565:
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
case COLOR_BITNESS_888:
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
default:
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
uint32_t color_value;
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
size_t source_idx_mod;
for (int x = 0; x != w; x++, source_idx++) {
switch (bitness) {
default:
color_value = ptr[source_idx];
break;
case COLOR_BITNESS_565:
source_idx_mod = source_idx * 2;
if (big_endian) {
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
} else {
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
}
break;
case COLOR_BITNESS_888:
source_idx_mod = source_idx * 3;
if (big_endian) {
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
} else {
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
}
break;
}
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
}
}
}
-11
View File
@@ -139,11 +139,6 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
/// Whether a rotation swaps the x and y axes.
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
}
const int EDGES_TRIGON = 3;
const int EDGES_TRIANGLE = 3;
const int EDGES_TETRAGON = 4;
@@ -722,12 +717,6 @@ class Display : public PollingComponent {
virtual DisplayRotation get_rotation() const { return this->rotation_; }
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
/// neighbours along x land a row apart in the frame buffer. Not the same as
/// get_width()/get_height() swapping: a display that rotates in hardware
/// reports swapped dimensions but writes unswapped pixels.
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
@@ -2,7 +2,6 @@
#include <array>
#include <cstdint>
#include "esphome/core/color.h"
#include "esphome/core/helpers.h"
namespace esphome::display {
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
@@ -11,27 +10,6 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
class ColorUtil {
public:
/// Reads one packed pixel of the given bitness from a source buffer into the
/// value to_color() takes. BITNESS is a template argument so the byte layout
/// folds; pass it on to to_color() as a constant too, and its channel scaling
/// divides by a constant, which the compiler turns into a multiply and shift.
template<ColorBitness BITNESS>
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
if constexpr (BITNESS == COLOR_BITNESS_565) {
const size_t idx = index * 2;
if (big_endian)
return (ptr[idx] << 8) + ptr[idx + 1];
return ptr[idx] + (ptr[idx + 1] << 8);
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
const size_t idx = index * 3;
if (big_endian)
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
} else {
return ptr[index];
}
}
static Color to_color(uint32_t colorcode, ColorOrder color_order,
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
uint8_t first_color, second_color, third_color;
@@ -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 {
+6 -1
View File
@@ -40,6 +40,7 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from . import models
@@ -197,11 +198,15 @@ def _final_validate(config) -> None:
"epaper_spi", require_miso=False, require_mosi=True
)(config)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if display.has_external_writer(config[CONF_ID]):
if LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
@@ -106,7 +106,6 @@ class EPaperBase : public Display,
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
void draw_pixel_at(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
@@ -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
+1 -2
View File
@@ -243,7 +243,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, hdmi_cec, opentherm, Arduino BLE libs
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs
"esp_driver_i2c", # I2C driver - re-included by i2c; esp32-camera pulls it back itself
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_ledc", # LEDC PWM driver - re-included by ledc; esp32-camera pulls it back itself
@@ -2920,7 +2920,6 @@ async def _finalize_arduino_aware_flags():
if not arduino_linked:
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
cg.add_define("USE_ESP32_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
elif not CORE.using_arduino:
cg.add_build_flag("-DUSE_ARDUINO")
+2 -3
View File
@@ -161,7 +161,7 @@ static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-c
namespace esphome::esp32 {
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG);
ESPHOME_LOG_TAG(TAG, "esp32.crash");
// RAM copy of the build timestamp. The generated constant lives in flash,
// which the panic handler must not read (cache may be disabled during
@@ -175,8 +175,7 @@ static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME)
// Validate the NOINIT record. Runs on every has_data() call; re-running is
// harmless and the magic is left alone so the record survives an OTA
// rollback reboot, crash_handler_clear() drops it once a log client has it
// (logger::log_crash_to_client(), for the API and MQTT).
// rollback reboot, crash_handler_clear() drops it once an API client has it.
static void read_crash_data() {
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
s_crash_data_valid = true;
-3
View File
@@ -4,9 +4,6 @@
namespace esphome::esp32 {
/// The tag the crash record is logged under, so a client can check the level in force for it
inline constexpr const char *CRASH_LOG_TAG = "esp32.crash";
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
-1
View File
@@ -131,7 +131,6 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
void ESP32Preferences::open() {
// Runs from app_main() before the logger is initialized; any logging here
// must be deferred. See s_open_err and make_preference() below.
// Components that need NVS check nvs_handle instead of initializing it again.
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
+12 -4
View File
@@ -5,7 +5,6 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#ifndef CONFIG_BT_CONTROLLER_DISABLED
#include <esp_bt.h>
@@ -29,6 +28,7 @@ extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#ifdef USE_ARDUINO
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
@@ -77,9 +77,8 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
void ESP32BLE::setup() {
global_ble = this;
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
if (global_preferences->nvs_handle == 0) {
ESP_LOGE(TAG, "NVS unavailable");
if (!ble_pre_setup_()) {
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
this->mark_failed();
return;
}
@@ -186,6 +185,15 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
}
#endif
bool ESP32BLE::ble_pre_setup_() {
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
return false;
}
return true;
}
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr)
+1
View File
@@ -185,6 +185,7 @@ class ESP32BLE final : public Component {
}
this->ble_events_.get_and_reset_dropped_count();
}
bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
#endif
@@ -10,6 +10,7 @@
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cstring>
#include "esphome/core/hal.h"
@@ -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;
@@ -7,6 +7,7 @@
#ifdef USE_ESP32
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
#ifndef CONFIG_BT_CONTROLLER_DISABLED
@@ -16,6 +16,7 @@
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cinttypes>
#ifdef USE_OTA
-1
View File
@@ -47,7 +47,6 @@ CanMode = esp32_can_ns.enum("CanMode")
CAN_MODES = {
"NORMAL": CanMode.CAN_MODE_NORMAL,
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
}
# Currently the driver only supports a subset of the bit rates defined in canbus
+2 -5
View File
@@ -79,13 +79,10 @@ bool ESP32Can::setup_internal() {
}
// Select TWAI mode based on configuration
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
twai_mode = TWAI_MODE_LISTEN_ONLY;
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
} else if (this->mode_ == CAN_MODE_NO_ACK) {
twai_mode = TWAI_MODE_NO_ACK;
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
}
twai_general_config_t g_config =
-1
View File
@@ -12,7 +12,6 @@ namespace esphome::esp32_can {
enum CanMode : uint8_t {
CAN_MODE_NORMAL = 0,
CAN_MODE_LISTEN_ONLY = 1,
CAN_MODE_NO_ACK = 2,
};
class ESP32Can final : public canbus::Canbus {
@@ -42,7 +42,6 @@ extern "C" {
}
#include "esp_now_hosted_rpc.h"
#include "esphome/core/log.h"
namespace {
@@ -53,8 +52,6 @@ ESPHOME_LOG_TAG(TAG, "esp_now_hosted");
// single esp-hosted RPC RX thread. Serializing requests keeps the shared
// response slot race-free; a sequence number stops a late/stale response from
// being mistaken for ours.
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables): the shim is
// C-ABI state shared between the main loop and the esp-hosted RX thread.
SemaphoreHandle_t g_req_mutex = nullptr;
SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands
bool g_setup_done = false; // set only after setup fully succeeds
@@ -88,7 +85,6 @@ constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20;
uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6];
size_t g_peer_count = 0;
portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
// Caller must hold g_peer_lock.
int peer_cache_find_locked(const uint8_t *mac) {
@@ -235,14 +231,12 @@ esp_err_t ensure_setup() {
g_resp_sem = xSemaphoreCreateBinary();
if (g_req_mutex == nullptr || g_resp_sem == nullptr)
return ESP_ERR_NO_MEM;
esp_err_t err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr);
if (err != ESP_OK)
esp_err_t err;
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK)
return err;
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr);
if (err != ESP_OK)
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK)
return err;
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr);
if (err != ESP_OK)
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK)
return err;
g_setup_done = true;
return ESP_OK;
@@ -428,11 +422,10 @@ esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len
uint8_t hdr[sizeof(esp_now_hosted_send_req_t)];
auto *s = reinterpret_cast<esp_now_hosted_send_req_t *>(hdr);
s->has_addr = peer_addr != nullptr ? 1 : 0;
if (peer_addr != nullptr) {
if (peer_addr != nullptr)
memcpy(s->peer_addr, peer_addr, 6);
} else {
else
memset(s->peer_addr, 0, 6);
}
s->data_len = static_cast<uint16_t>(len);
// Fire-and-forget (wait=false): native esp_now_send returns once the frame is
// queued, with the real TX result delivered later through the send callback.
-12
View File
@@ -53,14 +53,10 @@ from .const import (
KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED,
KEY_WAVEFORM_REQUIRED,
PREFERENCES_RTC_EBOOT_WORDS,
PREFERENCES_RTC_NORMAL_WORDS,
THROW_STUBS_HEADER,
check_preferences_fit,
enable_serial,
enable_serial1,
esp8266_ns,
preferences_flash_words,
)
from .gpio import PinInitialState, add_pin_initial_states_array
@@ -420,7 +416,6 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE)
cg.add_define("USE_ESP8266_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
@@ -472,12 +467,6 @@ async def to_code(config: ConfigType) -> None:
# Other projects like Tasmota & ESPEasy also use this
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
cg.add_define("ESP8266_PREFERENCES_RTC_NORMAL_WORDS", PREFERENCES_RTC_NORMAL_WORDS)
cg.add_define("ESP8266_PREFERENCES_RTC_EBOOT_WORDS", PREFERENCES_RTC_EBOOT_WORDS)
cg.add_define(
"ESP8266_PREFERENCES_FLASH_WORDS",
preferences_flash_words(config[CONF_RESTORE_FROM_FLASH]),
)
if config[CONF_RESTORE_FROM_FLASH]:
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
@@ -539,7 +528,6 @@ async def to_code(config: ConfigType) -> None:
CORE.add_job(add_pin_initial_states_array)
CORE.add_job(finalize_waveform_config)
CORE.add_job(finalize_serial_config)
CORE.add_job(check_preferences_fit)
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
+1 -86
View File
@@ -1,13 +1,9 @@
import logging
import esphome.codegen as cg
# Re-exported from the shared definition; here it indexes the BOARDS
# metadata dicts, whose entries in boards.py spell the literal.
from esphome.const import KEY_FLASH_SIZE # noqa: F401 # pylint: disable=unused-import
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
_LOGGER = logging.getLogger(__name__)
from esphome.core import CORE
KEY_ESP8266 = "esp8266"
KEY_BOARD = "board"
@@ -28,87 +24,6 @@ KEY_LDSCRIPT = "ldscript"
# Crash-decode tools, resolved once per run by __init__
KEY_DECODE_TOOLS = "decode_tools"
# Saved-settings space in 4-byte words; preferences.cpp gets these as defines. A value cannot span the two
# RTC regions, and the flash store is smaller when only explicit flash preferences use it.
PREFERENCES_RTC_NORMAL_WORDS = 78
PREFERENCES_RTC_EBOOT_WORDS = 32
PREFERENCES_RTC_WORDS = PREFERENCES_RTC_NORMAL_WORDS + PREFERENCES_RTC_EBOOT_WORDS
PREFERENCES_FLASH_WORDS = 128
PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE = 64
KEY_PREFERENCES = "preferences"
def preferences_flash_words(restore_from_flash: bool) -> int:
"""Size of the flash store for this configuration."""
return (
PREFERENCES_FLASH_WORDS
if restore_from_flash
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
)
def preference_words(value_bytes: int) -> int:
"""Words a saved value takes, checksum word included (rtc_pref_bytes_to_words plus one)."""
return (value_bytes + 3) // 4 + 1
def reserve_preferences(
count: int, value_bytes: int, in_flash: bool | None = None
) -> None:
"""Record saved values a component creates; check_preferences_fit() adds them all up.
in_flash is the value's own storage, as passed to make_preference(); None is the two-argument
form, which follows restore_from_flash.
"""
if in_flash is None:
in_flash = CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
reservations = CORE.data.setdefault(KEY_ESP8266, {}).setdefault(KEY_PREFERENCES, [])
reservations.extend([(preference_words(value_bytes), in_flash)] * count)
def _rtc_values_can_split(values: list[int]) -> bool:
"""Whether some order fills the two RTC regions without a value spanning them."""
total = sum(values)
if total <= PREFERENCES_RTC_NORMAL_WORDS:
return True
# Bit i of reachable is set when some subset of the values sums to i
reachable = 1
for words in values:
reachable |= reachable << words
lowest = max(0, total - PREFERENCES_RTC_EBOOT_WORDS)
highest = min(total, PREFERENCES_RTC_NORMAL_WORDS)
return any(reachable >> normal & 1 for normal in range(lowest, highest + 1))
@coroutine_with_priority(CoroPriority.FINAL)
async def check_preferences_fit() -> None:
"""Fail the build when the reserved saved values cannot fit the space preferences.cpp has."""
flash_store = preferences_flash_words(
CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
)
reservations = CORE.data[KEY_ESP8266].get(KEY_PREFERENCES, [])
flash_words = sum(w for w, in_flash in reservations if in_flash)
rtc_values = [w for w, in_flash in reservations if not in_flash]
if flash_words > flash_store:
raise EsphomeError(
f"Saved values need {flash_words} words but the ESP8266 flash store has {flash_store}; "
"use fewer or smaller saved values"
)
if not _rtc_values_can_split(rtc_values):
raise EsphomeError(
f"Saved values need {sum(rtc_values)} words but ESP8266 RTC memory has {PREFERENCES_RTC_WORDS} in "
f"regions of {PREFERENCES_RTC_NORMAL_WORDS} and {PREFERENCES_RTC_EBOOT_WORDS} that a value cannot "
"span; use fewer or smaller saved values, or store them in flash"
)
if sum(rtc_values) > PREFERENCES_RTC_NORMAL_WORDS:
_LOGGER.warning(
"Saved values reserve %d of %d words of ESP8266 RTC memory; the fit depends on setup order, and "
"saved settings that do not reserve are not counted",
sum(rtc_values),
PREFERENCES_RTC_WORDS,
)
# esp8266 namespace is already defined by arduino, manually prefix esphome
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
+7 -5
View File
@@ -4,7 +4,6 @@
#ifdef USE_ESP8266_CRASH_HANDLER
#include "crash_handler.h"
#include "rtc_layout.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -47,8 +46,11 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
// Recover the actual code address from a windowed-ABI return address on the stack.
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
// Crash data lives in the last esphome::esp8266::RTC_CRASH_WORDS of user RTC memory (rtc_layout.h).
// RTC user memory layout for crash backtrace data.
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
// Store 16 raw candidates, filter to real return addresses at log time.
static constexpr uint8_t RTC_CRASH_BASE = 174;
static constexpr size_t MAX_BACKTRACE = 16;
// Magic word packs sentinel, version, and count into one uint32_t:
@@ -76,7 +78,7 @@ struct RtcCrashData {
uint32_t backtrace[MAX_BACKTRACE];
uint32_t epc1; // Fault PC, used to filter duplicates
};
static_assert(sizeof(RtcCrashData) == esphome::esp8266::RTC_CRASH_WORDS * 4, "RtcCrashData must fill its RTC words");
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
namespace esphome::esp8266 {
@@ -127,7 +129,7 @@ static const LogString *get_reset_reason(uint32_t reason) {
// Returns the number of valid backtrace entries (0 if no data found).
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
RtcCrashData rtc_data;
if (!system_rtc_mem_read(esphome::esp8266::RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
return 0;
uint32_t magic = rtc_data.magic;
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
@@ -231,7 +233,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3
data.epc1 = epc1;
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
system_rtc_mem_write(esphome::esp8266::RTC_CRASH_BASE, &data, sizeof(data));
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
}
#endif // USE_ESP8266_CRASH_HANDLER
+17 -13
View File
@@ -6,7 +6,6 @@ extern "C" {
}
#include "preferences.h"
#include "rtc_layout.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
@@ -18,26 +17,31 @@ namespace esphome::esp8266 {
ESPHOME_LOG_TAG(TAG, "preferences");
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = RTC_USER_MEM_WORDS;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
// RTC memory layout (sizes come from const.py through codegen):
// RTC memory layout:
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = ESP8266_PREFERENCES_RTC_EBOOT_WORDS;
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = ESP8266_PREFERENCES_RTC_NORMAL_WORDS;
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
static_assert(PREF_TOTAL_WORDS <= RTC_CRASH_BASE - RTC_SYSTEM_USER_BASE,
"preference regions overlap the crash handler's RTC words");
// Maximum preference size in words (limited by uint8_t length_words field)
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
// Smaller without restore_from_flash, when only explicit flash preferences live here
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = ESP8266_PREFERENCES_FLASH_WORDS;
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
#ifdef USE_ESP8266_PREFERENCES_FLASH
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
#else
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
#endif
static uint32_t
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -56,7 +60,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
}
if (index < RTC_EBOOT_REGION_WORDS && s_prevent_write) {
if (index < 32 && s_prevent_write) {
return false;
}
@@ -119,7 +123,7 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
// Maximum buffer for any single preference - bounded by storage sizes.
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (78) - a single pref can't span both RTC regions.
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
@@ -176,8 +180,8 @@ ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t
} else {
uint32_t start = this->current_offset;
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
// Normal: offset 0-77 maps to RTC offset 32-109
// Eboot: offset 78-109 maps to RTC offset 0-31
// Normal: offset 0-95 maps to RTC offset 32-127
// Eboot: offset 96-127 maps to RTC offset 0-31
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
// start is in normal but end is not -> switch to Eboot
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#ifdef USE_ESP8266
#include <cstdint>
namespace esphome::esp8266 {
// User RTC memory is system blocks 64-191, one 4-byte word each. preferences.cpp owns the first
// words, crash_handler.cpp the last; const.py hands preferences.cpp its region sizes.
static constexpr uint32_t RTC_SYSTEM_USER_BASE = 64;
static constexpr uint32_t RTC_USER_MEM_WORDS = 128;
static constexpr uint32_t RTC_CRASH_WORDS = 18;
static constexpr uint32_t RTC_CRASH_BASE = RTC_SYSTEM_USER_BASE + RTC_USER_MEM_WORDS - RTC_CRASH_WORDS;
} // namespace esphome::esp8266
#endif // USE_ESP8266
+10 -101
View File
@@ -44,14 +44,11 @@ const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
}
#endif
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 60000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
// Milliseconds for data transfer. Covers the lwIP retransmit run seen in
// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits
// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000;
// A handshake this old gives way to a waiting connection: a client retrying over a lossy link otherwise queues
// behind its own stalled attempt until both time out
static constexpr uint32_t OTA_HANDSHAKE_PREEMPT = 10000;
static constexpr uint32_t OTA_PROGRESS_INTERVAL_MS = 1000;
static constexpr size_t OTA_SIZE_FIELD_BYTES = 4; // sizes on the wire are 4 bytes MSB first
@@ -202,9 +199,6 @@ static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_DEFLATE = 0x10;
#ifdef ESPHOME_OTA_UDP
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_UDP = 0x20;
#endif
// Noise needs the extended protocol: the prologue binds the 2-byte feature ack
static constexpr uint8_t CLIENT_NOISE_FEATURES =
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
@@ -214,10 +208,6 @@ static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
// Raw deflate, window <= OTA_INFLATE_WINDOW_SIZE. Binding once offered: the
// client must then send the image size frame and a deflate stream.
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_DEFLATE = 0x08;
#ifdef ESPHOME_OTA_UDP
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_UDP = 0x10;
static constexpr uint8_t UDP_REQUEST_V1 = 0x01; // espota2.UDP_REQUEST
#endif
#ifdef USE_OTA_ENCRYPTION
inline bool ESPHomeOTAComponent::noise_offered_() const {
@@ -276,12 +266,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
this->cleanup_connection_();
return;
}
// Only unauthenticated handshakes reach here; the data phase blocks inside handle_data_
if (now - this->client_connect_time_ > OTA_HANDSHAKE_PREEMPT && this->server_->ready()) {
ESP_LOGD(TAG, "Newer connection waiting");
this->cleanup_connection_();
return;
}
switch (this->ota_state_) {
case OTAState::MAGIC_READ: {
@@ -345,9 +329,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS;
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
#ifdef ESPHOME_OTA_UDP
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_UDP;
#endif
#ifdef USE_OTA_PARTITIONS
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
#endif
@@ -502,10 +483,6 @@ void ESPHomeOTAComponent::handle_data_() {
char *sbuf = reinterpret_cast<char *>(buf);
size_t image_size;
ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP;
#ifdef ESPHOME_OTA_UDP
UdpLink udp_link;
this->udp_ = nullptr; // client_connect_time_ is done with
#endif
// Set socket timeouts and blocking mode (see strategy table above)
struct timeval tv;
@@ -521,38 +498,6 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge auth OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
#ifdef ESPHOME_OTA_UDP
// The server always offers UDP on the extended protocol
if (this->extended_proto_() && (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_UDP) != 0) {
// Whether the client wants UDP (0: its link looks good), answered with our token
if (!this->data_readall_(buf, 1)) {
if (this->client_left_before_start_())
return;
this->log_read_error_(LOG_STR("UDP request"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
// 1 is this UDP format; other values are free for later ones, which we decline (a zero reply keeps TCP)
if (buf[0] == UDP_REQUEST_V1) {
this->udp_open_(udp_link);
if (udp_link.reply[0] == 0) {
ESP_LOGW(TAG, "UDP unavailable, using TCP");
}
}
if (!this->data_write_(udp_link.reply, sizeof(udp_link.reply)))
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
if (udp_link.reply[0] != 0) {
this->udp_ = &udp_link;
if (!this->udp_start_()) {
// Nothing started yet: just drop the session, which the client retries as a network failure
this->log_read_error_(LOG_STR("UDP start"));
this->cleanup_connection_();
return;
}
}
}
#endif
if (this->extended_proto_()) {
// Read ota type, 1 byte
if (!this->data_readall_(buf, 1)) {
@@ -653,17 +598,11 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge Update end OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
#ifdef ESPHOME_OTA_UDP
// UDP never reports a close: wait only until the client has our responses, not for its final ack
if (this->udp_ != nullptr) {
this->udp_linger_();
} else
#endif
// Read ACK
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
this->log_read_error_(LOG_STR("ack"));
// do not go to error, this is not fatal
}
// Read ACK
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
this->log_read_error_(LOG_STR("ack"));
// do not go to error, this is not fatal
}
this->cleanup_connection_();
delay(10);
@@ -684,9 +623,6 @@ void ESPHomeOTAComponent::handle_data_() {
error:
this->data_write_byte_(static_cast<uint8_t>(error_code));
#ifdef ESPHOME_OTA_UDP
this->udp_linger_();
#endif
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
// Always call abort() unconditionally: backends register external partitions before
@@ -715,7 +651,7 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
return false;
}
ssize_t read = this->transport_read_(buf + at, len - at);
ssize_t read = this->client_->read(buf + at, len - at);
if (read == -1) {
const int err = errno;
if (!this->would_block_(err)) {
@@ -740,10 +676,6 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
return true;
}
bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
#ifdef ESPHOME_OTA_UDP
if (this->udp_ != nullptr)
return this->udp_send_(buf, len);
#endif
uint32_t start = millis();
uint32_t at = 0;
while (len - at > 0) {
@@ -796,20 +728,7 @@ void ESPHomeOTAComponent::log_read_error_(const LogString *what) {
void ESPHomeOTAComponent::log_start_(const LogString *phase) {
char peername[socket::SOCKADDR_STR_LEN];
#ifdef ESPHOME_OTA_UDP
if (this->client_ == nullptr) {
// A committed UDP session has already dropped TCP
#ifdef ESPHOME_OTA_UDP_LWIP
ipaddr_ntoa_r(&this->udp_->peer_ip, peername, sizeof(peername));
#else
socket::format_sockaddr_to(reinterpret_cast<const struct sockaddr *>(&this->udp_->peer), this->udp_->peer_len,
peername);
#endif
} else
#endif
{
this->client_->getpeername_to(peername);
}
this->client_->getpeername_to(peername);
ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername);
}
@@ -921,7 +840,7 @@ ssize_t ESPHomeOTAComponent::receive_data_(uint8_t *buf, DataTransfer &xfer) {
break;
}
#endif
read = this->transport_read_(buf, requested);
read = this->client_->read(buf, requested);
if (read > 0)
break;
if (read == 0) {
@@ -955,11 +874,6 @@ ssize_t ESPHomeOTAComponent::receive_data_(uint8_t *buf, DataTransfer &xfer) {
void ESPHomeOTAComponent::send_chunk_acks_(DataTransfer &xfer) {
#if USE_OTA_VERSION == 2
#ifdef ESPHOME_OTA_UDP
// UDP acks every message itself
if (this->udp_ != nullptr)
return;
#endif
while (xfer.acknowledged + OTA_BLOCK_SIZE <= xfer.total ||
(xfer.total == xfer.ota_size && xfer.acknowledged < xfer.ota_size)) {
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
@@ -969,12 +883,7 @@ void ESPHomeOTAComponent::send_chunk_acks_(DataTransfer &xfer) {
}
void ESPHomeOTAComponent::cleanup_connection_() {
#ifdef ESPHOME_OTA_UDP
this->udp_ = nullptr; // the link itself lives on handle_data_'s stack
#endif
// Already gone once a UDP session committed
if (this->client_ != nullptr)
this->client_->close();
this->client_->close();
this->client_ = nullptr;
this->client_connect_time_ = 0;
this->handshake_buf_pos_ = 0;
+5 -103
View File
@@ -15,25 +15,6 @@
#include "esphome/core/preferences.h"
#include "esphome/core/hash_base.h"
// UDP data phase: raw lwIP needs a main loop lock (or a single thread); the host polls a socket instead
#ifdef USE_ESP32
#include <sdkconfig.h>
#endif
#if defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || \
(defined(USE_ESP32) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING))
#define ESPHOME_OTA_UDP
#define ESPHOME_OTA_UDP_LWIP
#include "lwip/ip_addr.h"
#include "lwip/pbuf.h"
#include "lwip/udp.h"
#if defined(USE_LIBRETINY) && !LWIP_TCPIP_CORE_LOCKING
#undef ESPHOME_OTA_UDP
#undef ESPHOME_OTA_UDP_LWIP
#endif
#elif defined(USE_HOST)
#define ESPHOME_OTA_UDP
#endif
namespace esphome {
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
@@ -84,69 +65,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
protected:
void handle_handshake_();
void handle_data_();
#ifdef ESPHOME_OTA_UDP
// UDP transport for the data phase; wire format in ota_esphome_udp.cpp
static constexpr size_t UDP_OUT_SIZE = 64; // our unacked responses: at most three encrypted ones
// Messages the device keeps unread: like TCP's 4 segment window, so no more memory than TCP holds
static constexpr uint8_t UDP_WINDOW = 4;
#ifdef ESPHOME_OTA_UDP_LWIP
using UdpSlot = struct pbuf *;
#else
using UdpSlot = struct OtaUdpDatagram *;
#endif
// Lives on handle_data_'s stack; the receive callback shares it under the lwIP lock
struct UdpLink {
~UdpLink();
void release(); // closes the socket and frees what is queued
#ifdef ESPHOME_OTA_UDP_LWIP
struct udp_pcb *pcb{nullptr};
ip_addr_t peer_ip;
#else
std::unique_ptr<socket::Socket> sock;
struct sockaddr_storage peer; // the TCP peer; its port is replaced by peer_port
socklen_t peer_len{0};
#endif
uint16_t peer_port{0}; // learned from the client's datagrams
// The session token, our answer to the client: top bit clear, so never an error code; all zero declines
uint8_t reply[4]{};
UdpSlot queue[UDP_WINDOW]{}; // slot seq % UDP_WINDOW for seq in [consumed, consumed + UDP_WINDOW)
uint16_t received{0}; // every seq before it is here
uint16_t consumed{0}; // the message being read
uint16_t read_offset{0}; // payload bytes of it already read
uint16_t out_base{0}; // stream offset of out[0]
uint8_t out_len{0};
bool committed{false}; // the client heard us over UDP
uint8_t out[UDP_OUT_SIZE];
};
// Fills link.reply on success; leaves it all zero (declined) otherwise
void udp_open_(UdpLink &link);
// Waits for the client to commit to UDP or fall back to TCP; false on failure
bool udp_start_();
// Like a blocking socket read with SO_RCVTIMEO: up to len bytes, or -1 with EWOULDBLOCK
ssize_t udp_read_(uint8_t *buf, size_t len);
bool udp_send_(const uint8_t *data, size_t len);
// In a UDP session, keeps answering until the client has our responses (an error code), briefly
void udp_linger_();
// A datagram from the client's IP; caller holds the lwIP lock (or runs in the receive callback)
static void udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port);
// Caller holds the lwIP lock (or runs in the receive callback)
static void udp_send_ack(UdpLink &link, uint16_t prompted_by);
#ifdef ESPHOME_OTA_UDP_LWIP
static void udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, uint16_t port);
void udp_poll_() {}
#else
void udp_poll_(); // hands what the socket received to udp_on_datagram
#endif
#endif
// Data phase only (udp_ shares storage with the handshake's connect time): TCP or, once committed, the UDP stream
inline ssize_t transport_read_(uint8_t *buf, size_t len) {
#ifdef ESPHOME_OTA_UDP
if (this->udp_ != nullptr)
return this->udp_read_(buf, len);
#endif
return this->client_->read(buf, len);
}
#ifdef USE_OTA_PASSWORD
static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
bool handle_auth_send_();
@@ -157,6 +75,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
#endif // USE_OTA_PASSWORD
bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len);
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
#ifdef USE_OTA_ENCRYPTION
// Heap-allocated only while an encrypted OTA session is active.
@@ -186,21 +105,18 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
bool noise_readall_(uint8_t *buf, size_t len);
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
static constexpr size_t NOISE_MAX_RESPONSE = 4; // the UDP token
bool noise_write_(const uint8_t *data, size_t len);
bool noise_write_byte_(uint8_t byte);
#endif // USE_OTA_ENCRYPTION
// Data-phase I/O dispatch: through the noise transport when a session is
// active, straight to the socket otherwise.
// len is at most NOISE_MAX_RESPONSE
inline bool data_write_(const uint8_t *data, size_t len) {
inline bool data_write_byte_(uint8_t byte) {
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr)
return this->noise_write_(data, len);
return this->noise_write_byte_(byte);
#endif
return this->writeall_(data, len);
return this->write_byte_(byte);
}
inline bool data_write_byte_(uint8_t byte) { return this->data_write_(&byte, 1); }
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
inline bool data_readall_(uint8_t *buf, size_t len) {
#ifdef USE_OTA_ENCRYPTION
@@ -284,21 +200,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
std::unique_ptr<socket::Socket> client_;
ota::OTABackendPtr backend_;
#ifdef ESPHOME_OTA_UDP_LWIP
union {
uint32_t client_connect_time_{0}; // handshake states
UdpLink *udp_; // data phase (never both), points into handle_data_'s stack
};
static_assert(sizeof(UdpLink *) == sizeof(uint32_t), "clearing client_connect_time_ must clear udp_");
#else
uint32_t client_connect_time_{0};
#ifdef ESPHOME_OTA_UDP
UdpLink *udp_{nullptr}; // data phase, points into handle_data_'s stack
#endif
#endif
#if defined(ESPHOME_OTA_UDP) && defined(USE_OTA_ENCRYPTION)
static_assert(sizeof(UdpLink::reply) <= NOISE_MAX_RESPONSE, "the token goes out through noise_write_");
#endif
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
// Buffer size for OTA data transfer. The upload client derives its maximum
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
@@ -266,15 +266,13 @@ ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
}
/// Blocking write of a short response (at most NOISE_MAX_RESPONSE bytes) as one encrypted frame.
bool ESPHomeOTAComponent::noise_write_(const uint8_t *data, size_t len) {
uint8_t frame[noise::FRAME_HEADER_SIZE + NOISE_MAX_RESPONSE + noise::MAC_SIZE];
if (len > NOISE_MAX_RESPONSE)
return false;
memcpy(frame + noise::FRAME_HEADER_SIZE, data, len);
/// Blocking write of one response byte as an encrypted frame.
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
frame[noise::FRAME_HEADER_SIZE] = byte;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, len, len + noise::MAC_SIZE);
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
if (err != 0) {
ESP_LOGW(TAG, "Encrypt: %d", err);
@@ -1,375 +0,0 @@
#include "ota_esphome.h"
#if defined(USE_OTA) && defined(ESPHOME_OTA_UDP)
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef ESPHOME_OTA_UDP_LWIP
#include "lwip/inet.h"
#ifdef USE_LIBRETINY
#include "lwip/tcpip.h"
#endif
#endif
#include <algorithm>
#include <cstring>
#include <memory>
/*
* UDP data phase: the same byte stream as TCP, cut into numbered datagrams.
* client -> device [type | COMMITTED][token u32][seq u16][device bytes received u16][payload]
* device -> client [ACK][token u32][received u16][seq that prompted it u16][stream offset u16][window u8][our bytes]
* The lwIP callback acks each datagram on arrival, so flash writes never look like loss, and keeps messages in its
* UDP_WINDOW slots until the OTA loop reads them. "window": free slots in the high nibble, and in the low nibble bit k
* set when received + k is already here (like TCP SACK).
*/
namespace esphome {
ESPHOME_LOG_TAG(TAG, "esphome.ota");
// PING (0x02) and PROBE (0x03) only prompt an ACK
static constexpr uint8_t UDP_MSG_DATA = 0x01;
static constexpr uint8_t UDP_FLAG_COMMITTED = 0x80;
static constexpr uint8_t UDP_MSG_ACK = 0x40;
static constexpr size_t UDP_HEADER_SIZE = 9;
static constexpr size_t UDP_ACK_HEADER_SIZE = 12;
// The client's 60 s wait for the token over a lossy TCP link, plus its 15 s probe
static constexpr uint32_t UDP_COMMIT_TIMEOUT = 80000;
// Like the TCP socket's SO_RCVTIMEO in handle_data_, so readall_ treats both alike
static constexpr uint32_t UDP_READ_WAIT_MS = 2000;
// How often the commit wait looks at the UDP link between reads of the TCP fallback byte
static constexpr uint32_t UDP_COMMIT_POLL_MS = 100;
static constexpr uint32_t UDP_LINGER_MS = 3000;
#ifdef ESPHOME_OTA_UDP_LWIP
#ifdef USE_LIBRETINY
// LibreTiny's LwIPLock is a no-op; take the core lock directly (the header drops UDP without one)
struct UdpLock {
UdpLock() { LOCK_TCPIP_CORE(); }
~UdpLock() { UNLOCK_TCPIP_CORE(); }
};
#else
using UdpLock = LwIPLock;
#endif
static uint16_t slot_len(struct pbuf *p) { return p->tot_len; }
static uint16_t slot_copy(struct pbuf *p, void *dst, uint16_t len, uint16_t offset) {
return pbuf_copy_partial(p, dst, len, offset);
}
static void slot_free(struct pbuf *p) { pbuf_free(p); }
#else
// The host has no lwIP thread: the OTA loop drains the socket itself. Empty bodies, not = default, so clang-tidy
// does not flag the guards as unused
struct UdpLock {
UdpLock() {}
~UdpLock() {}
};
struct OtaUdpDatagram {
uint16_t len;
uint8_t data[1500];
};
static uint16_t slot_len(OtaUdpDatagram *p) { return p->len; }
static uint16_t slot_copy(OtaUdpDatagram *p, void *dst, uint16_t len, uint16_t offset) {
const uint16_t n = std::min<uint16_t>(len, p->len - offset);
memcpy(dst, p->data + offset, n);
return n;
}
static void slot_free(OtaUdpDatagram *p) { delete p; }
#endif
static void put_u16(uint8_t *at, uint16_t value) {
at[0] = value >> 8;
at[1] = value & 0xFF;
}
ESPHomeOTAComponent::UdpLink::~UdpLink() { this->release(); }
void ESPHomeOTAComponent::UdpLink::release() {
UdpLock lock;
#ifdef ESPHOME_OTA_UDP_LWIP
if (this->pcb != nullptr)
udp_remove(this->pcb);
this->pcb = nullptr;
#else
this->sock = nullptr;
#endif
for (auto &p : this->queue) {
if (p != nullptr)
slot_free(p);
p = nullptr;
}
}
void ESPHomeOTAComponent::udp_send_ack(UdpLink &link, uint16_t prompted_by) {
const uint16_t len = UDP_ACK_HEADER_SIZE + link.out_len;
#ifdef ESPHOME_OTA_UDP_LWIP
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
if (p == nullptr)
return; // Like a lost ACK: the client asks again
auto *ack = static_cast<uint8_t *>(p->payload);
#else
uint8_t ack[UDP_ACK_HEADER_SIZE + UDP_OUT_SIZE];
#endif
ack[0] = UDP_MSG_ACK;
memcpy(ack + 1, link.reply, 4);
put_u16(ack + 5, link.received);
put_u16(ack + 7, prompted_by);
put_u16(ack + 9, link.out_base);
uint8_t window = (UDP_WINDOW - static_cast<uint16_t>(link.received - link.consumed)) << 4;
for (uint8_t k = 1; k < UDP_WINDOW; k++) {
if (link.queue[(link.received + k) % UDP_WINDOW] != nullptr)
window |= 1 << k;
}
ack[11] = window;
memcpy(ack + UDP_ACK_HEADER_SIZE, link.out, link.out_len);
// Best effort: a lost ACK is repeated for the client's next datagram
#ifdef ESPHOME_OTA_UDP_LWIP
udp_sendto(link.pcb, p, &link.peer_ip, link.peer_port);
pbuf_free(p);
#else
struct sockaddr_storage to = link.peer;
if (to.ss_family == AF_INET6) {
reinterpret_cast<sockaddr_in6 *>(&to)->sin6_port = htons(link.peer_port);
} else {
reinterpret_cast<sockaddr_in *>(&to)->sin_port = htons(link.peer_port);
}
link.sock->sendto(ack, len, 0, reinterpret_cast<const struct sockaddr *>(&to), link.peer_len);
#endif
}
void ESPHomeOTAComponent::udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port) {
uint8_t hdr[UDP_HEADER_SIZE];
if (slot_copy(p, hdr, UDP_HEADER_SIZE, 0) != UDP_HEADER_SIZE || memcmp(hdr + 1, link.reply, 4) != 0) {
slot_free(p);
return;
}
// The token proves the sender; answer the port it arrived from, which NAT may have rewritten
link.peer_port = port;
link.committed |= (hdr[0] & UDP_FLAG_COMMITTED) != 0;
// Drop the responses the client already has
const uint16_t acked = encode_uint16(hdr[7], hdr[8]) - link.out_base;
if (acked != 0 && acked <= link.out_len) {
link.out_len -= acked;
memmove(link.out, link.out + acked, link.out_len);
link.out_base += acked;
}
const uint16_t seq = encode_uint16(hdr[5], hdr[6]);
UdpSlot &slot = link.queue[seq % UDP_WINDOW];
if ((hdr[0] & ~UDP_FLAG_COMMITTED) == UDP_MSG_DATA && slot_len(p) > UDP_HEADER_SIZE &&
static_cast<uint16_t>(seq - link.consumed) < UDP_WINDOW && slot == nullptr) {
slot = p;
while (static_cast<uint16_t>(link.received - link.consumed) < UDP_WINDOW &&
link.queue[link.received % UDP_WINDOW] != nullptr)
link.received++;
} else {
slot_free(p);
}
udp_send_ack(link, seq);
}
#ifdef ESPHOME_OTA_UDP_LWIP
// lwIP context: the tcpip thread (ESP32, LibreTiny), SYS (ESP8266) or an IRQ (RP2040)
void ESPHomeOTAComponent::udp_recv_cb(void *arg, struct udp_pcb * /*pcb*/, struct pbuf *p, const ip_addr_t *addr,
uint16_t port) {
auto &link = *static_cast<UdpLink *>(arg);
if (!ip_addr_cmp(addr, &link.peer_ip)) {
pbuf_free(p);
return;
}
udp_on_datagram(link, p, port);
}
#else
static bool same_ip(const struct sockaddr_storage &a, const struct sockaddr_storage &b) {
uint32_t ipv4_a, ipv4_b;
const bool v4_a = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&a), &ipv4_a);
const bool v4_b = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&b), &ipv4_b);
if (v4_a || v4_b)
return v4_a && v4_b && ipv4_a == ipv4_b;
return a.ss_family == AF_INET6 && b.ss_family == AF_INET6 &&
memcmp(&reinterpret_cast<const sockaddr_in6 *>(&a)->sin6_addr,
&reinterpret_cast<const sockaddr_in6 *>(&b)->sin6_addr, sizeof(in6_addr)) == 0;
}
void ESPHomeOTAComponent::udp_poll_() {
UdpLink &link = *this->udp_;
for (;;) {
uint8_t buf[sizeof(OtaUdpDatagram::data)];
struct sockaddr_storage from;
socklen_t from_len = sizeof(from);
const ssize_t len = link.sock->recvfrom(buf, sizeof(buf), reinterpret_cast<struct sockaddr *>(&from), &from_len);
if (len < 0)
return;
if (!same_ip(from, link.peer))
continue;
auto *datagram = new OtaUdpDatagram; // NOLINT(cppcoreguidelines-owning-memory)
datagram->len = len;
memcpy(datagram->data, buf, len);
const uint16_t port = from.ss_family == AF_INET6 ? ntohs(reinterpret_cast<sockaddr_in6 *>(&from)->sin6_port)
: ntohs(reinterpret_cast<sockaddr_in *>(&from)->sin_port);
udp_on_datagram(link, datagram, port);
}
}
#endif
void ESPHomeOTAComponent::udp_open_(UdpLink &link) {
struct sockaddr_storage peer;
socklen_t peer_len = sizeof(peer);
if (this->client_->getpeername(reinterpret_cast<struct sockaddr *>(&peer), &peer_len) != 0)
return;
uint8_t token[4];
if (!random_bytes(token, sizeof(token)))
return;
#ifdef ESPHOME_OTA_UDP_LWIP
uint32_t ipv4;
if (socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&peer), &ipv4)) {
ip_addr_set_ip4_u32(&link.peer_ip, ipv4);
} else {
#if USE_NETWORK_IPV6 && LWIP_IPV6
inet6_addr_to_ip6addr(ip_2_ip6(&link.peer_ip), &reinterpret_cast<const sockaddr_in6 *>(&peer)->sin6_addr);
IP_SET_TYPE_VAL(link.peer_ip, IPADDR_TYPE_V6);
#else
return;
#endif
}
UdpLock lock;
#if LWIP_IPV6
link.pcb = udp_new_ip_type(IPADDR_TYPE_ANY);
#else
link.pcb = udp_new();
#endif
// On failure the reply stays all zero and ~UdpLink removes the pcb
if (link.pcb == nullptr || udp_bind(link.pcb, IP_ANY_TYPE, this->port_) != ERR_OK)
return;
udp_recv(link.pcb, udp_recv_cb, &link);
#else
link.peer = peer;
link.peer_len = peer_len;
// Same domain as the listener, so the peer address from getpeername fits
link.sock = socket::socket_ip(SOCK_DGRAM, IPPROTO_UDP);
struct sockaddr_storage local;
const socklen_t local_len =
socket::set_sockaddr_any(reinterpret_cast<struct sockaddr *>(&local), sizeof(local), this->port_);
if (link.sock == nullptr || local_len == 0 ||
link.sock->bind(reinterpret_cast<struct sockaddr *>(&local), local_len) != 0 ||
link.sock->setblocking(false) != 0)
return;
#endif
// Below the error codes (0x80 and up) the client checks the first byte against, and never all zero
token[0] = (token[0] & 0x7F) | 0x01;
memcpy(link.reply, token, sizeof(token));
}
bool ESPHomeOTAComponent::udp_start_() {
// TCP now only carries a fallback byte; short read timeouts let the UDP link be checked in between
struct timeval tv {
0, UDP_COMMIT_POLL_MS * 1000
};
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
const uint32_t start = millis();
while (millis() - start < UDP_COMMIT_TIMEOUT) {
this->udp_poll_();
bool committed;
{
UdpLock lock;
committed = this->udp_->committed;
}
if (committed) {
// The client heard us over UDP, so TCP is done. Outside the lock: closing a socket waits on the tcpip thread
this->client_ = nullptr;
return true;
}
// A raw byte outside any Noise frame: the client heard nothing over UDP. Forging it only forces the ordinary
// TCP path, which dropping our datagrams forces anyway
uint8_t fallback;
const ssize_t read = this->client_->read(&fallback, 1);
if (read == 1) {
this->udp_->release();
this->udp_ = nullptr;
tv = {UDP_READ_WAIT_MS / 1000, 0};
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
return true;
}
if (read == 0) {
this->remote_closed_ = true;
return false;
}
if (!this->would_block_(errno))
return false;
App.feed_wdt();
}
return false;
}
ssize_t ESPHomeOTAComponent::udp_read_(uint8_t *buf, size_t len) {
UdpLink &link = *this->udp_;
const uint32_t start = millis();
size_t got = 0;
for (;;) {
this->udp_poll_();
UdpSlot p;
{
UdpLock lock;
p = link.consumed != link.received ? link.queue[link.consumed % UDP_WINDOW] : nullptr;
}
if (p == nullptr) {
if (got != 0)
return got;
if (millis() - start >= UDP_READ_WAIT_MS)
break;
this->yield_and_feed_watchdog_();
continue;
}
// The callback never touches the slot being read, so the copy runs without the lock
const uint16_t offset = UDP_HEADER_SIZE + link.read_offset;
const uint16_t n = slot_copy(p, buf + got, std::min<size_t>(len - got, slot_len(p) - offset), offset);
got += n;
link.read_offset += n;
if (UDP_HEADER_SIZE + link.read_offset >= slot_len(p)) {
UdpLock lock;
slot_free(p);
link.queue[link.consumed % UDP_WINDOW] = nullptr;
// A full window just opened: tell the client, which is holding back
const bool was_full = static_cast<uint16_t>(link.received - link.consumed) == UDP_WINDOW;
link.consumed++;
link.read_offset = 0;
if (was_full)
udp_send_ack(link, link.received - 1);
}
if (got == len)
return got;
}
errno = EWOULDBLOCK;
return -1;
}
bool ESPHomeOTAComponent::udp_send_(const uint8_t *data, size_t len) {
UdpLink &link = *this->udp_;
UdpLock lock;
if (link.out_len + len > UDP_OUT_SIZE)
return false;
memcpy(link.out + link.out_len, data, len);
link.out_len += len;
// Prompted by nothing new, so the client takes no round trip sample from it
udp_send_ack(link, link.received - 1);
return true;
}
void ESPHomeOTAComponent::udp_linger_() {
if (this->udp_ == nullptr)
return;
const uint32_t start = millis();
while (millis() - start < UDP_LINGER_MS) {
this->udp_poll_();
{
UdpLock lock;
if (this->udp_->out_len == 0)
return;
}
this->yield_and_feed_watchdog_();
}
}
} // namespace esphome
#endif // USE_OTA && ESPHOME_OTA_UDP
-40
View File
@@ -1,40 +0,0 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN
from esphome.types import ConfigType
CODEOWNERS = ["@remcom"]
DEPENDENCIES = ["i2c"]
DOMAIN = "fusb302b"
CONF_REQUEST_VOLTAGE = "request_voltage"
fusb302b_ns = cg.esphome_ns.namespace("fusb302b")
FUSB302B = fusb302b_ns.class_("FUSB302B", cg.Component, i2c.I2CDevice)
# USB PD fixed supply voltages are 5, 9, 12, 15 and 20 V; the highest one that does not exceed the
# requested voltage is chosen.
validate_voltage = cv.int_range(min=5, max=20)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(FUSB302B),
cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pullup_pin_schema,
cv.Required(CONF_REQUEST_VOLTAGE): validate_voltage,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x22)),
cv.only_on_esp32,
)
async def to_code(config: ConfigType) -> None:
interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
var = cg.new_Pvariable(config[CONF_ID], interrupt_pin)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_request_voltage(config[CONF_REQUEST_VOLTAGE]))
-415
View File
@@ -1,415 +0,0 @@
#include "fusb302b.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include "fusb302b_registers.h"
namespace esphome::fusb302b {
ESPHOME_LOG_TAG(TAG, "fusb302b");
// Task notification bits
static constexpr uint32_t NOTIFY_IRQ = 1 << 0;
static constexpr uint32_t NOTIFY_STOP = 1 << 1;
static constexpr uint32_t TASK_STACK_SIZE = 4096;
static constexpr UBaseType_t TASK_PRIORITY = 18;
static constexpr uint32_t STARTUP_DELAY_MS = 2000;
static constexpr uint32_t CC_SETTLE_MS = 5;
static constexpr uint32_t CC_POLL_INTERVAL_MS = 500;
static constexpr uint32_t PD_POLL_INTERVAL_MS = 100;
// Safety net in case an interrupt edge is missed while attached and idle
static constexpr uint32_t IDLE_POLL_INTERVAL_MS = 1000;
// Time to wait for the source to send its capabilities on its own after attach
static constexpr uint32_t SOURCE_CAP_WAIT_MS = 5000;
// Time to wait after each GET_SOURCE_CAP / soft reset before the next recovery step
static constexpr uint32_t SOURCE_CAP_RETRY_MS = 2500;
// Upper bound on back-to-back interrupt handling while INT_N stays asserted
static constexpr uint8_t MAX_IRQ_ROUNDS = 8;
// BC_LVL is read this many extra times to confirm a stable measurement
static constexpr uint8_t CC_STABLE_READS = 5;
static constexpr uint8_t CC_LEVEL_UNSTABLE = 0xFF;
void IRAM_ATTR FUSB302B::gpio_intr(FUSB302B *arg) {
TaskHandle_t handle = arg->task_.get_handle();
if (handle == nullptr)
return;
BaseType_t woken = pdFALSE;
xTaskNotifyFromISR(handle, NOTIFY_IRQ, eSetBits, &woken);
portYIELD_FROM_ISR(woken);
}
void FUSB302B::task_func(void *arg) { static_cast<FUSB302B *>(arg)->run_task_(); }
void FUSB302B::setup() {
uint8_t device_id = 0;
if (!this->read_byte(FUSB_DEVICE_ID, &device_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
const uint8_t version = device_id & FUSB_DEVICE_ID_VERSION_MASK;
if (version != FUSB_DEVICE_ID_VERSION_A && version != FUSB_DEVICE_ID_VERSION_B) {
ESP_LOGE(TAG, "Unknown device ID 0x%02X", device_id);
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Device ID 0x%02X (product %u, revision %u)", device_id, (device_id >> 2) & 0x3, device_id & 0x3);
if (!this->init_chip_()) {
this->mark_failed(LOG_STR("Initialization failed"));
return;
}
this->interrupt_pin_->setup();
// The task must exist before the interrupt is attached, since the ISR notifies it
if (!this->task_.create(FUSB302B::task_func, "fusb302b", TASK_STACK_SIZE, this, TASK_PRIORITY, false)) {
this->mark_failed(LOG_STR("Failed to create task"));
return;
}
this->interrupt_pin_->attach_interrupt(&FUSB302B::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
}
void FUSB302B::dump_config() {
ESP_LOGCONFIG(TAG,
"FUSB302B:\n"
" Request Voltage: %u V",
this->get_request_voltage());
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
}
void FUSB302B::on_shutdown() {
this->interrupt_pin_->detach_interrupt();
// The task parks itself instead of being deleted, so it can never be stopped in the middle of an I2C transfer
if (this->task_.is_created())
xTaskNotify(this->task_.get_handle(), NOTIFY_STOP, eSetBits);
}
void FUSB302B::run_task_() {
// Give the source time to settle after power-up before the first CC measurement
vTaskDelay(pdMS_TO_TICKS(STARTUP_DELAY_MS));
while (true) {
uint32_t wait_ms = CC_POLL_INTERVAL_MS;
if (this->attached_) {
const bool busy = this->waiting_for_source_caps_ || this->retry_request_pending_ || this->check_ams_();
wait_ms = busy ? PD_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
}
uint32_t bits = 0;
xTaskNotifyWait(0, UINT32_MAX, &bits, pdMS_TO_TICKS(wait_ms));
if (bits & NOTIFY_STOP) {
while (true)
vTaskSuspend(nullptr);
}
// INT_N is held low until the interrupt registers are read, so a new event that arrives while it is
// already low has no falling edge: keep servicing while the pin stays asserted.
if (bits & NOTIFY_IRQ)
this->handle_interrupt_();
for (uint8_t i = 0; i < MAX_IRQ_ROUNDS && !this->interrupt_pin_->digital_read(); i++)
this->handle_interrupt_();
if (!this->attached_) {
this->try_attach_();
continue;
}
this->check_transition_timeout_();
this->check_request_retry_();
this->check_source_caps_();
}
}
bool FUSB302B::init_chip_() {
// Software reset restores all registers to their defaults
if (!this->write_byte(FUSB_RESET, FUSB_RESET_SW_RES))
return false;
if (!this->reset_pd_())
return false;
// Unmask the interrupts used for PD messaging and VBUS changes
if (!this->write_byte(FUSB_MASK1, FUSB_MASK1_DEFAULT) || !this->write_byte(FUSB_MASKA, 0) ||
!this->write_byte(FUSB_MASKB, 0))
return false;
// Let the chip retransmit unacknowledged messages up to 3 times
uint8_t control3 = 0;
if (!this->read_byte(FUSB_CONTROL3, &control3))
return false;
control3 =
(control3 & ~FUSB_CONTROL3_N_RETRIES_MASK) | (3 << FUSB_CONTROL3_N_RETRIES_SHIFT) | FUSB_CONTROL3_AUTO_RETRY;
if (!this->write_byte(FUSB_CONTROL3, control3))
return false;
if (!this->write_byte(FUSB_POWER, FUSB_POWER_ALL))
return false;
return this->reset_pd_();
}
bool FUSB302B::reset_pd_() {
this->reset_protocol_();
// Writing CONTROL0 as a whole byte also clears INT_MASK, which is what enables the INT_N pin
return this->write_byte(FUSB_CONTROL0, FUSB_CONTROL0_TX_FLUSH) &&
this->write_byte(FUSB_CONTROL1, FUSB_CONTROL1_RX_FLUSH) && this->write_byte(FUSB_RESET, FUSB_RESET_PD_RESET);
}
bool FUSB302B::enable_auto_crc_() {
uint8_t switches1 = 0;
return this->read_byte(FUSB_SWITCHES1, &switches1) &&
this->write_byte(FUSB_SWITCHES1, switches1 | FUSB_SWITCHES1_AUTO_CRC);
}
void FUSB302B::handle_interrupt_() {
// STATUS0A, STATUS1A, INTERRUPTA, INTERRUPTB, STATUS0, STATUS1, INTERRUPT; reading clears the interrupts
uint8_t regs[7];
if (this->read_register(FUSB_STATUS0A, regs, sizeof(regs)) != i2c::ERROR_OK) {
this->enter_error_();
return;
}
const uint8_t interrupta = regs[2];
const uint8_t status0 = regs[4];
uint8_t status1 = regs[5];
const uint8_t interrupt = regs[6];
if ((interrupt & FUSB_INTERRUPT_I_VBUSOK) && !(status0 & FUSB_STATUS0_VBUSOK)) {
if (this->attached_)
this->detach_();
return;
}
if (interrupta & FUSB_INTERRUPTA_I_HARDRST) {
if (this->attached_)
this->handle_hard_reset_();
return;
}
if (interrupta & FUSB_INTERRUPTA_I_SOFTRST) {
ESP_LOGV(TAG, "Soft reset received");
}
if ((interrupta & FUSB_INTERRUPTA_I_RETRYFAIL) && this->attached_)
this->handle_transmit_failed_();
if (!this->attached_)
return;
PdMsg msg;
while (!(status1 & FUSB_STATUS1_RX_EMPTY)) {
bool is_sop = false;
if (!this->read_message_(msg, is_sop)) {
ESP_LOGW(TAG, "Reading message failed");
// A partial read leaves the FIFO misaligned
this->write_byte(FUSB_CONTROL1, FUSB_CONTROL1_RX_FLUSH);
return;
}
// Only SOP packets are addressed to a sink; SOP'/SOP'' are for cable plugs
if (is_sop)
this->handle_message_(msg);
if (!this->read_byte(FUSB_STATUS1, &status1))
return;
}
}
bool FUSB302B::read_message_(PdMsg &msg, bool &is_sop) {
uint8_t token = 0;
uint8_t header[2];
if (!this->read_byte(FUSB_FIFOS, &token) || this->read_register(FUSB_FIFOS, header, 2) != i2c::ERROR_OK)
return false;
msg.set_header(encode_uint16(header[1], header[0]));
// Data objects, followed by the CRC32 that the chip already checked
uint8_t buf[PD_MAX_NUM_DATA_OBJECTS * 4 + 4];
if (this->read_register(FUSB_FIFOS, buf, msg.num_of_obj * 4 + 4) != i2c::ERROR_OK)
return false;
for (uint8_t i = 0; i < msg.num_of_obj; i++) {
const uint8_t *obj = &buf[i * 4];
msg.data_objects[i] = encode_uint32(obj[3], obj[2], obj[1], obj[0]);
}
is_sop = (token & FUSB_FIFO_RX_TOKEN_BITS) == FUSB_FIFO_RX_SOP;
return true;
}
bool FUSB302B::send_message(const PdMsg &msg) {
// Register address, then the TX tokens; written in one transfer without the copy write_register() makes
uint8_t buf[1 + 4 + 1 + 2 + PD_MAX_NUM_DATA_OBJECTS * 4 + 4];
size_t len = 0;
const uint16_t header = msg.get_coded_header();
buf[len++] = FUSB_FIFOS;
buf[len++] = FUSB_TX_TOKEN_SYNC1;
buf[len++] = FUSB_TX_TOKEN_SYNC1;
buf[len++] = FUSB_TX_TOKEN_SYNC1;
buf[len++] = FUSB_TX_TOKEN_SYNC2;
buf[len++] = FUSB_TX_TOKEN_PACKSYM | (msg.num_of_obj * 4 + 2);
buf[len++] = header & 0xFF;
buf[len++] = header >> 8;
for (uint8_t i = 0; i < msg.num_of_obj; i++) {
const uint32_t obj = msg.data_objects[i];
buf[len++] = obj & 0xFF;
buf[len++] = (obj >> 8) & 0xFF;
buf[len++] = (obj >> 16) & 0xFF;
buf[len++] = obj >> 24;
}
buf[len++] = FUSB_TX_TOKEN_JAM_CRC;
buf[len++] = FUSB_TX_TOKEN_EOP;
buf[len++] = FUSB_TX_TOKEN_TXOFF;
buf[len++] = FUSB_TX_TOKEN_TXON;
i2c::ErrorCode err = this->write(buf, len);
if (err != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Sending message type %u failed (error %d)", msg.type, static_cast<int>(err));
return false;
}
return true;
}
bool FUSB302B::measure_cc_(uint8_t meas_switch, uint8_t &level) {
if (!this->write_byte(FUSB_SWITCHES0, FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | meas_switch))
return false;
vTaskDelay(pdMS_TO_TICKS(CC_SETTLE_MS));
uint8_t status0 = 0;
if (!this->read_byte(FUSB_STATUS0, &status0))
return false;
level = status0 & FUSB_STATUS0_BC_LVL;
for (uint8_t i = 0; i < CC_STABLE_READS; i++) {
if (!this->read_byte(FUSB_STATUS0, &status0))
return false;
if ((status0 & FUSB_STATUS0_BC_LVL) != level) {
level = CC_LEVEL_UNSTABLE;
return true;
}
}
return true;
}
void FUSB302B::try_attach_() {
if (this->state_ == PdState::PD_STATE_ERROR) {
if (!this->init_chip_())
return;
ESP_LOGI(TAG, "Communication restored");
this->set_state_(PdState::PD_STATE_DISCONNECTED);
}
uint8_t status0 = 0;
if (!this->write_byte(FUSB_POWER, FUSB_POWER_ALL) || !this->read_byte(FUSB_STATUS0, &status0)) {
this->enter_error_();
return;
}
// A source may present its CC pull-up before VBUS is on; detach is only detected when VBUS drops
if (!(status0 & FUSB_STATUS0_VBUSOK)) {
this->cc_ambiguous_logged_ = false;
return;
}
uint8_t cc1 = 0;
uint8_t cc2 = 0;
if (!this->write_byte(FUSB_SWITCHES1, FUSB_SWITCHES1_SPECREV_REV2) ||
!this->write_byte(FUSB_MEASURE, FUSB_MEASURE_MDAC_CC) || !this->measure_cc_(FUSB_SWITCHES0_MEAS_CC1, cc1) ||
!this->measure_cc_(FUSB_SWITCHES0_MEAS_CC2, cc2)) {
this->enter_error_();
return;
}
// Exactly one CC line carries the source pull-up; the other one is open
uint8_t meas_switch;
uint8_t tx_switch;
if (cc1 != CC_LEVEL_UNSTABLE && cc1 > 0 && cc2 == 0) {
meas_switch = FUSB_SWITCHES0_MEAS_CC1;
tx_switch = FUSB_SWITCHES1_TXCC1;
} else if (cc2 != CC_LEVEL_UNSTABLE && cc2 > 0 && cc1 == 0) {
meas_switch = FUSB_SWITCHES0_MEAS_CC2;
tx_switch = FUSB_SWITCHES1_TXCC2;
} else {
if (!this->cc_ambiguous_logged_) {
ESP_LOGD(TAG, "VBUS present but no single CC line with a source pull-up (CC1 %u, CC2 %u)", cc1, cc2);
this->cc_ambiguous_logged_ = true;
}
return;
}
if (!this->write_byte(FUSB_SWITCHES0, FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | meas_switch) ||
!this->write_byte(FUSB_SWITCHES1, tx_switch | FUSB_SWITCHES1_SPECREV_REV2) || !this->enable_auto_crc_() ||
!this->reset_pd_()) {
this->enter_error_();
return;
}
ESP_LOGD(TAG, "Attached on CC%u", meas_switch == FUSB_SWITCHES0_MEAS_CC1 ? 1 : 2);
this->attached_ = true;
this->cc_ambiguous_logged_ = false;
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT);
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
}
void FUSB302B::detach_() {
ESP_LOGD(TAG, "Detached (VBUS lost)");
this->attached_ = false;
this->waiting_for_source_caps_ = false;
this->reset_pd_();
this->clear_contract_();
this->set_state_(PdState::PD_STATE_DISCONNECTED);
}
void FUSB302B::handle_hard_reset_() {
ESP_LOGD(TAG, "Hard reset received");
// The source returns to Type-C default power and sends its capabilities again
// Also flushes the RX FIFO, so the caller has nothing left to read
if (!this->reset_pd_()) {
this->enter_error_();
return;
}
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT);
}
void FUSB302B::enter_error_() {
if (this->state_ == PdState::PD_STATE_ERROR)
return;
ESP_LOGW(TAG, "Communication with the chip failed");
this->attached_ = false;
this->waiting_for_source_caps_ = false;
this->clear_contract_();
this->set_state_(PdState::PD_STATE_ERROR);
}
void FUSB302B::send_soft_reset_() {
this->reset_pd_();
this->send_message(this->make_control_msg_(PD_CNTRL_SOFT_RESET));
// The source answers a soft reset with new source capabilities
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_SOFT_RESET_SENT);
}
void FUSB302B::start_source_cap_wait_(SourceCapStep step) {
this->waiting_for_source_caps_ = true;
this->source_cap_step_ = step;
this->source_cap_step_start_ = millis();
}
void FUSB302B::check_source_caps_() {
if (!this->waiting_for_source_caps_ || this->check_ams_())
return;
const uint32_t elapsed = millis() - this->source_cap_step_start_;
switch (this->source_cap_step_) {
case SourceCapStep::SOURCE_CAP_STEP_WAIT:
if (elapsed < SOURCE_CAP_WAIT_MS)
return;
ESP_LOGD(TAG, "No source capabilities received, requesting them");
this->send_message(this->make_control_msg_(PD_CNTRL_GET_SOURCE_CAP));
this->source_cap_step_ = SourceCapStep::SOURCE_CAP_STEP_GET_SENT;
break;
case SourceCapStep::SOURCE_CAP_STEP_GET_SENT:
if (elapsed < SOURCE_CAP_RETRY_MS)
return;
ESP_LOGD(TAG, "No source capabilities received, sending soft reset");
this->send_soft_reset_();
return;
case SourceCapStep::SOURCE_CAP_STEP_SOFT_RESET_SENT:
if (elapsed < SOURCE_CAP_RETRY_MS)
return;
ESP_LOGW(TAG, "PD negotiation failed, staying at 5 V");
this->waiting_for_source_caps_ = false;
this->set_state_(PdState::PD_STATE_PD_TIMEOUT);
return;
}
this->source_cap_step_start_ = millis();
}
} // namespace esphome::fusb302b
#endif // USE_ESP32
-74
View File
@@ -1,74 +0,0 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/static_task.h"
#include "pd.h"
namespace esphome::fusb302b {
/// FUSB302B USB Type-C / Power Delivery sink controller.
///
/// A dedicated task owns all I2C traffic to the chip so that PD replies meet the protocol timing
/// even while the main loop is busy.
class FUSB302B : public PowerDelivery, public Component, public i2c::I2CDevice {
public:
explicit FUSB302B(InternalGPIOPin *interrupt_pin) : interrupt_pin_(interrupt_pin) {}
void setup() override;
void dump_config() override;
void on_shutdown() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
enum class SourceCapStep : uint8_t {
SOURCE_CAP_STEP_WAIT,
SOURCE_CAP_STEP_GET_SENT,
SOURCE_CAP_STEP_SOFT_RESET_SENT,
};
static void gpio_intr(FUSB302B *arg);
static void task_func(void *arg);
bool send_message(const PdMsg &msg) override;
void on_state_changed() override {}
void on_soft_reset_received() override { this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT); }
void on_source_caps_received() override { this->waiting_for_source_caps_ = false; }
void on_transition_timeout() override { this->send_soft_reset_(); }
// PHY task only
[[noreturn]] void run_task_();
bool init_chip_();
bool reset_pd_();
bool enable_auto_crc_();
void handle_interrupt_();
bool read_message_(PdMsg &msg, bool &is_sop);
void try_attach_();
bool measure_cc_(uint8_t meas_switch, uint8_t &level);
void detach_();
void handle_hard_reset_();
void start_source_cap_wait_(SourceCapStep step);
void send_soft_reset_();
void check_source_caps_();
void enter_error_();
InternalGPIOPin *interrupt_pin_;
StaticTask task_;
// PHY task only
uint32_t source_cap_step_start_{0};
SourceCapStep source_cap_step_{SourceCapStep::SOURCE_CAP_STEP_WAIT};
bool attached_{false};
bool waiting_for_source_caps_{false};
bool cc_ambiguous_logged_{false};
};
} // namespace esphome::fusb302b
#endif // USE_ESP32
@@ -1,94 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::fusb302b {
// Register addresses
static constexpr uint8_t FUSB_DEVICE_ID = 0x01;
static constexpr uint8_t FUSB_SWITCHES0 = 0x02;
static constexpr uint8_t FUSB_SWITCHES1 = 0x03;
static constexpr uint8_t FUSB_MEASURE = 0x04;
static constexpr uint8_t FUSB_CONTROL0 = 0x06;
static constexpr uint8_t FUSB_CONTROL1 = 0x07;
static constexpr uint8_t FUSB_CONTROL3 = 0x09;
static constexpr uint8_t FUSB_MASK1 = 0x0A;
static constexpr uint8_t FUSB_POWER = 0x0B;
static constexpr uint8_t FUSB_RESET = 0x0C;
static constexpr uint8_t FUSB_MASKA = 0x0E;
static constexpr uint8_t FUSB_MASKB = 0x0F;
static constexpr uint8_t FUSB_STATUS0A = 0x3C;
static constexpr uint8_t FUSB_STATUS0 = 0x40;
static constexpr uint8_t FUSB_STATUS1 = 0x41;
static constexpr uint8_t FUSB_FIFOS = 0x43;
// DEVICE_ID: version in bits 7:4, product variant (I2C address 0x22-0x25) in bits 3:2, silicon revision in bits 1:0
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_MASK = 0xF0;
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_A = 0x80;
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_B = 0x90;
// SWITCHES0 bits
static constexpr uint8_t FUSB_SWITCHES0_MEAS_CC2 = 1 << 3;
static constexpr uint8_t FUSB_SWITCHES0_MEAS_CC1 = 1 << 2;
static constexpr uint8_t FUSB_SWITCHES0_PDWN_2 = 1 << 1;
static constexpr uint8_t FUSB_SWITCHES0_PDWN_1 = 1 << 0;
// SWITCHES1 bits
static constexpr uint8_t FUSB_SWITCHES1_SPECREV_REV2 = 0x01 << 5;
static constexpr uint8_t FUSB_SWITCHES1_AUTO_CRC = 1 << 2;
static constexpr uint8_t FUSB_SWITCHES1_TXCC2 = 1 << 1;
static constexpr uint8_t FUSB_SWITCHES1_TXCC1 = 1 << 0;
// MEASURE value used for CC detection
static constexpr uint8_t FUSB_MEASURE_MDAC_CC = 49;
// CONTROL0 bits
static constexpr uint8_t FUSB_CONTROL0_TX_FLUSH = 1 << 6;
// CONTROL1 bits
static constexpr uint8_t FUSB_CONTROL1_RX_FLUSH = 1 << 2;
// CONTROL3 bits
static constexpr uint8_t FUSB_CONTROL3_N_RETRIES_SHIFT = 1;
static constexpr uint8_t FUSB_CONTROL3_N_RETRIES_MASK = 0x3 << FUSB_CONTROL3_N_RETRIES_SHIFT;
static constexpr uint8_t FUSB_CONTROL3_AUTO_RETRY = 1 << 0;
// MASK1: mask the ACTIVITY, CRC_CHK and BC_LVL interrupts
static constexpr uint8_t FUSB_MASK1_DEFAULT = 0x51;
// POWER bits
static constexpr uint8_t FUSB_POWER_ALL = 0x0F;
// RESET bits
static constexpr uint8_t FUSB_RESET_PD_RESET = 1 << 1;
static constexpr uint8_t FUSB_RESET_SW_RES = 1 << 0;
// INTERRUPTA bits
static constexpr uint8_t FUSB_INTERRUPTA_I_RETRYFAIL = 1 << 4;
static constexpr uint8_t FUSB_INTERRUPTA_I_SOFTRST = 1 << 1;
static constexpr uint8_t FUSB_INTERRUPTA_I_HARDRST = 1 << 0;
// INTERRUPT bits
static constexpr uint8_t FUSB_INTERRUPT_I_VBUSOK = 1 << 7;
// STATUS0 bits
static constexpr uint8_t FUSB_STATUS0_VBUSOK = 1 << 7;
static constexpr uint8_t FUSB_STATUS0_BC_LVL = 0x03;
// STATUS1 bits
static constexpr uint8_t FUSB_STATUS1_RX_EMPTY = 1 << 5;
// RX FIFO token
static constexpr uint8_t FUSB_FIFO_RX_TOKEN_BITS = 0xE0;
static constexpr uint8_t FUSB_FIFO_RX_SOP = 0xE0;
// TX FIFO tokens
static constexpr uint8_t FUSB_TX_TOKEN_TXON = 0xA1;
static constexpr uint8_t FUSB_TX_TOKEN_SYNC1 = 0x12;
static constexpr uint8_t FUSB_TX_TOKEN_SYNC2 = 0x13;
static constexpr uint8_t FUSB_TX_TOKEN_PACKSYM = 0x80;
static constexpr uint8_t FUSB_TX_TOKEN_JAM_CRC = 0xFF;
static constexpr uint8_t FUSB_TX_TOKEN_EOP = 0x14;
static constexpr uint8_t FUSB_TX_TOKEN_TXOFF = 0xFE;
} // namespace esphome::fusb302b
-330
View File
@@ -1,330 +0,0 @@
#include "pd.h"
#include <cstring>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::fusb302b {
ESPHOME_LOG_TAG(TAG, "fusb302b.pd");
static constexpr uint32_t AMS_TIMEOUT_MS = 2000;
// tSinkRequest: delay before repeating a request that the source answered with Wait
static constexpr uint32_t SINK_REQUEST_MS = 100;
static constexpr uint8_t MAX_WAIT_RETRIES = 10;
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object flags; only the first (5 V) object carries them
static constexpr uint32_t SINK_PDO_HIGHER_CAPABILITY = 1u << 28;
static constexpr uint32_t SINK_PDO_USB_COMMS_CAPABLE = 1u << 26;
// Operational current advertised for every sink object: 3 A (300 x 10 mA)
static constexpr uint16_t SINK_PDO_CURRENT = 300;
// USB PD 6.4.1.2.3 Source Fixed Supply Power Data Object; other PDO types only report their type
static PdContract parse_pdo(uint32_t pdo) {
PdContract info{};
info.type = static_cast<PdPdoType>(pdo >> 30);
if (info.type == PD_PDO_TYPE_FIXED_SUPPLY) {
info.max_v = (pdo >> 10) & 0x3FF;
info.max_i = pdo & 0x3FF;
}
return info;
}
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object; voltage in 50 mV units, current in 10 mA units
static constexpr uint32_t sink_fixed_pdo(uint16_t voltage, uint16_t current) {
return (static_cast<uint32_t>(PD_PDO_TYPE_FIXED_SUPPLY) << 30) | (static_cast<uint32_t>(voltage & 0x3FF) << 10) |
(current & 0x3FF);
}
void PdMsg::set_header(uint16_t header) {
this->type = header & 0x1F;
this->spec_rev = static_cast<PdSpecRevision>((header >> 6) & 0x3);
this->id = (header >> 9) & 0x7;
this->num_of_obj = (header >> 12) & 0x7;
this->extended = (header >> 15) & 0x1;
}
uint16_t PdMsg::get_coded_header() const {
// Data role UFP (bit 5 = 0), power role sink (bit 8 = 0)
return static_cast<uint16_t>(this->type & 0x1F) | (static_cast<uint16_t>(this->spec_rev) << 6) |
(static_cast<uint16_t>(this->id & 0x7) << 9) | (static_cast<uint16_t>(this->num_of_obj & 0x7) << 12) |
(static_cast<uint16_t>(this->extended) << 15);
}
PdMsg PowerDelivery::make_control_msg_(PdControlMsgType type) const {
PdMsg msg;
msg.type = type;
msg.id = this->msg_counter_ % 8;
return msg;
}
PdMsg PowerDelivery::make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const {
PdMsg msg;
msg.type = type;
msg.id = this->msg_counter_ % 8;
msg.num_of_obj = len;
memcpy(msg.data_objects, objects, len * sizeof(uint32_t));
return msg;
}
void PowerDelivery::reset_protocol_() {
this->last_received_msg_id_ = 255;
this->msg_counter_ = 0;
this->active_ams_ = false;
this->ps_rdy_expected_ = false;
this->retry_request_pending_ = false;
this->wait_retries_ = 0;
}
void PowerDelivery::set_state_(PdState state) {
if (this->state_.exchange(state) != state)
this->on_state_changed();
}
void PowerDelivery::set_contract_(const PdContract &contract) {
this->accepted_contract_ = contract;
const uint32_t packed = PackedContract::from(contract).value;
if (this->contract_.exchange(packed) != packed)
this->on_state_changed();
}
void PowerDelivery::clear_contract_() {
this->accepted_contract_ = {};
if (this->contract_.exchange(0) != 0)
this->on_state_changed();
}
void PowerDelivery::set_state_and_contract_(PdState state, const PdContract &contract) {
this->accepted_contract_ = contract;
const uint32_t packed = PackedContract::from(contract).value;
// The contract is stored first, so a reader that sees the new state also sees the new contract
const bool contract_changed = this->contract_.exchange(packed) != packed;
const bool state_changed = this->state_.exchange(state) != state;
if (contract_changed || state_changed)
this->on_state_changed();
}
void PowerDelivery::set_ams_(bool active) {
this->active_ams_ = active;
// A new sequence has no accepted request yet, and an ended one can no longer complete
this->ps_rdy_expected_ = false;
if (active)
this->ams_start_ = millis();
}
bool PowerDelivery::check_ams_() {
if (this->active_ams_ && millis() - this->ams_start_ > AMS_TIMEOUT_MS)
this->set_ams_(false);
return this->active_ams_;
}
void PowerDelivery::check_request_retry_() {
if (!this->retry_request_pending_ || millis() - this->wait_received_ms_ < SINK_REQUEST_MS)
return;
if (this->send_request_()) {
this->retry_request_pending_ = false;
} else if (this->wait_retries_ < MAX_WAIT_RETRIES) {
// Try again after another tSinkRequest; failed attempts count against the same limit as Wait replies
this->wait_retries_++;
this->wait_received_ms_ = millis();
} else {
ESP_LOGW(TAG, "Giving up repeating the request");
this->retry_request_pending_ = false;
this->request_failed_();
}
}
bool PowerDelivery::send_request_() {
this->set_ams_(true);
if (this->send_message(this->make_data_msg_(PD_DATA_REQUEST, &this->last_rdo_, 1)))
return true;
ESP_LOGW(TAG, "Sending the request failed");
this->set_ams_(false);
return false;
}
void PowerDelivery::request_failed_() {
// A contract already in place stays valid. Without one, report that negotiation is over at default power.
if (this->state_ == PdState::PD_STATE_DEFAULT_CONTRACT)
this->set_state_(PdState::PD_STATE_PD_TIMEOUT);
}
void PowerDelivery::check_transition_timeout_() {
// The AMS that sent the request only ends on PS_RDY, so an ended AMS while in transition means a timeout
if (this->state_ != PdState::PD_STATE_TRANSITION || this->check_ams_())
return;
ESP_LOGW(TAG, "Source did not report the new supply ready, falling back to default power");
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
this->on_transition_timeout();
}
void PowerDelivery::handle_transmit_failed_() {
// USB PD 6.12.2.2: the counter also moves on after a failed transmission. The source may have received the
// message and only its GoodCRC was lost, so reusing the MessageID would make it drop the next one as a repeat.
ESP_LOGD(TAG, "Message %u not acknowledged", this->msg_counter_ % 8);
this->msg_counter_++;
}
void PowerDelivery::handle_message_(const PdMsg &msg) {
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_GOODCRC) {
// A GoodCRC carries the MessageID it acknowledges; a duplicate or stray one must not advance the counter
if (msg.id != this->msg_counter_ % 8) {
ESP_LOGD(TAG, "Ignoring GoodCRC for MessageID %u, expected %u", msg.id, this->msg_counter_ % 8);
return;
}
// Our last message was received, so the next one gets a new MessageID
this->msg_counter_++;
return;
}
// Soft_Reset always has MessageID 0, so it must not be dropped as a retransmission of the last message
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_SOFT_RESET) {
this->handle_soft_reset_(msg);
return;
}
// Retransmissions repeat the MessageID of a message that was already handled
if (msg.id == this->last_received_msg_id_)
return;
this->last_received_msg_id_ = msg.id;
if (msg.num_of_obj == 0) {
this->handle_control_message_(msg);
} else {
this->handle_data_message_(msg);
}
}
void PowerDelivery::handle_data_message_(const PdMsg &msg) {
if (msg.extended || msg.type != PD_DATA_SOURCE_CAP) {
// BIST and Vendor_Defined messages must not be answered with Reject, so unsupported data messages are ignored
ESP_LOGV(TAG, "Ignoring data message type %u%s", msg.type,
msg.extended ? LOG_STR_LITERAL(" (extended)") : LOG_STR_LITERAL(""));
return;
}
this->on_source_caps_received();
this->respond_to_source_caps_(msg);
}
void PowerDelivery::handle_control_message_(const PdMsg &msg) {
switch (msg.type) {
case PD_CNTRL_ACCEPT:
if (!this->check_ams_()) {
ESP_LOGD(TAG, "Ignoring Accept outside a request");
break;
}
this->ps_rdy_expected_ = true;
if (this->requested_contract_ != this->accepted_contract_)
this->set_state_and_contract_(PdState::PD_STATE_TRANSITION, this->requested_contract_);
break;
case PD_CNTRL_PS_RDY:
// check_ams_() first, so a PS_RDY that arrives after the sequence timed out is ignored too
if (!this->check_ams_() || !this->ps_rdy_expected_) {
ESP_LOGD(TAG, "Ignoring PS_RDY without an accepted request");
break;
}
this->set_ams_(false);
this->set_state_(PdState::PD_STATE_EXPLICIT_CONTRACT);
break;
case PD_CNTRL_REJECT:
if (!this->check_ams_()) {
ESP_LOGD(TAG, "Ignoring Reject outside a request");
break;
}
ESP_LOGW(TAG, "Source rejected the request");
this->set_ams_(false);
this->request_failed_();
break;
case PD_CNTRL_WAIT:
if (!this->check_ams_()) {
ESP_LOGD(TAG, "Ignoring Wait outside a request");
break;
}
this->set_ams_(false);
if (this->wait_retries_ < MAX_WAIT_RETRIES) {
ESP_LOGD(TAG, "Source asked to wait, repeating the request");
this->wait_retries_++;
this->retry_request_pending_ = true;
this->wait_received_ms_ = millis();
} else {
ESP_LOGW(TAG, "Source kept answering Wait, giving up");
this->request_failed_();
}
break;
case PD_CNTRL_PING:
case PD_CNTRL_NOT_SUPPORTED:
break;
case PD_CNTRL_GET_SINK_CAP: {
// 5 V first, then the requested voltage. Higher Capability tells the source that 5 V is not enough for
// full operation.
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
uint32_t pdos[2];
uint8_t count = 0;
pdos[count++] = sink_fixed_pdo(100, SINK_PDO_CURRENT) | SINK_PDO_USB_COMMS_CAPABLE |
(request_v > 100 ? SINK_PDO_HIGHER_CAPABILITY : 0);
if (request_v > 100)
pdos[count++] = sink_fixed_pdo(request_v, SINK_PDO_CURRENT);
this->send_message(this->make_data_msg_(PD_DATA_SINK_CAP, pdos, count));
break;
}
default:
// Messages are sent as PD 2.0, which has no Not_Supported message
this->send_message(this->make_control_msg_(PD_CNTRL_REJECT));
break;
}
}
void PowerDelivery::handle_soft_reset_(const PdMsg &msg) {
this->reset_protocol_();
this->last_received_msg_id_ = msg.id;
// If this fails the source escalates to a hard reset. The hook still runs, so the PHY starts waiting for source
// capabilities and recovers on its own if neither follows.
if (!this->send_message(this->make_control_msg_(PD_CNTRL_ACCEPT))) {
ESP_LOGW(TAG, "Accepting the soft reset failed");
}
// A soft reset keeps the power level, but the outcome of a transition in progress is unknown
if (this->state_ == PdState::PD_STATE_TRANSITION)
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
this->on_soft_reset_received();
}
void PowerDelivery::respond_to_source_caps_(const PdMsg &msg) {
// Pick the highest fixed supply PDO that does not exceed the requested voltage.
// The first PDO is always the 5 V fixed supply.
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
PdContract selected{};
uint8_t position = 0; // 1-based object position, 0 = none
for (uint8_t idx = 0; idx < msg.num_of_obj; idx++) {
PdContract info = parse_pdo(msg.data_objects[idx]);
if (info.type != PD_PDO_TYPE_FIXED_SUPPLY)
continue;
if (position == 0 || (info.max_v <= request_v && info.max_v > selected.max_v)) {
selected = info;
position = idx + 1;
}
}
// USB PD 6.4.2 Request Data Object: no USB suspend (bit 24), USB communications capable (bit 25)
uint32_t rdo = (1u << 24) | (1u << 25);
if (position == 0) {
ESP_LOGW(TAG, "No usable PDO in source capabilities, requesting 5 V");
// Object 1 (5 V fixed supply), 300 mA maximum, 100 mA operating current
selected = PdContract{PD_PDO_TYPE_FIXED_SUPPLY, 100, 30};
position = 1;
rdo |= (30u << 0) | (10u << 10);
} else {
// Request the full current the source offers
const uint32_t current = selected.max_i;
rdo |= (current << 0) | (current << 10);
}
rdo |= static_cast<uint32_t>(position) << 28;
ESP_LOGD(TAG, "Requesting PDO %u: %.2f V", position, selected.max_v * 0.05f);
this->requested_contract_ = selected;
this->last_rdo_ = rdo;
this->retry_request_pending_ = false;
this->wait_retries_ = 0;
// A repeat would come too late: the source resets if it gets no Request within tSenderResponse (24-30 ms)
if (!this->send_request_())
this->request_failed_();
}
} // namespace esphome::fusb302b
-179
View File
@@ -1,179 +0,0 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdint>
namespace esphome::fusb302b {
static constexpr uint8_t PD_MAX_NUM_DATA_OBJECTS = 7;
// Standard Power Range fixed supplies, which are the only ones this sink negotiates
static constexpr uint8_t MIN_REQUEST_VOLTAGE = 5;
static constexpr uint8_t MAX_REQUEST_VOLTAGE = 20;
enum PdSpecRevision : uint8_t {
PD_SPEC_REV_1 = 0,
PD_SPEC_REV_2 = 1,
PD_SPEC_REV_3 = 2,
};
enum PdDataMsgType : uint8_t {
PD_DATA_SOURCE_CAP = 0x01,
PD_DATA_REQUEST = 0x02,
PD_DATA_SINK_CAP = 0x04,
};
enum PdControlMsgType : uint8_t {
PD_CNTRL_GOODCRC = 0x01,
PD_CNTRL_ACCEPT = 0x03,
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,
PD_CNTRL_NOT_SUPPORTED = 0x10,
};
enum PdPdoType : uint8_t {
PD_PDO_TYPE_FIXED_SUPPLY = 0,
PD_PDO_TYPE_BATTERY = 1,
PD_PDO_TYPE_VARIABLE_SUPPLY = 2,
PD_PDO_TYPE_AUGMENTED = 3,
};
enum class PdState : uint8_t {
PD_STATE_DISCONNECTED,
/// Negotiation is over without a PD contract (no capabilities, or the request was not accepted); the source
/// supplies Type-C default power.
PD_STATE_PD_TIMEOUT,
PD_STATE_DEFAULT_CONTRACT,
PD_STATE_TRANSITION,
PD_STATE_EXPLICIT_CONTRACT,
PD_STATE_ERROR,
};
/// Returns true for the states in which a source is attached and supplying power.
inline bool is_connected_state(PdState state) {
return state == PdState::PD_STATE_DEFAULT_CONTRACT || state == PdState::PD_STATE_TRANSITION ||
state == PdState::PD_STATE_EXPLICIT_CONTRACT || state == PdState::PD_STATE_PD_TIMEOUT;
}
struct PdContract {
PdPdoType type{PD_PDO_TYPE_FIXED_SUPPLY};
uint16_t max_v{0}; // 50 mV units
uint16_t max_i{0}; // 10 mA units
bool operator==(const PdContract &other) const {
return this->max_v == other.max_v && this->max_i == other.max_i && this->type == other.type;
}
bool operator!=(const PdContract &other) const { return !(*this == other); }
};
// Type-C default power: 5 V (100 x 50 mV) at 500 mA (50 x 10 mA)
static constexpr PdContract DEFAULT_CONTRACT{PD_PDO_TYPE_FIXED_SUPPLY, 100, 50};
/// A contract packed into one word so it can be shared between tasks as an atomic.
/// 0 means there is no contract (nothing attached).
struct PackedContract {
uint32_t value{0};
static PackedContract from(const PdContract &contract) {
return {(static_cast<uint32_t>(contract.max_v) << 16) | contract.max_i};
}
bool is_set() const { return this->value != 0; }
float voltage() const { return (this->value >> 16) * 0.05f; }
float current() const { return (this->value & 0xFFFF) * 0.01f; }
};
struct PdMsg {
uint8_t type{0};
PdSpecRevision spec_rev{PD_SPEC_REV_2};
uint8_t id{0};
uint8_t num_of_obj{0};
bool extended{false};
uint32_t data_objects[PD_MAX_NUM_DATA_OBJECTS]{};
void set_header(uint16_t header);
uint16_t get_coded_header() const;
};
/// USB-PD sink protocol logic, independent of the PHY chip.
/// Everything except the getters and set_request_voltage() runs in the PHY task, which must also call
/// check_transition_timeout_() and check_request_retry_() regularly; the protocol has no timers of its own.
class PowerDelivery {
public:
void set_request_voltage(uint8_t voltage) {
this->request_voltage_ = std::clamp(voltage, MIN_REQUEST_VOLTAGE, MAX_REQUEST_VOLTAGE);
}
uint8_t get_request_voltage() const { return this->request_voltage_; }
PdState get_state() const { return this->state_; }
bool is_connected() const { return is_connected_state(this->state_); }
protected:
/// Returns false when the message could not be handed to the chip; the PHY logs the failure.
virtual bool send_message(const PdMsg &msg) = 0;
/// Called after state_ or contract_ changed, to let the main loop publish it.
virtual void on_state_changed() = 0;
/// Called after the source soft reset the protocol; it sends new source capabilities next.
virtual void on_soft_reset_received() = 0;
/// Called when source capabilities arrive, before the request for them is sent.
virtual void on_source_caps_received() = 0;
/// Called after an accepted request was not followed by PS_RDY in time and the state fell back to default power;
/// the PHY sends a soft reset so the source offers its capabilities again.
virtual void on_transition_timeout() = 0;
void handle_message_(const PdMsg &msg);
/// Called by the PHY when the chip gave up on a message after its retries without receiving a GoodCRC.
void handle_transmit_failed_();
void reset_protocol_();
void set_state_(PdState state);
void set_contract_(const PdContract &contract);
void clear_contract_();
/// Stores both before calling on_state_changed() once, so the main loop never sees the new state with the old
/// contract.
void set_state_and_contract_(PdState state, const PdContract &contract);
void set_ams_(bool active);
/// Returns true while an atomic message sequence is in progress; ends it after a timeout.
bool check_ams_();
/// Falls back to default power when the source accepted a request but did not report the new supply ready in time.
void check_transition_timeout_();
/// Sends the last request again once tSinkRequest has passed after the source answered Wait.
void check_request_retry_();
PdMsg make_control_msg_(PdControlMsgType type) const;
PdMsg make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const;
// Shared with the main loop
std::atomic<PdState> state_{PdState::PD_STATE_DISCONNECTED};
std::atomic<uint32_t> contract_{0};
std::atomic<uint8_t> request_voltage_{5};
// PHY task only
uint32_t ams_start_{0};
uint32_t wait_received_ms_{0};
bool retry_request_pending_{false};
private:
void handle_data_message_(const PdMsg &msg);
void handle_control_message_(const PdMsg &msg);
void handle_soft_reset_(const PdMsg &msg);
void respond_to_source_caps_(const PdMsg &msg);
bool send_request_();
void request_failed_();
PdContract requested_contract_{};
PdContract accepted_contract_{};
uint32_t last_rdo_{0};
bool active_ams_{false};
// Set by Accept; PS_RDY is only valid after the source accepted the request of the current sequence
bool ps_rdy_expected_{false};
uint8_t last_received_msg_id_{255};
uint8_t msg_counter_{0};
uint8_t wait_retries_{0};
};
} // namespace esphome::fusb302b
+11 -165
View File
@@ -2,8 +2,6 @@
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include "hon_climate.h"
@@ -14,26 +12,6 @@ using namespace esphome::uart;
namespace esphome::haier {
static bool vertical_axis_is_auto(climate::ClimateSwingMode swing_mode) {
return (swing_mode == climate::CLIMATE_SWING_VERTICAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
}
static bool horizontal_axis_is_auto(climate::ClimateSwingMode swing_mode) {
return (swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
}
static bool is_auto(hon_protocol::VerticalSwingMode mode) {
return (mode == hon_protocol::VerticalSwingMode::AUTO) || (mode == hon_protocol::VerticalSwingMode::AUTO_SPECIAL);
}
static bool is_auto(hon_protocol::HorizontalSwingMode mode) { return mode == hon_protocol::HorizontalSwingMode::AUTO; }
// A position the user just set wins over the appliance's echo of the old one for this long
constexpr uint32_t AIRFLOW_SELECT_DEBOUNCE_MS = 10000;
static bool in_debounce(uint32_t set_time, uint32_t now) {
return set_time != 0 && now - set_time < AIRFLOW_SELECT_DEBOUNCE_MS;
}
ESPHOME_LOG_TAG(TAG, "haier.climate");
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
@@ -111,60 +89,21 @@ bool HonClimate::get_quiet_mode_state() const {
}
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
if (!this->current_vertical_swing_.has_value()) {
return this->current_vertical_swing_;
}
// While the appliance swings, or right after the user chose a position, report the chosen position
if (is_auto(this->current_vertical_swing_.value()) ||
in_debounce(this->vertical_direction_set_time_, App.get_loop_component_start_time())) {
return this->settings_.last_vertiacal_swing;
}
return this->current_vertical_swing_;
}
};
void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) {
if (is_auto(direction)) {
ESP_LOGW(TAG, "Vertical AUTO mode is not allowed for airflow position selector");
return;
}
if (this->settings_.last_vertiacal_swing != direction) {
this->settings_.last_vertiacal_swing = direction;
this->hon_rtc_.save(&this->settings_);
}
this->pending_vertical_direction_ = direction;
this->force_send_control_ = true;
this->vertical_direction_set_time_ = App.get_loop_component_start_time();
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
}
esphome::optional<hon_protocol::HorizontalSwingMode> HonClimate::get_horizontal_airflow() const {
if (!this->current_horizontal_swing_.has_value()) {
return this->current_horizontal_swing_;
}
if (is_auto(this->current_horizontal_swing_.value()) ||
in_debounce(this->horizontal_direction_set_time_, App.get_loop_component_start_time())) {
return this->settings_.last_horizontal_swing;
}
return this->current_horizontal_swing_;
}
void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) {
if (is_auto(direction)) {
ESP_LOGW(TAG, "Horizontal AUTO mode is not allowed for airflow position selector");
return;
}
if (this->settings_.last_horizontal_swing != direction) {
this->settings_.last_horizontal_swing = direction;
this->hon_rtc_.save(&this->settings_);
}
this->pending_horizontal_direction_ = direction;
this->force_send_control_ = true;
this->horizontal_direction_set_time_ = App.get_loop_component_start_time();
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
}
const char *HonClimate::get_cleaning_status_text() const {
@@ -594,18 +533,6 @@ void HonClimate::initialization() {
} else {
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false};
}
bool settings_fixed = false;
if (is_auto(this->settings_.last_vertiacal_swing)) {
this->settings_.last_vertiacal_swing = hon_protocol::VerticalSwingMode::CENTER;
settings_fixed = true;
}
if (is_auto(this->settings_.last_horizontal_swing)) {
this->settings_.last_horizontal_swing = hon_protocol::HorizontalSwingMode::CENTER;
settings_fixed = true;
}
if (settings_fixed) {
this->hon_rtc_.save(&this->settings_);
}
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
@@ -741,21 +668,12 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
}
}
}
const ClimateSwingMode target_swing_mode = this->current_hvac_settings_.swing_mode.value_or(this->swing_mode);
if (this->pending_vertical_direction_.has_value()) {
if (!vertical_axis_is_auto(target_swing_mode)) {
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
} else {
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
}
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
this->pending_vertical_direction_.reset();
}
if (this->pending_horizontal_direction_.has_value()) {
if (!horizontal_axis_is_auto(target_swing_mode)) {
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
} else {
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
}
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
this->pending_horizontal_direction_.reset();
}
{
@@ -922,41 +840,6 @@ void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) {
}
#endif // USE_SWITCH
#ifdef USE_SELECT
template<typename Mode, size_t N>
static void publish_airflow_index(select::Select *sel, const std::array<Mode, N> &order, Mode mode) {
if (sel == nullptr) {
return;
}
// Saved settings never hold AUTO after initialization(); any other value outside the table shows as Center
size_t index = N;
size_t center = 0;
for (size_t i = 0; i < N; i++) {
const Mode entry = airflow_order_at(order, i);
if (entry == mode) {
index = i;
} else if (entry == Mode::CENTER) {
center = i;
}
}
if (index == N) {
index = center;
}
const auto active = sel->active_index();
if (active.has_value() && *active == index) {
return;
}
sel->publish_state(index);
}
void HonClimate::publish_airflow_selects_() {
publish_airflow_index(this->vertical_airflow_select_, VERTICAL_SWING_MODES_ORDER,
this->settings_.last_vertiacal_swing);
publish_airflow_index(this->horizontal_airflow_select_, HORIZONTAL_SWING_MODES_ORDER,
this->settings_.last_horizontal_swing);
}
#endif // USE_SELECT
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
@@ -1191,25 +1074,16 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
// Saving last known non auto mode for vertical and horizontal swing
this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode;
this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode;
const uint32_t now_ms = App.get_loop_component_start_time();
bool save_vertical = !in_debounce(this->vertical_direction_set_time_, now_ms) &&
!is_auto(this->current_vertical_swing_.value()) &&
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing);
bool save_horizontal = !in_debounce(this->horizontal_direction_set_time_, now_ms) &&
!is_auto(this->current_horizontal_swing_.value()) &&
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing);
if (save_vertical || save_horizontal) {
if (save_vertical) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
}
if (save_horizontal) {
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
}
bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) &&
(this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) &&
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) ||
((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) &&
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing));
if (save_settings) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
this->hon_rtc_.save(&this->settings_);
}
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
should_publish = should_publish || (old_swing_mode != this->swing_mode);
}
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
@@ -1385,10 +1259,8 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::SET_POINT,
buffer, 2);
}
ClimateSwingMode target_swing_mode = this->swing_mode;
// Vertical swing mode
if (climate_control.swing_mode.has_value()) {
target_swing_mode = climate_control.swing_mode.value();
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO};
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO};
switch (climate_control.swing_mode.value()) {
@@ -1414,32 +1286,6 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
vertical_swing_buf, 2);
}
// A swing change above already sent both axes from settings_.last_*, which set_*_airflow() updated
const bool swing_sent = climate_control.swing_mode.has_value();
if (this->pending_vertical_direction_.has_value()) {
if (vertical_axis_is_auto(target_swing_mode)) {
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
} else if (!swing_sent) {
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) this->pending_vertical_direction_.value()};
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
vertical_swing_buf, 2);
}
this->pending_vertical_direction_.reset();
}
if (this->pending_horizontal_direction_.has_value()) {
if (horizontal_axis_is_auto(target_swing_mode)) {
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
} else if (!swing_sent) {
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) this->pending_horizontal_direction_.value()};
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
horizontal_swing_buf, 2);
}
this->pending_horizontal_direction_.reset();
}
// Fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
-38
View File
@@ -1,6 +1,5 @@
#pragma once
#include <array>
#include <chrono>
#include <queue>
#ifdef USE_SENSOR
@@ -15,36 +14,12 @@
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#include "esphome/core/automation.h"
#include "esphome/core/hal.h"
#include "haier_base.h"
#include "hon_packet.h"
namespace esphome::haier {
#ifdef USE_SELECT
// Positions in the order the select platform lists them; in flash on ESP8266, so read with airflow_order_at()
inline constexpr std::array<hon_protocol::VerticalSwingMode, 7> VERTICAL_SWING_MODES_ORDER PROGMEM{
hon_protocol::VerticalSwingMode::HEALTH_UP, hon_protocol::VerticalSwingMode::MAX_UP,
hon_protocol::VerticalSwingMode::UP, hon_protocol::VerticalSwingMode::CENTER,
hon_protocol::VerticalSwingMode::DOWN, hon_protocol::VerticalSwingMode::MAX_DOWN,
hon_protocol::VerticalSwingMode::HEALTH_DOWN,
};
inline constexpr std::array<hon_protocol::HorizontalSwingMode, 5> HORIZONTAL_SWING_MODES_ORDER PROGMEM{
hon_protocol::HorizontalSwingMode::MAX_LEFT, hon_protocol::HorizontalSwingMode::LEFT,
hon_protocol::HorizontalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::RIGHT,
hon_protocol::HorizontalSwingMode::MAX_RIGHT,
};
template<typename Mode, size_t N> Mode airflow_order_at(const std::array<Mode, N> &order, size_t index) {
static_assert(sizeof(Mode) == 1);
return static_cast<Mode>(progmem_read_byte(reinterpret_cast<const uint8_t *>(order.data() + index)));
}
#endif
enum class CleaningState : uint8_t {
NO_CLEANING = 0,
SELF_CLEAN = 1,
@@ -125,16 +100,6 @@ class HonClimate final : public HaierClimateBase {
protected:
switch_::Switch *beeper_switch_{nullptr};
switch_::Switch *quiet_mode_switch_{nullptr};
#endif
#ifdef USE_SELECT
public:
void set_vertical_airflow_select(select::Select *sel) { this->vertical_airflow_select_ = sel; }
void set_horizontal_airflow_select(select::Select *sel) { this->horizontal_airflow_select_ = sel; }
protected:
void publish_airflow_selects_();
select::Select *vertical_airflow_select_{nullptr};
select::Select *horizontal_airflow_select_{nullptr};
#endif
public:
HonClimate();
@@ -230,9 +195,6 @@ class HonClimate final : public HaierClimateBase {
uint8_t big_data_counter_{0};
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
// millis() of the last user-set position per axis, 0 when none
uint32_t vertical_direction_set_time_{0};
uint32_t horizontal_direction_set_time_{0};
HonSettings settings_{};
ESPPreferenceObject hon_rtc_;
SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF};
@@ -1,67 +0,0 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from ..climate import (
CONF_HAIER_ID,
CONF_HORIZONTAL_AIRFLOW,
CONF_VERTICAL_AIRFLOW,
HonClimate,
haier_ns,
)
CODEOWNERS = ["@paveldn"]
VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select)
HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select)
# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [
"Health Up",
"Max Up",
"Up",
"Center",
"Down",
"Max Down",
"Health Down",
]
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = [
"Max Left",
"Left",
"Center",
"Right",
"Max Right",
]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema(
VerticalAirflowSelect,
icon="mdi:arrow-expand-vertical",
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema(
HorizontalAirflowSelect,
icon="mdi:arrow-expand-horizontal",
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_VERTICAL_AIRFLOW,
hub.set_vertical_airflow_select,
options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS,
)
await selects(
CONF_HORIZONTAL_AIRFLOW,
hub.set_horizontal_airflow_select,
options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS,
)
@@ -1,15 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class HorizontalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -1,15 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class VerticalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
-141
View File
@@ -1,141 +0,0 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_DATA,
CONF_ID,
CONF_ON_MESSAGE,
CONF_PIN,
CONF_SOURCE,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RP2,
PLATFORM_RTL87XX,
)
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@JosVanEijndhoven"]
DOMAIN = "hdmi_cec"
MAX_LOGICAL_ADDRESS = 15
MAX_PAYLOAD_LENGTH = 15
CONF_PROMISCUOUS_MODE = "promiscuous_mode"
CONF_MONITOR_MODE = "monitor_mode"
CONF_DESTINATION = "destination"
CONF_OPCODE = "opcode"
hdmi_cec_ns = cg.esphome_ns.namespace("hdmi_cec")
HDMICEC = hdmi_cec_ns.class_("HDMICEC", cg.Component)
_PAYLOAD_SPAN = cg.std_span.template(cg.uint8.operator("const"))
_PAYLOAD_VECTOR = cg.std_vector.template(cg.uint8)
_MESSAGE_ARGS = [
(cg.uint8, "source"),
(cg.uint8, "destination"),
(_PAYLOAD_SPAN, "data"),
]
_request_message_callback_slot = cg.slot_counter("HDMI_CEC_MESSAGE_CALLBACK_COUNT")
logical_address = cv.int_range(min=0, max=MAX_LOGICAL_ADDRESS)
payload = cv.All(cv.ensure_list(cv.hex_uint8_t), cv.Length(max=MAX_PAYLOAD_LENGTH))
payload_prefix = cv.All(payload, cv.Length(min=1))
def _validate_message_filter(config: ConfigType) -> ConfigType:
# The opcode is the first payload byte: fold it into the data prefix
if (opcode := config.pop(CONF_OPCODE, None)) is not None:
data = config.setdefault(CONF_DATA, [opcode])
if data[0] != opcode:
raise cv.Invalid(
f"opcode 0x{opcode:02X} differs from the first data byte 0x{data[0]:02X}"
)
return config
def _message_filter(conf: ConfigType) -> str | None:
"""The C++ condition an on_message automation runs under, or None for every frame."""
checks = []
if (source := conf.get(CONF_SOURCE)) is not None:
checks.append(f"source == {source}")
if (destination := conf.get(CONF_DESTINATION)) is not None:
checks.append(f"destination == {destination}")
if data := conf.get(CONF_DATA):
checks.append(f"data.size() >= {len(data)}")
checks.extend(f"data[{i}] == 0x{byte:02X}" for i, byte in enumerate(data))
return " && ".join(checks) or None
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HDMICEC),
cv.Required(CONF_PIN): pins.internal_gpio_input_pullup_pin_schema,
cv.Required(CONF_ADDRESS): logical_address,
cv.Optional(CONF_PROMISCUOUS_MODE, default=False): cv.boolean,
cv.Optional(CONF_MONITOR_MODE, default=False): cv.boolean,
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{
cv.Optional(CONF_SOURCE): logical_address,
cv.Optional(CONF_DESTINATION): logical_address,
cv.Optional(CONF_OPCODE): cv.hex_uint8_t,
cv.Optional(CONF_DATA): payload_prefix,
},
_validate_message_filter,
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(
[
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
PLATFORM_LN882X,
]
),
)
async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
# The acknowledge bit is released from a general purpose timer, left out of the build by default
include_builtin_idf_component("esp_driver_gptimer")
pin = await cg.gpio_pin_expression(config[CONF_PIN])
var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_ADDRESS])
await cg.register_component(var, config)
if config[CONF_PROMISCUOUS_MODE]:
cg.add(var.set_promiscuous_mode(True))
if config[CONF_MONITOR_MODE]:
cg.add(var.set_monitor_mode(True))
for conf in config.get(CONF_ON_MESSAGE, []):
_request_message_callback_slot()
await automation.build_callback_automation(
var,
"add_on_message_callback",
_MESSAGE_ARGS,
conf,
when=_message_filter(conf),
)
automation.register_apply_action(
"hdmi_cec.send",
cv.Schema(
{
cv.GenerateID(): cv.use_id(HDMICEC),
cv.Required(CONF_DESTINATION): cv.templatable(logical_address),
cv.Required(CONF_DATA): cv.templatable(payload),
}
),
automation.ApplyCall(
"send({}, {})",
((CONF_DESTINATION, cg.uint8), (CONF_DATA, _PAYLOAD_VECTOR)),
),
)
-484
View File
@@ -1,484 +0,0 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <map>
#include <array>
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#ifdef HDMI_CEC_USE_DECODER
#include "hdmi_cec.h"
#include "cec_decoder.h"
namespace esphome {
namespace hdmi_cec {
const std::array<const char *, 0x77> Decoder::UI_COMMANDS = {
/* 0x00 = */ "Select",
"Up",
"Down",
"Left",
"Right",
"Right-Up",
"Right-Down",
"Left-Up",
/* 0x08 = */ "Left-Down",
"Root Menu",
"Setup Menu",
"Contents Menu",
"Favorite Menu",
"Exit",
"Reserved",
"Reserved",
/* 0x10 = */ "Media Top Menu",
"Media Context-sensitive Menu",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
/* 0x18 = */ "Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Number Entry Mode",
"11",
"12",
/* 0x20 = */ "0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
/* 0x28 = */ "8",
"9",
"Dot",
"Enter",
"Clear",
"Reserved",
"Reserved",
"Next Favorite",
/* 0x30 = */ "Channel Up",
"Channel Down",
"Previous Channel",
"Sound Select",
"Input Select",
"Display Information",
"Help",
"Page Up",
/* 0x38 = */ "Page Down",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
/* 0x40 = */ "Power",
"Volume Up",
"Volume Down",
"Mute",
"Play",
"Stop",
"Pause",
"Record",
/* 0x48 = */ "Rewind",
"Fast forward",
"Eject",
"Forward",
"Backward",
"Stop-Record",
"Pause-Record",
"Reserved",
/* 0x50 = */ "Angle",
"Sub picture",
"Video on Demand",
"Electronic Program Guide",
"Timer Programming",
"Initial Configuration",
"Select Broadcast Type",
"Select Sound Presentation",
/* 0x58 = */ "Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
/* 0x60 = */ "Play Function",
"Pause-Play Function",
"Record Function",
"Pause-Record Function",
"Stop Function",
"Mute Function",
"Restore Volume Function",
"Tune Function",
/* 0x68 = */ "Select Media Function",
"Select A/V Input Function",
"Select Audio Input Function",
"Power Toggle Function",
"Power Off Function",
"Power On Function",
"Reserved",
"Reserved",
/* 0x70 = */ "Reserved",
"F1 (Blue)",
"F2 (Red)",
"F3 (Green)",
"F4 (Yellow)",
"F5",
"Data"};
// See 'short audio descriptor' in https://en.wikipedia.org/wiki/Extended_Display_Identification_Data
const std::array<const char *, 0x11> Decoder::AUDIO_FORMATS = {
"reserved", "LPCM", "AC3", "MPEG-1", "MP3", "MPEG-2", "AAC", "DTS", "ATRAC",
"DSD", "DD+", "DTS-HD", "MAT/Dolby TrueHD", "DST Audio", "WMA Pro", "Extension?"};
const std::array<const char *, 8> Decoder::AUDIO_SAMPLERATES = {"32", "44.1", "48", "88",
"96", "176", "192", "Reserved"};
template<uint32_t OPERANDS> bool Decoder::do_operand_() {
if (OPERANDS <= 0xFF) {
// generic function called for single operand of unkown type and length
return append_operand_(".");
} else {
// generic function to handle two or more operands
return do_operand_<OPERANDS & 0xFF>() && do_operand_<(OPERANDS >> 8u)>();
}
}
#define APPEND_LINE(...) (length_ = buf_append_printf(line_, size_, length_, __VA_ARGS__))
/**
* List of specialised operand decode functions, one function per operand type.
* (Not fully complete, might be extended later)
*/
template<> bool Decoder::do_operand_<Decoder::NONE>() { return false; }
template<> bool Decoder::do_operand_<Decoder::ABORT_REASON>() {
const static std::array<const char *, 6> NAMES = {
"Unrecognized opcode", "Not in correct mode to respond", "Cannot provide source", "Invalid operand", "Refused",
"Unable to determine"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::AUDIO_FORMAT>() {
// this type of operand comes in a sequence, until exhausted
bool ok = true;
while (ok && (offset_ < frame_.size())) {
ok = append_operand_<AUDIO_FORMATS.size()>(AUDIO_FORMATS);
}
return ok;
}
template<> bool Decoder::do_operand_<Decoder::AUDIO_STATUS>() {
char line[20];
if (offset_ < frame_.size()) {
uint8_t field = frame_[offset_];
std::snprintf(line, sizeof(line), "Mute=%d,Vol=%02X", (field >> 7), (field & 0x7f));
return append_operand_(line);
} else {
return append_operand_("?");
}
}
template<> bool Decoder::do_operand_<Decoder::DEVICE_TYPE>() {
const static std::array<const char *, 8> NAMES = {"TV", "Recording Device", "Reserved",
"Tuner", "Playback Device", "Audio System",
"CEC Switch", "Video Processor"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::DISPLAY_CONTROL>() {
const static std::array<const char *, 4> NAMES = {"Default time", "Until cleared", "Clear previous", "Reserved"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::FEATURE_OPCODE>() {
if (offset_ >= frame_.size()) {
return false;
}
uint8_t opcode = frame_[offset_];
return append_operand_(find_opcode_name_(opcode));
}
template<> bool Decoder::do_operand_<Decoder::OSD_STRING>() {
char line[20]; // frame size() is at most 16
unsigned int i;
for (i = 0; i < frame_.size() - offset_; i++) {
line[i] = (char) (frame_[offset_ + i]);
}
line[i] = '\0'; // append '\0' termination: that is likely NOT part of the frame
offset_ = frame_.size(); // no useful data further in this frame
return append_operand_(line);
}
template<> bool Decoder::do_operand_<Decoder::PHYSICAL_ADDRESS>() {
// Exception: if this is an operand of <System Audio Mode Request> 0x70, then this operand is
// merely optional, and its absence means 'Off'
if (frame_.at(1) == 0x70 && offset_ >= frame_.size()) {
return append_operand_("Off");
}
if (offset_ + 1 >= frame_.size()) {
return append_operand_("?", 2);
}
char line[12];
std::snprintf(line, sizeof(line), "%1x.%1x.%1x.%1x", (frame_[offset_] >> 4) & 0xF, frame_[offset_] & 0xF,
(frame_[offset_ + 1] >> 4) & 0xF, frame_[offset_ + 1] & 0xF);
return append_operand_(line, 2);
}
template<> bool Decoder::do_operand_<Decoder::POWER_STATUS>() {
const static std::array<const char *, 4> NAMES = {"On", "Standby", "Standby->On", "On->Standby"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::SHORT_AUDIO_DESCRIPTOR>() {
// The frame can have a sequence of these operands, count is not fixed;
// each such operand takes 3 bytes in the frame.
// For a specification of this "Short Audio descriptor" format, see the Wikipedia page on
// "Extended Display Identification Data (EDID)", section "Audio Data Blocks".
std::array<char, 100> line;
uint32_t pos = 0;
bool ok = true;
while (ok && (offset_ + 2 < frame_.size())) {
const uint8_t descriptor = frame_[offset_];
uint8_t format = (descriptor >> 3) & 0x0F;
pos = buf_append_printf(&line[0], line.size(), 0, "%s", AUDIO_FORMATS[format]);
pos = buf_append_printf(&line[0], line.size(), pos, ",num_channels=%d", (descriptor & 0x07));
uint8_t rates = frame_[offset_ + 1];
for (int bit = 0; rates; bit++, rates >>= 1) {
if (rates & 0x1) {
// show support of various audio sample rates
pos = buf_append_printf(&line[0], line.size(), pos, ",%skHz", AUDIO_SAMPLERATES[bit]);
}
}
if (format == 1) {
// for LPCM format
uint8_t widths = frame_[offset_ + 2] & 0x7;
for (int i = 0; widths; i++, widths >>= 1) {
if (widths & 0x1) {
// show support of audio samble bit widths of 16, 20, and/or 24
pos = buf_append_printf(&line[0], line.size(), pos, ",%dbits", (16 + 4 * i));
}
}
}
ok = append_operand_(line.data(), 3);
}
// Note: Further descriptor 'extensions' not yet decoded
return ok;
}
template<> bool Decoder::do_operand_<Decoder::SYSTEM_AUDIO_STATUS>() {
const static std::array<const char *, 2> NAMES = {"Off", "On"};
return append_operand_<NAMES.size()>(NAMES);
}
template<> bool Decoder::do_operand_<Decoder::UI_COMMAND>() {
uint8_t command = frame_[offset_];
bool ok = append_operand_<UI_COMMANDS.size()>(UI_COMMANDS);
if (!ok) {
return false;
}
// out of the 100+ UI commands, a few exceptional UI commands have appended an extra parameter:
switch (command) {
case 0x56:
return do_operand_<UI_BROADCAST_TYPE>();
case 0x57:
return do_operand_<UI_SOUND_PRESENTATION_CONTROL>();
case 0x60:
return do_operand_<PLAY_MODE>();
case 0x67:
return do_operand_<CHANNEL_IDENTIFIER>();
case 0x68:
return do_operand_<UI_FUNCTION_MEDIA>();
case 0x69:
return do_operand_<UI_FUNCTION_SELECT_AV_INPUT>();
case 0x6A:
return do_operand_<UI_FUNCTION_SELECT_AUDIO_INPUT>();
default:
return ok;
}
}
template<> bool Decoder::do_operand_<Decoder::VENDOR_ID>() {
if (offset_ + 2 >= frame_.size()) {
return append_operand_("?", 3);
}
uint32_t id = (uint32_t) (frame_[offset_]) << 16 | (uint32_t) (frame_[offset_ + 1]) << 8 | frame_[offset_ + 2];
const char *const *name_lookup = VENDOR_IDS.find(id);
const char *vendor_name = name_lookup ? (*name_lookup) : nullptr;
if (!vendor_name) {
// if the hdmi-cec vendor id is not in our list, the id value itself is printed.
char line[12];
std::snprintf(line, sizeof(line), "ID=%06x", id);
return append_operand_(line, 3);
}
return append_operand_(vendor_name, 3);
}
template<> bool Decoder::do_operand_<Decoder::CEC_VERSION>() {
const static std::array<const char *, 9> NAMES = {"?", "1.2", "1.2a", "1.3", "1.3a", "1.4", "2.0", "2.x", "2.x"};
return append_operand_<NAMES.size()>(NAMES);
}
const Decoder::CecOpcodeTable Decoder::CEC_OPCODE_TABLE = {
{// Must be given sorted on increasing opcode:
// opcode, name, function pointer with specification of its operands
{0x00, {"Feature Abort", &Decoder::do_operand_<two(FEATURE_OPCODE, ABORT_REASON)>}},
{0x04, {"Image View On", &Decoder::do_operand_<NONE>}},
{0x05, {"Tuner Step Increment", &Decoder::do_operand_<NONE>}},
{0x06, {"Tuner Step Decrement", &Decoder::do_operand_<NONE>}},
{0x07, {"Tuner Device Status", &Decoder::do_operand_<TUNER_DEVICE_INFO>}},
{0x08, {"Give Tuner Device Status", &Decoder::do_operand_<STATUS_REQUEST>}},
{0x09, {"Record On", &Decoder::do_operand_<RECORD_SOURCE>}},
{0x0A, {"Record Status", &Decoder::do_operand_<RECORD_STATUS_INFO>}},
{0x0B, {"Record Off", &Decoder::do_operand_<NONE>}},
{0x0D, {"Text View On", &Decoder::do_operand_<NONE>}},
{0x0F, {"Record TV Screen", &Decoder::do_operand_<NONE>}},
{0x1A, {"Give Deck Status", &Decoder::do_operand_<STATUS_REQUEST>}},
{0x1B, {"Deck Status", &Decoder::do_operand_<DECK_INFO>}},
{0x32, {"Set Menu Language", &Decoder::do_operand_<LANGUAGE>}},
{0x33, {"Clear Analogue Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x34, {"Set Analogue Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x35, {"Timer Status", &Decoder::do_operand_<TIMER_STATUS_DATA>}},
{0x36, {"Standby", &Decoder::do_operand_<NONE>}},
{0x41, {"Play", &Decoder::do_operand_<PLAY_MODE>}},
{0x42, {"Deck Control", &Decoder::do_operand_<DECK_CONTROL_MODE>}},
{0x43, {"Timer Cleared Status", &Decoder::do_operand_<TIMER_CLEARED_STATUS_DATA>}},
{0x44, {"User Control Pressed", &Decoder::do_operand_<UI_COMMAND>}},
{0x45, {"User Control Released", &Decoder::do_operand_<NONE>}},
{0x46, {"Give OSD Name", &Decoder::do_operand_<NONE>}},
{0x47, {"Set OSD Name", &Decoder::do_operand_<OSD_NAME>}},
{0x64, {"Set OSD String", &Decoder::do_operand_<two(DISPLAY_CONTROL, OSD_STRING)>}},
{0x67, {"Set Timer Program Title", &Decoder::do_operand_<PROGRAM_TITLE_STRING>}},
{0x70, {"System Audio Mode Request", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x71, {"Give Audio Status", &Decoder::do_operand_<NONE>}},
{0x72, {"Set System Audio Mode", &Decoder::do_operand_<SYSTEM_AUDIO_STATUS>}},
{0x7A, {"Report Audio Status", &Decoder::do_operand_<AUDIO_STATUS>}},
{0x7D, {"Give System Audio Mode Status", &Decoder::do_operand_<NONE>}},
{0x7E, {"System Audio Mode Status", &Decoder::do_operand_<SYSTEM_AUDIO_STATUS>}},
{0x80, {"Routing Change", &Decoder::do_operand_<two(PHYSICAL_ADDRESS, PHYSICAL_ADDRESS)>}},
{0x81, {"Routing Information", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x82, {"Active Source", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x83, {"Give Physical Address", &Decoder::do_operand_<NONE>}},
{0x84, {"Report Physical Address", &Decoder::do_operand_<two(PHYSICAL_ADDRESS, DEVICE_TYPE)>}},
{0x85, {"Request Active Source", &Decoder::do_operand_<NONE>}},
{0x86, {"Set Stream Path", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x87, {"Device Vendor ID", &Decoder::do_operand_<VENDOR_ID>}},
{0x89, {"Vendor Command", &Decoder::do_operand_<VENDOR_SPECIFIC_DATA>}},
{0x8A, {"Vendor Remote Button Down", &Decoder::do_operand_<VENDOR_SPECIFIC_RC_CODE>}},
{0x8B, {"Vendor Remote Button Up", &Decoder::do_operand_<NONE>}},
{0x8C, {"Give Device Vendor ID", &Decoder::do_operand_<NONE>}},
{0x8D, {"Menu Request", &Decoder::do_operand_<MENU_REQUEST_TYPE>}},
{0x8E, {"Menu Status", &Decoder::do_operand_<MENU_STATE>}},
{0x8F, {"Give Device Power Status", &Decoder::do_operand_<NONE>}},
{0x90, {"Report Power Status", &Decoder::do_operand_<POWER_STATUS>}},
{0x91, {"Get Menu Language", &Decoder::do_operand_<NONE>}},
{0x92,
{"Select Analogue Service",
&Decoder::do_operand_<three(ANALOG_BROADCAST_TYPE, ANALOG_FREQUENCY, BROADCAST_SYSTEM)>}},
{0x93, {"Select Digital Service", &Decoder::do_operand_<DIGITAL_SERVICE_IDENTIFICATION>}},
{0x9A, {"Set Audio Rate", &Decoder::do_operand_<AUDIO_RATE>}},
{0x9D, {"Inactive Source", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
{0x97, {"Set Digital Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x99, {"Clear Digital Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0x9E, {"CEC Version", &Decoder::do_operand_<CEC_VERSION>}},
{0x9F, {"Get CEC Version", &Decoder::do_operand_<NONE>}},
{0xA0, {"Vendor Command With ID", &Decoder::do_operand_<two(VENDOR_ID, VENDOR_SPECIFIC_DATA)>}},
{0xA1, {"Clear External Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0xA2, {"Set External Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
{0xA3, {"Report Short Audio Descriptor", &Decoder::do_operand_<SHORT_AUDIO_DESCRIPTOR>}},
{0xA4, {"Request Short Audio Descriptor", &Decoder::do_operand_<AUDIO_FORMAT>}},
{0xC0, {"Initiate ARC", &Decoder::do_operand_<NONE>}},
{0xC1, {"Report ARC Initiated", &Decoder::do_operand_<NONE>}},
{0xC2, {"Report ARC Terminated", &Decoder::do_operand_<NONE>}},
{0xC3, {"Request ARC Initiation", &Decoder::do_operand_<NONE>}},
{0xC4, {"Request ARC Termination", &Decoder::do_operand_<NONE>}},
{0xC5, {"Terminate ARC", &Decoder::do_operand_<NONE>}},
{0xF8, {"CDC Message", &Decoder::do_operand_<NONE>}},
{0xFF, {"Abort", &Decoder::do_operand_<NONE>}}}};
const Decoder::VendorIdTable Decoder::VENDOR_IDS = {
{// Must be given sorted on increasing id:
{0x000039, "Toshiba"}, {0x0000F0, "Samsung"}, {0x0005CD, "Denon"}, {0x000678, "Maranz"},
{0x000982, "Loewe"}, {0x0009B0, "Onkyo"}, {0x000CB8, "Medion"}, {0x000CE7, "Toshiba"},
{0x0010FA, "Apple"}, {0x001582, "Pulse Eight"}, {0x001950, "Harman Kardon"}, {0x001A11, "Google"},
{0x0020C7, "Akai"}, {0x002467, "AOC"}, {0x008045, "Panasonic"}, {0x00903E, "Philips"},
{0x009053, "Daewoo"}, {0x00A0DE, "Yamaha"}, {0x00D0D5, "Grundig"}, {0x00E036, "Pioneer"},
{0x00E091, "LG"}, {0x08001F, "Sharp"}, {0x080046, "Sony"}, {0x18C086, "Broadcom"},
{0x534850, "Sharp"}, {0x6B746D, "Vizio"}, {0x8065E9, "Benq"}, {0x9C645E, "Harman Kardon"}}};
void Decoder::address_decode_() {
const static std::array<const char *, 16> NAMES = {"TV", "RecordingDev1", "RecordingDev2", "Tuner1",
"PlaybackDev1", "AudioSystem", "Tuner2", "Tuner3",
"PlaybackDev2", "RecordingDev3", "Tuner4", "PlaybackDev3",
"Reserved", "Reserved", "SpecificUse", "Unregistered"};
const char *dst = (frame_.is_broadcast()) ? "All" : NAMES[frame_.destination_addr()];
const char *src = NAMES[frame_.initiator_addr()];
APPEND_LINE("%s to %s: ", src, dst);
}
const char *Decoder::find_opcode_name_(uint32_t opcode) const {
const FrameType *frametype = CEC_OPCODE_TABLE.find(opcode);
if (!frametype)
return "?";
return frametype->name_;
}
/**
* Helper function to implement the 'do_operand' methods, to gather a textual representation.
* @return true if a further operand can be decoded, false otherwise
*/
bool Decoder::append_operand_(const char *word, uint8_t offset_incr /* default 1 */) {
APPEND_LINE("[%s]", word);
offset_ += offset_incr;
return (length_ < size_) && (offset_ < frame_.size());
}
/**
* Entry function 'decode' to call for full decode of a CEC frame
*/
int Decoder::decode() {
// prepare text buffer state variables for an empty line
length_ = 0; // currently accumulated length of text of operands
line_[length_] = '\0'; // initialise output text to empty string
if (!line_ || !size_)
return 0;
// print src and dest fields into 'line_'
address_decode_();
// print opcode field
const FrameType *frametype = nullptr;
if (frame_.size() <= 1) {
// Missing frame operation field?
APPEND_LINE("Ping");
} else {
// find operation name
frametype = CEC_OPCODE_TABLE.find(frame_.opcode());
const char *opname = frametype ? frametype->name_ : "?";
APPEND_LINE("<%s>", opname);
}
// convert operand fields to text:
if (frametype) {
offset_ = 2; // location in frame of first operand, after address-byte and opcode-byte
OperandDecode_f f = frametype->decode_f_;
(this->*f)(); // call one of the above 'do_operand_<xx>' methods
}
return length_;
}
} // namespace hdmi_cec
} // namespace esphome
#endif // HDMI_CEC_USE_DECODER
-196
View File
@@ -1,196 +0,0 @@
#pragma once
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <array>
#include "hdmi_cec.h"
namespace esphome::hdmi_cec {
/**
* This LookupTable is const-initialised and only provides a simple 'find()'.
* This creates a compact (smaller code) alternative to an std::map;
*/
template<typename KeyT, typename ValueT, unsigned int SIZE> class LookupTable {
public:
struct KeyValue {
KeyT key_;
ValueT value_;
};
const ValueT *find(KeyT key) const {
int lo_inx = 0;
int hi_inx = SIZE - 1;
while (lo_inx <= hi_inx) {
// Search for 'key' in the array range lo_inx..hi_inx, bounds included.
// Rely on the array being sorted on increasing opcode!
int mid = (lo_inx + hi_inx) / 2;
if (key == table_[mid].key_) {
return &table_[mid].value_;
}
if (key < table_[mid].key_) {
hi_inx = mid - 1;
} else {
lo_inx = mid + 1;
}
}
return nullptr; // requested 'key' not found in table
}
const KeyValue table_[SIZE];
};
/**
* This Decoder class interprets a binary CEC Frame to create a textual representation.
* The information to create this decoder is mostly extracted from the HDMI 1.3a standard document,
* from its section "Supplement 1 Consumer Electronics Control (CEC)".
* Some further details were found in the Linux kernel source code of the "v4l-utils" repository,
* such as the "ARC" related functionality of HDMI-CEC 1.4, and the HDMI vendor ID names.
* Details on the digital audio format decoding came from the wikipedia page on
* "Extended Display Identification Data (EDID)"
*/
class Decoder {
public:
Decoder(const Frame &frame, char *line, unsigned int size)
: frame_(frame), line_(line), size_(size), length_(0), offset_(2) {}
/**
* Create a one-line textual representation of the Frame content for logging and debugging
* Store the text in the earlier provided 'line' buffer
* @return the length of the written text, excluding the terminating null character
*/
int decode();
protected:
const char *find_opcode_name_(uint32_t opcode) const;
void address_decode_();
/**
* Generic operand decode method, later specialised with operand-type-specific methods
* @return true if further conversions can continue, false when to stop.
*/
template<uint32_t OPERANDS> bool do_operand_();
/**
* The CecOpcodeTable is extracted from the HDMI CEC standard (1.4):
* It lists all Frame opcodes with their <name> and their expected [operand argument type(s)]
*/
using OperandDecode_f = bool (Decoder::*)();
struct FrameType {
const char *name_; // name of the operation (of the op_code)
const OperandDecode_f decode_f_; // a pointer to the corresponding 'do_operand_()' method
};
using CecOpcodeTable = LookupTable<uint8_t, FrameType, 71>;
using VendorIdTable = LookupTable<uint32_t, const char *, 28>;
const Frame &frame_;
char *const line_; // buffer to hold the text of the decoded frame
const unsigned int size_; // size of the 'line_' buffer
unsigned int length_; // currently accumulated length of output text in 'line'
unsigned int offset_; // current offset in frame to process next operand byte(s) (frame[0] and [1] are skipped)
/**
* The HDMI CEC standard specifies a set of distinct operand (parameter) types,
* used across the frame opcodes, denoted with "[operand type name]".
* These specified operand types are enumerated here for later type-specific decoding to text
*/
enum Operand : uint8_t {
NONE,
ABORT_REASON,
ANALOG_BROADCAST_TYPE,
ANALOG_FREQUENCY,
ASCII_DIGIT,
ASCII,
AUDIO_FORMAT,
AUDIO_RATE,
AUDIO_STATUS,
BOOLEAN,
BROADCAST_SYSTEM,
CEC_VERSION,
CHANNEL_IDENTIFIER,
// DayOfMonth, integrated in StartDateTime
DECK_CONTROL_MODE,
DECK_INFO,
DEVICE_TYPE,
DIGITAL_SERVICE_IDENTIFICATION,
DISPLAY_CONTROL,
DURATION,
EXTERNAL_PHYSICAL_ADDRESS,
EXTERNAL_PLUG,
EXTERNAL_SOURCE_SPECIFIER,
HOUR,
FEATURE_OPCODE,
LANGUAGE,
MENU_REQUEST_TYPE,
MENU_STATE,
MINUTE,
// MonthOfYear, integrated in StartDateTime
NEW_ADDRESS,
ORIGINAL_ADDRESS,
OSD_NAME,
OSD_STRING = OSD_NAME,
PHYSICAL_ADDRESS,
PLAY_MODE,
POWER_STATUS,
PROGRAM_TITLE_STRING,
RECORD_SOURCE,
RECORD_STATUS_INFO,
RECORDING_SEQUENCE,
SHORT_AUDIO_DESCRIPTOR,
STATUS_REQUEST,
START_DATE_TIME,
SYSTEM_AUDIO_STATUS,
TIME,
TIMER_CLEARED_STATUS_DATA,
TIMER_STATUS_DATA,
TUNER_DEVICE_INFO,
UI_BROADCAST_TYPE,
UI_COMMAND,
UI_FUNCTION_MEDIA,
UI_FUNCTION_SELECT_AV_INPUT,
UI_FUNCTION_SELECT_AUDIO_INPUT,
UI_SOUND_PRESENTATION_CONTROL,
VENDOR_ID,
VENDOR_SPECIFIC_DATA,
VENDOR_SPECIFIC_RC_CODE,
};
/**
* The plain 'operand types' are uint8.
* Further uint32 'operand type' values are used to encode a sequence of upto 4 (potentially different) operands
* in the MSB bytes of an uint32 value.
*/
constexpr static uint32_t two(uint32_t first, uint32_t secnd) { return first | (secnd << 8); }
constexpr static uint32_t three(uint32_t first, uint32_t secnd, uint32_t third) {
return first | (secnd << 8) | (third << 16);
}
/**
* Helper function to implement the 'do_operand' methods:
* Append one word
* @return true if a further operand shall be decoded, false to terminate operand decoding
*/
bool append_operand_(const char *word, uint8_t offset_incr = 1);
/**
* Append one word, determined by the operand value as index to an array of names
* @return true if a further operand shall be decoded, false to terminate operand decoding
*/
template<uint32_t N_STRINGS> bool append_operand_(const std::array<const char *, N_STRINGS> &strings) {
uint32_t operand_value = frame_[offset_];
const char *s = (operand_value < N_STRINGS) ? strings[operand_value] : "?";
return append_operand_(s);
}
/**
* String tables used in the subsequent 'do_operand' decode functions
*/
const static std::array<const char *, 0x77> UI_COMMANDS;
const static std::array<const char *, 0x11> AUDIO_FORMATS;
const static std::array<const char *, 8> AUDIO_SAMPLERATES;
const static CecOpcodeTable CEC_OPCODE_TABLE;
const static VendorIdTable VENDOR_IDS;
}; // class Decoder
} // namespace esphome::hdmi_cec
-450
View File
@@ -1,450 +0,0 @@
#include "hdmi_cec.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266
#include <Arduino.h>
#endif
#ifdef USE_RP2040
#include <pico/time.h>
#endif
namespace esphome::hdmi_cec {
ESPHOME_LOG_TAG(TAG, "hdmi_cec");
#if defined(USE_ESP8266) || defined(USE_RP2040)
// The timer interrupt of these platforms carries no argument
static HDMICEC *ack_timer_owner = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif
// Bit timing from the HDMI 1.4 CEC specification, in microseconds
static constexpr uint32_t START_BIT_MIN_US = 3500; // a low period at least this long is a start bit
static constexpr uint32_t START_BIT_NOM_US = 3700;
static constexpr uint32_t START_BIT_HIGH_US = 800;
static constexpr uint32_t HIGH_BIT_MIN_US = 400; // shortest low period of a logical 1
static constexpr uint32_t HIGH_BIT_US = 600; // nominal low period of a logical 1
static constexpr uint32_t LOW_BIT_US = 1500; // nominal low period of a logical 0
static constexpr uint32_t SAFE_SAMPLE_US = 1050; // the line is sampled here; a longer low period is a logical 0
static constexpr uint32_t TOTAL_BIT_US = 2400;
static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_RECEIVE = TOTAL_BIT_US * 5; // also for a new initiator
static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_XMIT_FAIL = TOTAL_BIT_US * 3;
static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_XMIT_SUCCESS = TOTAL_BIT_US * 7;
static constexpr uint32_t ABORTED_FRAME_US = TOTAL_BIT_US * 15; // no edge for this long: our frame was cut short
static constexpr uint8_t MAX_ATTEMPTS = 8; // CEC allows 5, but a busy bus counts as an attempt here too
Frame::Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload) {
this->data_[0] = ((initiator_addr & 0xf) << 4) | (destination_addr & 0xf);
this->size_ = 1 + payload.size();
std::copy(payload.begin(), payload.end(), this->data_.begin() + 1);
}
void HDMICEC::setup() {
// The line is open drain with a weak pull-up: driven low to send, released to read
this->pin_->setup();
this->isr_pin_ = this->pin_->to_isr();
this->set_pin_input_high_();
this->required_idle_period_ = SIGNAL_FREE_TIME_AFTER_RECEIVE;
if (!this->setup_ack_timer_()) {
ESP_LOGE(TAG, "Failed to set up the acknowledge timer");
this->mark_failed();
return;
}
this->pin_->attach_interrupt(HDMICEC::gpio_isr_s, this, gpio::INTERRUPT_ANY_EDGE);
// loop() only has work once the interrupt queues a frame or send() queues one
this->disable_loop();
}
void HDMICEC::dump_config() {
ESP_LOGCONFIG(TAG,
"HDMI-CEC:\n"
" Address: 0x%X\n"
" Monitor mode: %s\n"
" Promiscuous mode: %s",
this->address_, YESNO(this->monitor_mode_), YESNO(this->promiscuous_mode_));
LOG_PIN(" Pin: ", this->pin_);
}
void HDMICEC::loop() {
const uint8_t dropped = atomic_get(this->dropped_frames_);
if (dropped != this->reported_dropped_frames_) {
const uint8_t count = dropped - this->reported_dropped_frames_;
ESP_LOGW(TAG, "%u received frame%s dropped (queue full or oversized)", count,
count == 1 ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("s"));
this->reported_dropped_frames_ = dropped;
}
// One inbound frame per pass keeps the time spent in the automations bounded
if (const Frame *frame = this->frames_queue_.front()) {
#ifdef ESPHOME_LOG_HAS_DEBUG
char line[format_hex_pretty_size(Frame::MAX_LENGTH)];
ESP_LOGD(TAG, "Received: %s", format_hex_pretty_to(line, frame->bytes().data(), frame->size()));
#endif
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
this->message_callbacks_.call(frame->initiator_addr(), frame->destination_addr(), frame->payload());
#endif
this->frames_queue_.pop_front();
}
// A frame being sent stays at the front of the queue until its attempts are over
if (!this->send_queue_.is_empty()) {
this->transmit_message_();
}
if (this->frames_queue_.is_empty() && this->send_queue_.is_empty()) {
this->disable_loop();
}
}
bool HDMICEC::send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload) {
if (this->monitor_mode_) {
ESP_LOGW(TAG, "Monitor mode, not sending 0x%X->0x%X", source, destination);
return false;
}
if (source > 0xf || destination > 0xf) {
ESP_LOGE(TAG, "Send abort: logical address 0x%X->0x%X out of range", source, destination);
return false;
}
if (payload.size() > Frame::MAX_PAYLOAD_LENGTH) {
ESP_LOGE(TAG, "Send abort 0x%X->0x%X: payload of %zu bytes exceeds %zu", source, destination, payload.size(),
Frame::MAX_PAYLOAD_LENGTH);
return false;
}
Frame *frame = this->send_queue_.back();
if (frame == nullptr) {
ESP_LOGW(TAG, "Send queue full, frame 0x%X->0x%X dropped", source, destination);
return false;
}
*frame = Frame(source, destination, payload);
this->send_queue_.push_back();
this->enable_loop();
return true;
}
void HDMICEC::transmit_message_() {
const uint32_t now = micros();
const uint32_t since_last_edge = now - atomic_get(this->last_edge_us_);
if (this->transmitting_) {
// The interrupt sees our own frame; wait for its end of message bit, or give up when the
// bus went quiet without one
if (!atomic_get(this->eom_received_) && since_last_edge < ABORTED_FRAME_US) {
return;
}
this->transmitting_ = false;
atomic_set(this->eom_received_, false); // the end of our own frame is not a foreign one
const Frame *frame = this->send_queue_.front();
// A broadcast is acknowledged by nobody; a directed frame on every byte
const bool bytes_ok = this->n_bytes_received_ == frame->size();
const bool acks_ok = this->n_acks_received_ == (frame->is_broadcast() ? 0 : frame->size());
if (bytes_ok && acks_ok) {
ESP_LOGD(TAG, "Send frame success in %u attempt%s", this->transmit_attempts_,
this->transmit_attempts_ > 1 ? LOG_STR_LITERAL("s") : LOG_STR_LITERAL(""));
} else if (!bytes_ok) {
ESP_LOGD(TAG, "Send frame incorrect on attempt %u, saw %u bytes but expected %zu", this->transmit_attempts_,
this->n_bytes_received_, frame->size());
} else {
ESP_LOGD(TAG, "Send frame not acknowledged on attempt %u", this->transmit_attempts_);
}
this->finish_attempt_(bytes_ok && acks_ok);
} else if (atomic_get(this->eom_received_)) {
// Another device finished a frame; the standard asks a new initiator to wait after it
this->required_idle_period_ = SIGNAL_FREE_TIME_AFTER_RECEIVE;
atomic_set(this->eom_received_, false);
}
const Frame *frame = this->send_queue_.front();
// An edge within the signal free time means the bus is in use or not yet free for us; a start
// bit refused by a held line drives no edge, so that attempt is timed on its own
if (frame == nullptr || since_last_edge < this->required_idle_period_ ||
now - this->last_attempt_us_ < this->required_idle_period_) {
return;
}
this->last_attempt_us_ = now;
this->transmit_attempts_++;
#ifdef ESPHOME_LOG_HAS_DEBUG
if (this->transmit_attempts_ == 1) {
char line[format_hex_pretty_size(Frame::MAX_LENGTH)];
ESP_LOGD(TAG, "Sending: %s", format_hex_pretty_to(line, frame->bytes().data(), frame->size()));
}
#endif
// The interrupt counts the bytes and acknowledge bits of our own frame
this->n_bytes_received_ = 0;
this->n_acks_received_ = 0;
atomic_set(this->eom_received_, false);
this->transmitting_ = true;
if (!this->transmit_frame_(frame)) {
// The line was held, or another initiator sent at the same time
this->transmitting_ = false;
this->finish_attempt_(false);
}
// Otherwise the acknowledge result arrives through the interrupt
}
void HDMICEC::finish_attempt_(bool sent_ok) {
this->required_idle_period_ = sent_ok ? SIGNAL_FREE_TIME_AFTER_XMIT_SUCCESS : SIGNAL_FREE_TIME_AFTER_XMIT_FAIL;
if (!sent_ok) {
if (this->transmit_attempts_ < MAX_ATTEMPTS) {
return; // the signal free time has to pass before the retry
}
ESP_LOGW(TAG, "Frame not sent after %u attempts, dropped", this->transmit_attempts_);
}
this->transmit_attempts_ = 0;
this->send_queue_.pop_front();
}
bool HDMICEC::transmit_frame_(const Frame *frame) {
if (!this->send_start_bit_()) {
return false;
}
const auto bytes = frame->bytes();
for (size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) {
const uint8_t current_byte = bytes[byte_inx];
// Most significant bit first. The four initiator address bits watch for a collision with
// another initiator that started at the same moment; a lower address wins by holding the line
for (int8_t i = 7; i >= 0; i--) {
const bool bit_value = (current_byte >> i) & 0x1;
if (!this->send_bit_(bit_value) && byte_inx == 0 && i >= 4) {
ESP_LOGD(TAG, "Bus collision on sending initiator address 0x%X on attempt %u", frame->initiator_addr(),
this->transmit_attempts_);
return false;
}
}
// End of message bit, then the acknowledge bit which the receiver may pull low
this->send_bit_(byte_inx == bytes.size() - 1);
this->send_bit_(true);
}
return true;
}
bool HDMICEC::send_start_bit_() {
if (!this->isr_pin_.digital_read()) {
ESP_LOGD(TAG, "Bus occupied before start bit on attempt %u", this->transmit_attempts_);
return false;
}
this->set_pin_output_low_();
delay_microseconds_safe(START_BIT_NOM_US);
// Release the line and check that nobody else keeps it low
this->set_pin_input_high_();
delay_microseconds_safe(START_BIT_HIGH_US / 2);
bool value = this->isr_pin_.digital_read();
delay_microseconds_safe(START_BIT_HIGH_US / 2);
value &= this->isr_pin_.digital_read();
if (!value) {
ESP_LOGD(TAG, "Bus collision on sending start bit on attempt %u", this->transmit_attempts_);
}
return value;
}
bool HDMICEC::send_bit_(bool bit_value) {
const uint32_t start_us = micros();
this->set_pin_output_low_();
delay_microseconds_safe(bit_value ? HIGH_BIT_US : LOW_BIT_US);
this->set_pin_input_high_();
bool clear = true;
if (bit_value) {
// Sample in the middle of the safe sample period: a line still low means another initiator
// is sending a logical 0 at the same time
uint32_t elapsed = micros() - start_us;
if (elapsed < SAFE_SAMPLE_US) {
delay_microseconds_safe(SAFE_SAMPLE_US - elapsed);
}
clear = this->isr_pin_.digital_read();
}
const uint32_t elapsed = micros() - start_us;
if (elapsed < TOTAL_BIT_US) {
delay_microseconds_safe(TOTAL_BIT_US - elapsed);
}
return clear;
}
void IRAM_ATTR HDMICEC::send_ack_() {
// The acknowledge has to start right at this falling edge; the timer releases the line
this->set_pin_output_low_();
this->start_ack_timer_();
}
#if defined(USE_ESP32)
bool HDMICEC::setup_ack_timer_() {
gptimer_config_t config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1000000, // one tick per microsecond
};
if (gptimer_new_timer(&config, &this->ack_timer_) != ESP_OK) {
return false;
}
gptimer_event_callbacks_t callbacks = {.on_alarm = HDMICEC::ack_timer_isr_s};
// The alarm reloads the count to zero itself, so the interrupt only has to start the timer
gptimer_alarm_config_t alarm = {
.alarm_count = LOW_BIT_US, .reload_count = 0, .flags = {.auto_reload_on_alarm = true}};
return gptimer_register_event_callbacks(this->ack_timer_, &callbacks, this) == ESP_OK &&
gptimer_set_alarm_action(this->ack_timer_, &alarm) == ESP_OK && gptimer_enable(this->ack_timer_) == ESP_OK;
}
void IRAM_ATTR HDMICEC::start_ack_timer_() { gptimer_start(this->ack_timer_); }
bool IRAM_ATTR HDMICEC::ack_timer_isr_s(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *arg) {
auto *self = static_cast<HDMICEC *>(arg);
self->set_pin_input_high_();
gptimer_stop(timer);
return false; // no task to wake
}
#elif defined(USE_ESP8266)
static constexpr uint32_t TIMER1_TICKS_PER_US = 5; // TIM_DIV16 of the 80 MHz clock
bool HDMICEC::setup_ack_timer_() {
ack_timer_owner = this;
timer1_isr_init();
timer1_attachInterrupt(HDMICEC::ack_timer_isr_s);
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); // a write arms one shot
return true;
}
void IRAM_ATTR HDMICEC::start_ack_timer_() { timer1_write(LOW_BIT_US * TIMER1_TICKS_PER_US); }
void IRAM_ATTR HDMICEC::ack_timer_isr_s() { ack_timer_owner->set_pin_input_high_(); }
#elif defined(USE_RP2040)
bool HDMICEC::setup_ack_timer_() {
this->ack_alarm_ = hardware_alarm_claim_unused(false);
if (this->ack_alarm_ < 0) {
return false; // every hardware alarm is taken
}
ack_timer_owner = this;
hardware_alarm_set_callback(this->ack_alarm_, HDMICEC::ack_timer_isr_s);
return true;
}
void IRAM_ATTR HDMICEC::start_ack_timer_() {
hardware_alarm_set_target(this->ack_alarm_, make_timeout_time_us(LOW_BIT_US));
}
void IRAM_ATTR HDMICEC::ack_timer_isr_s(uint alarm_num) { ack_timer_owner->set_pin_input_high_(); }
#else
bool HDMICEC::setup_ack_timer_() { return true; }
// Without a one-shot timer the interrupt holds the line itself
void IRAM_ATTR HDMICEC::start_ack_timer_() {
delay_microseconds_safe(LOW_BIT_US);
this->set_pin_input_high_();
}
#endif
void IRAM_ATTR HDMICEC::gpio_isr_s(HDMICEC *self) { self->gpio_isr_(); }
void IRAM_ATTR HDMICEC::gpio_isr_() {
const uint32_t now = micros();
const bool level = this->isr_pin_.digital_read();
if (level == this->prev_pin_level_) {
return; // a mode change of the pin, not a bus edge
}
this->prev_pin_level_ = level;
atomic_set(this->last_edge_us_, now);
if (!level) {
// Falling edge: the start of a low period
if (now - this->last_falling_edge_us_ > START_BIT_NOM_US + TOTAL_BIT_US) {
// The bus was quiet; whatever was gathered belongs to an aborted frame
this->reset_receiver_();
}
this->last_falling_edge_us_ = now;
if (this->recv_ack_queued_) {
// Acknowledge by stretching this low period
this->send_ack_();
this->recv_ack_queued_ = false;
}
return;
}
// Rising edge: the low period tells the bit value
const uint32_t pulse_duration = now - this->last_falling_edge_us_;
if (pulse_duration > START_BIT_MIN_US) {
this->reset_receiver_();
this->receiver_state_ = ReceiverState::RECEIVER_STATE_RECEIVING_BYTE;
// Without a free slot the frame is neither acknowledged nor delivered, so its initiator retries
this->recv_frame_ = this->frames_queue_.back();
if (this->recv_frame_ != nullptr) {
this->recv_frame_->clear();
}
return;
}
if (pulse_duration < HIGH_BIT_MIN_US / 4) {
return; // glitch
}
const bool value = pulse_duration <= SAFE_SAMPLE_US;
// Our own frame is on the bus until its end of message; send() may give it any initiator address
const bool own_frame = this->transmitting_ && !atomic_get(this->eom_received_);
switch (this->receiver_state_) {
case ReceiverState::RECEIVER_STATE_IDLE:
break;
case ReceiverState::RECEIVER_STATE_RECEIVING_BYTE:
this->recv_byte_ = (this->recv_byte_ << 1) | value;
if (++this->recv_bit_counter_ == 8) {
if (this->recv_length_++ == 0) {
this->recv_header_ = this->recv_byte_;
}
if (this->recv_frame_ != nullptr && !this->recv_frame_->push_back(this->recv_byte_)) {
this->recv_frame_ = nullptr; // longer than the bus allows: corrupt, do not deliver it
}
this->recv_bit_counter_ = 0;
this->receiver_state_ = ReceiverState::RECEIVER_STATE_WAITING_FOR_EOM;
}
break;
case ReceiverState::RECEIVER_STATE_WAITING_FOR_EOM:
this->recv_eom_ = value;
// A frame for us is acknowledged on the next bit, unless there is no room for it (so its
// initiator retries) or it is our own, which includes a polling message from a device
// claiming our logical address. A polling message for us (header only) needs no room, and
// goes unacknowledged only when nobody holds the address
if (!this->monitor_mode_ && !own_frame && (this->recv_frame_ != nullptr || (value && this->recv_length_ == 1)) &&
(this->recv_header_ & 0xf) == this->address_ && (this->recv_header_ >> 4) != this->address_) {
this->recv_ack_queued_ = true;
}
this->receiver_state_ = ReceiverState::RECEIVER_STATE_WAITING_FOR_ACK;
break;
case ReceiverState::RECEIVER_STATE_WAITING_FOR_ACK:
if (own_frame) {
// A low acknowledge bit means the addressed device took the byte
// (for a broadcast it means some device refused it)
this->n_bytes_received_++;
if (!value) {
this->n_acks_received_++;
}
}
if (!this->recv_eom_) {
this->receiver_state_ = ReceiverState::RECEIVER_STATE_RECEIVING_BYTE;
break;
}
atomic_set(this->eom_received_, true); // publishes the tally above to loop()
// The application only gets frames that are not our own and not a polling message (header
// only), which the acknowledge answered already, and that are addressed to us unless
// promiscuous
const uint8_t destination = this->recv_header_ & 0xf;
const bool wanted = this->recv_length_ > 1 && !own_frame && (this->recv_header_ >> 4) != this->address_ &&
(this->promiscuous_mode_ || destination == 0xf || destination == this->address_);
if (!wanted) {
// nothing to deliver
} else if (this->recv_frame_ == nullptr) {
atomic_set<uint8_t>(this->dropped_frames_, atomic_get(this->dropped_frames_) + 1);
} else {
this->frames_queue_.push_back();
this->enable_loop_soon_any_context();
}
this->reset_receiver_();
break;
}
}
void IRAM_ATTR HDMICEC::reset_receiver_() {
this->receiver_state_ = ReceiverState::RECEIVER_STATE_IDLE;
this->recv_bit_counter_ = 0;
this->recv_length_ = 0;
this->recv_ack_queued_ = false;
this->recv_frame_ = nullptr;
}
} // namespace esphome::hdmi_cec
-218
View File
@@ -1,218 +0,0 @@
#pragma once
#include <array>
#include <atomic>
#include <initializer_list>
#include <span>
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32
#include "driver/gptimer.h"
#endif
#ifdef USE_RP2040
#include <hardware/timer.h>
#endif
namespace esphome::hdmi_cec {
// Release/acquire store and load. The read-modify-write forms of std::atomic are not available on
// every target (the ESP8266 has none), so a writer computes the new value itself; that is safe here
// because every atomic in this component has exactly one writer.
template<typename T> ESPHOME_ALWAYS_INLINE void atomic_set(std::atomic<T> &atom, T value) {
atom.store(value, std::memory_order_release);
}
template<typename T> ESPHOME_ALWAYS_INLINE T atomic_get(const std::atomic<T> &atom) {
return atom.load(std::memory_order_acquire);
}
/// One CEC bus message: a header byte with the initiator and destination addresses, then up to
/// 15 payload bytes of which the first is the opcode. Fixed storage, so the receive interrupt
/// never allocates.
class Frame {
public:
static constexpr size_t MAX_LENGTH = 16; // HDMI-CEC 1.4
static constexpr size_t MAX_PAYLOAD_LENGTH = MAX_LENGTH - 1;
Frame() = default;
/// The payload is at most MAX_PAYLOAD_LENGTH bytes; send() checks that
Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload);
ESPHOME_ALWAYS_INLINE uint8_t initiator_addr() const { return this->data_[0] >> 4; }
ESPHOME_ALWAYS_INLINE uint8_t destination_addr() const { return this->data_[0] & 0xf; }
ESPHOME_ALWAYS_INLINE bool is_broadcast() const { return this->destination_addr() == 0xf; }
ESPHOME_ALWAYS_INLINE size_t size() const { return this->size_; }
std::span<const uint8_t> bytes() const { return {this->data_.data(), this->size_}; }
/// The bytes after the header, starting with the opcode
std::span<const uint8_t> payload() const { return this->bytes().subspan(1); }
ESPHOME_ALWAYS_INLINE void clear() { this->size_ = 0; }
/// False when the frame is full already; the bus allows no more
ESPHOME_ALWAYS_INLINE bool push_back(uint8_t data) {
if (this->size_ == MAX_LENGTH) {
return false;
}
this->data_[this->size_++] = data;
return true;
}
protected:
std::array<uint8_t, MAX_LENGTH> data_{};
uint8_t size_{0};
};
/// A queue of frames between one producer and one consumer, for instance the receive interrupt and
/// loop(). The indices run freely and each is written by one side only, so no lock is needed.
/// back() hands out a slot to fill and push_back() commits it; front() and pop_front() do the same
/// on the reading side.
template<size_t SIZE> class FrameRingBuffer {
static_assert((SIZE & (SIZE - 1)) == 0, "SIZE must be a power of two");
public:
ESPHOME_ALWAYS_INLINE Frame *front() { return this->is_empty() ? nullptr : this->slot_(this->front_inx_); }
ESPHOME_ALWAYS_INLINE void pop_front() { this->advance_(this->front_inx_); }
ESPHOME_ALWAYS_INLINE Frame *back() { return this->count_() == SIZE ? nullptr : this->slot_(this->back_inx_); }
ESPHOME_ALWAYS_INLINE void push_back() { this->advance_(this->back_inx_); }
ESPHOME_ALWAYS_INLINE bool is_empty() const { return this->count_() == 0; }
protected:
ESPHOME_ALWAYS_INLINE uint8_t count_() const {
return static_cast<uint8_t>(atomic_get(this->back_inx_) - atomic_get(this->front_inx_));
}
ESPHOME_ALWAYS_INLINE Frame *slot_(const std::atomic<uint8_t> &inx) { return &this->store_[atomic_get(inx) % SIZE]; }
ESPHOME_ALWAYS_INLINE void advance_(std::atomic<uint8_t> &inx) {
atomic_set<uint8_t>(inx, static_cast<uint8_t>(atomic_get(inx) + 1));
}
std::array<Frame, SIZE> store_{};
std::atomic<uint8_t> front_inx_{0};
std::atomic<uint8_t> back_inx_{0};
};
/// The HDMI-CEC bus on one GPIO pin. The receive interrupt decodes the edges of every frame,
/// including those this device sends, and queues complete frames for loop(); loop() runs the
/// on_message automations and transmits queued frames, one blocking bit-banged attempt per pass.
class HDMICEC : public Component {
public:
HDMICEC(InternalGPIOPin *pin, uint8_t address) : pin_(pin), address_(address) {}
void setup() override;
void dump_config() override;
void loop() override;
void set_promiscuous_mode(bool promiscuous) { this->promiscuous_mode_ = promiscuous; }
void set_monitor_mode(bool monitor_mode) { this->monitor_mode_ = monitor_mode; }
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
/// Called with the initiator, the destination and the payload (opcode first) of every received
/// frame; the payload is only valid during the call
template<typename F> void add_on_message_callback(F &&callback) {
this->message_callbacks_.add(std::forward<F>(callback));
}
#endif
/// Queue a frame for transmission; false when the queue is full or in monitor mode
bool send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload);
bool send(uint8_t destination, std::span<const uint8_t> payload) {
return this->send(this->address_, destination, payload);
}
/// For lambdas: id(cec).send(0x0, {0x44, 0x41});
bool send(uint8_t destination, std::initializer_list<uint8_t> payload) {
return this->send(destination, std::span<const uint8_t>(payload.begin(), payload.size()));
}
protected:
enum class ReceiverState : uint8_t {
RECEIVER_STATE_IDLE,
RECEIVER_STATE_RECEIVING_BYTE,
RECEIVER_STATE_WAITING_FOR_EOM,
RECEIVER_STATE_WAITING_FOR_ACK,
};
// Receive side, in the interrupt
static void gpio_isr_s(HDMICEC *self);
void gpio_isr_();
void reset_receiver_();
/// Pull the line low for the acknowledge bit of a frame addressed to us
void send_ack_();
// A hardware one-shot timer releases the line after the acknowledge bit (gptimer on the ESP32,
// timer 1 on the ESP8266, a hardware alarm on the RP2040), so the interrupt returns at once;
// without one the interrupt holds the line for the 1.5 ms itself
bool setup_ack_timer_();
void start_ack_timer_();
#ifdef USE_ESP32
static bool ack_timer_isr_s(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *arg);
#endif
#ifdef USE_ESP8266
static void ack_timer_isr_s();
#endif
#ifdef USE_RP2040
static void ack_timer_isr_s(uint alarm_num);
#endif
// Transmit side, in loop()
void transmit_message_();
/// Keep the frame for another attempt, or drop it once the attempts are used up
void finish_attempt_(bool sent_ok);
/// Drive the frame onto the line; false when another initiator sent at the same time
bool transmit_frame_(const Frame *frame);
bool send_start_bit_();
/// Send one bit; false when a logical 1 found the line held low by another initiator
bool send_bit_(bool bit_value);
ESPHOME_ALWAYS_INLINE void set_pin_input_high_() { this->isr_pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
ESPHOME_ALWAYS_INLINE void set_pin_output_low_() {
this->isr_pin_.pin_mode(gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
this->isr_pin_.digital_write(false);
}
static constexpr size_t QUEUE_SIZE = 4;
// Each queue is 70 bytes, so two single-byte fields follow each to fill the alignment gap
FrameRingBuffer<QUEUE_SIZE> frames_queue_; // received, interrupt to loop()
uint8_t recv_bit_counter_{0};
uint8_t recv_byte_{0};
FrameRingBuffer<QUEUE_SIZE> send_queue_; // to send, both ends on loop()
uint8_t transmit_attempts_{0};
uint8_t reported_dropped_frames_{0};
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
StaticCallbackManager<HDMI_CEC_MESSAGE_CALLBACK_COUNT, void(uint8_t, uint8_t, std::span<const uint8_t>)>
message_callbacks_;
#endif
InternalGPIOPin *pin_;
ISRInternalGPIOPin isr_pin_;
#ifdef USE_ESP32
gptimer_handle_t ack_timer_{nullptr};
#endif
#ifdef USE_RP2040
int ack_alarm_{-1};
#endif
// The word-sized fields come first and the single-byte ones form one run, so nothing is padded
std::atomic<uint32_t> last_edge_us_{0}; // the bus is busy until this is some time ago; interrupt writes
Frame *recv_frame_{nullptr}; // interrupt only; null when the queue was full or the frame too long
uint32_t last_falling_edge_us_{0}; // interrupt only
uint32_t last_attempt_us_{0}; // loop() only; a refused start bit drives no edge, so it is timed here
uint16_t required_idle_period_{0}; // loop() only
// Shared with the interrupt. The tally of our own frame is written by the interrupt while
// transmitting_ is set and no end of message has passed yet; loop() reads it once eom_received_
// says the frame is over
std::atomic<bool> eom_received_{false}; // set for every frame, ours or not
std::atomic<uint8_t> dropped_frames_{0};
uint8_t n_bytes_received_{0};
uint8_t n_acks_received_{0};
bool transmitting_{false};
// Receive state, interrupt only
uint8_t recv_header_{0}; // kept outside the frame so a dropped frame can still be judged
uint8_t recv_length_{0};
ReceiverState receiver_state_{ReceiverState::RECEIVER_STATE_IDLE};
bool recv_eom_{false};
bool recv_ack_queued_{false};
bool prev_pin_level_{true};
uint8_t address_; // logical address, 0xf when only listening
bool promiscuous_mode_{false};
bool monitor_mode_{false};
};
} // namespace esphome::hdmi_cec
+2 -2
View File
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
var = await climate_ir.new_climate_ir(config, tag())
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
+64 -68
View File
@@ -5,6 +5,7 @@
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <cmath>
#include <map>
#include <IRSender.h>
#include <HeatpumpIRFactory.h>
#include "esphome/components/remote_base/remote_base.h"
@@ -44,76 +45,71 @@ class IRSenderESPHome : public IRSender {
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
template<Protocol P> struct ProtocolClass;
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
return new typename ProtocolClass<P>::type(); // NOLINT
}
// Codegen calls only the configured protocols, so the linker drops the other classes
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
template<> struct ProtocolClass<protocol> { using type = cls; }; \
template HeatpumpIR *create_heatpump_ir<protocol>();
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
#undef HEATPUMPIR_PROTOCOL
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
};
void HeatpumpIRClimate::setup() {
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
ESP_LOGE(TAG, "Invalid protocol");
return;
}
this->heatpump_ir_ = protocol_constructor->second();
climate_ir::ClimateIR::setup();
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
+12 -20
View File
@@ -89,21 +89,25 @@ enum VerticalDirection {
VERTICAL_DIRECTION_DOWN = 5,
};
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
template<Protocol P> HeatpumpIR *create_heatpump_ir();
/// Selects the protocol at compile time so the enum and the library object always match.
template<Protocol P> struct ProtocolTag {};
// Temperature
const float TEMP_MIN = 0; // Celsius
const float TEMP_MAX = 100; // Celsius
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
public:
template<Protocol P>
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
HeatpumpIRClimate()
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
void setup() override;
void set_protocol(Protocol protocol) {
this->protocol_ = protocol;
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
void set_horizontal_default(HorizontalDirection horizontal_direction) {
this->default_horizontal_direction_ = horizontal_direction;
}
@@ -115,18 +119,6 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
protected:
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
heatpump_ir_(heatpump_ir),
protocol_(protocol) {
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
HeatpumpIR *heatpump_ir_;
void control(const climate::ClimateCall &call) override;
/// Transmit via IR the state of this climate controller.
@@ -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);
}
+2 -2
View File
@@ -59,8 +59,8 @@ namespace esphome::host {
char **get_argv() { return s_argv; }
const std::string &get_exe_path() {
static const std::string EMPTY;
return s_exe_path != nullptr ? *s_exe_path : EMPTY;
static const std::string empty;
return s_exe_path != nullptr ? *s_exe_path : empty;
}
void arm_reexec(const std::string &path) {
+1 -1
View File
@@ -5,7 +5,7 @@
#include "esphome/core/log.h"
#include "core.h"
#include <ctime>
#include <time.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
+2 -2
View File
@@ -39,8 +39,8 @@ bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock();
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
static constexpr uint8_t HOST_MAC_ADDRESS[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, HOST_MAC_ADDRESS, sizeof(HOST_MAC_ADDRESS));
static const uint8_t esphome_host_mac_address[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
}
} // namespace esphome
+4 -4
View File
@@ -2,7 +2,6 @@
#include <filesystem>
#include <fstream>
#include <utility>
#include "preferences.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -46,10 +45,11 @@ void HostPreferences::setup_() {
break;
if (fread(&len, sizeof(len), 1, fp) != 1)
break;
std::vector<uint8_t> data(len);
if (fread(data.data(), sizeof(uint8_t), len, fp) != len)
uint8_t data[len];
if (fread(data, sizeof(uint8_t), len, fp) != len)
break;
this->data_[key] = std::move(data);
std::vector vec(data, data + len);
this->data_[key] = vec;
}
fclose(fp);
}
@@ -53,7 +53,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
for (const auto &[name, value] : request_headers) {
h_headers.emplace(name, value);
}
httplib::Client client(scheme_host);
httplib::Client client(scheme_host.c_str());
if (!client.is_valid()) {
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
return nullptr;
@@ -114,7 +114,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
}
container->content_length = container->response_body_.size();
for (const auto &header : response.headers) {
for (auto header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first); // NOLINT
if (should_collect_header(lower_case_collect_headers, lower_name)) {
@@ -1,7 +1,6 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import audio, esp32, speaker
from esphome.components.microphone import DOMAIN as MICROPHONE_DOMAIN
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
@@ -15,11 +14,9 @@ from esphome.const import (
CONF_SAMPLE_RATE,
CONF_TIMEOUT,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
from .. import (
CONF_I2S_AUDIO_ID,
CONF_I2S_DOUT_PIN,
CONF_I2S_MODE,
CONF_LEFT,
@@ -47,7 +44,6 @@ I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase)
CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
CONF_KEEP_ALIVE = "keep_alive"
CONF_SPDIF_MODE = "spdif_mode"
I2SAudioSpeakerBase = i2s_audio_ns.class_(
@@ -177,7 +173,6 @@ BASE_SCHEMA = (
cv.positive_time_period_milliseconds,
cv.one_of(CONF_NEVER, lower=True),
),
cv.Optional(CONF_KEEP_ALIVE, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -240,23 +235,6 @@ def _final_validate(config: ConfigType) -> None:
raise cv.Invalid("SPDIF mode requires 'i2s_comm_fmt: stand_i2s'")
if config[CONF_MCLK_MULTIPLE] != 256:
raise cv.Invalid("SPDIF mode requires 'mclk_multiple: 256'")
if config[CONF_KEEP_ALIVE]:
raise cv.Invalid("SPDIF mode does not support 'keep_alive'")
if config[CONF_KEEP_ALIVE]:
# A kept channel holds the port between sessions, so nothing else may share the bus
bus = config[CONF_I2S_AUDIO_ID]
full_config = fv.full_config.get()
on_bus = sum(
1
for domain in (speaker.DOMAIN, MICROPHONE_DOMAIN)
for entry in full_config.get(domain, [])
if entry.get(CONF_I2S_AUDIO_ID) == bus
)
if on_bus > 1:
raise cv.Invalid(
"'keep_alive' needs its own I2S bus; another speaker or microphone shares this one"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -284,8 +262,6 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_TIMEOUT] != CONF_NEVER:
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
if config[CONF_KEEP_ALIVE]:
cg.add(var.set_keep_alive(True))
# The SPDIF encoder and speaker are fully #ifdef'd on USE_I2S_AUDIO_SPDIF_MODE,
@@ -56,9 +56,8 @@ void I2SAudioSpeakerBase::dump_config() {
ESP_LOGCONFIG(TAG,
"Speaker:\n"
" Pin: %d\n"
" Buffer duration: %" PRIu32 "\n"
" Keep alive: %s",
static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_, YESNO(this->keep_alive_));
" Buffer duration: %" PRIu32,
static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_);
if (this->timeout_.has_value()) {
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value());
}
@@ -106,13 +105,7 @@ void I2SAudioSpeakerBase::loop() {
vTaskDelete(this->speaker_task_handle_);
this->speaker_task_handle_ = nullptr;
// The task raises this bit before it exits, possibly a loop pass before TASK_STOPPED is seen, so read it
// live here; a channel the task could not bring up is rebuilt next time rather than kept
const bool task_failed = xEventGroupGetBits(this->event_group_) & SpeakerEventGroupBits::ERR_ESP_NO_MEM;
if (task_failed) {
ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)");
}
this->stop_i2s_driver_(this->keep_alive_ && !task_failed);
this->stop_i2s_driver_();
// ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at
// the top of loop(): the audio source's task can raise a start at any point above, including
// during stop_i2s_driver_(), and nothing would ever re-issue it.
@@ -128,6 +121,11 @@ void I2SAudioSpeakerBase::loop() {
this->state_ = speaker::STATE_STOPPED;
}
if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) {
ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
}
// Handle the speaker's state
switch (this->state_) {
case speaker::STATE_STARTING:
@@ -135,8 +133,8 @@ void I2SAudioSpeakerBase::loop() {
break;
}
// Still winding down from a previous run
if (this->speaker_task_handle_ != nullptr) {
// Still starting up or winding down from a previous run
if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) {
break;
}
@@ -156,7 +154,7 @@ void I2SAudioSpeakerBase::loop() {
if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
this->status_momentary_error(1000);
this->stop_i2s_driver_(false); // Stops the driver to return the lock; will be reloaded in next attempt
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
}
break;
case speaker::STATE_RUNNING: // Intentional fallthrough
@@ -272,25 +270,31 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize I2S channel");
this->delete_channel_();
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
this->parent_->unlock();
return err;
}
// Both queues are drained by the speaker task before it preloads, whether the channel is new or kept
if (this->i2s_event_queue_ == nullptr) {
this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t));
} else {
// Reset queue to clear any stale events from previous task
xQueueReset(this->i2s_event_queue_);
}
// Lockstep records queue. One record per in-flight DMA buffer; sized to match the I2S event queue
// so a fully-saturated DMA pipeline cannot overflow either side before drain.
if (this->write_records_queue_ == nullptr) {
this->write_records_queue_ = xQueueCreate(event_queue_size, sizeof(uint32_t));
} else {
xQueueReset(this->write_records_queue_);
}
if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)");
this->delete_channel_();
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
this->parent_->unlock();
return ESP_ERR_NO_MEM;
}
@@ -298,30 +302,22 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c
return ESP_OK;
}
void I2SAudioSpeakerBase::stop_i2s_driver_(bool keep_channel) {
void I2SAudioSpeakerBase::stop_i2s_driver_() {
if (this->tx_handle_ != nullptr) {
i2s_channel_disable(this->tx_handle_);
// A kept channel lets the next start skip i2s_new_channel and i2s_channel_init_std_mode, whose GPIO and
// clock reconfiguration is the audible pop
if (!keep_channel) {
this->delete_channel_();
}
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
// i2s_del_channel() leaves dout wired to this port's data-out signal in the GPIO matrix: it only
// clears an internal reservation mask, never the esp_rom_gpio_connect_out_signal() routing that
// setup installed. If another speaker reuses this port (shared bus), its audio still reaches our
// dout. Detach the pin and drive it low so a stale output stops driving downstream hardware: a
// SPDIF optical transmitter would otherwise stay lit, and an analog DAC would emit noise.
this->park_dout_pin_();
}
this->parent_->unlock();
}
void I2SAudioSpeakerBase::delete_channel_() {
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
// i2s_del_channel() leaves dout wired to this port's data-out signal in the GPIO matrix: it only
// clears an internal reservation mask, never the esp_rom_gpio_connect_out_signal() routing that
// setup installed. If another speaker reuses this port (shared bus), its audio still reaches our
// dout. Detach the pin and drive it low so a stale output stops driving downstream hardware: a
// SPDIF optical transmitter would otherwise stay lit, and an analog DAC would emit noise.
this->park_dout_pin_();
}
bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
int64_t now = esp_timer_get_time();
@@ -54,7 +54,6 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
void set_timeout(uint32_t ms) { this->timeout_ = ms; }
void set_keep_alive(bool keep_alive) { this->keep_alive_ = keep_alive; }
void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; }
/// @brief Get the I2S TX channel handle
@@ -127,15 +126,12 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg,
size_t event_queue_size);
/// @brief Stops the I2S driver and unlocks the I2S port; keep_channel leaves the disabled channel allocated
void stop_i2s_driver_(bool keep_channel);
/// @brief Stops the I2S driver and unlocks the I2S port
void stop_i2s_driver_();
/// @brief Detaches dout from the I2S signal and drives it low
void park_dout_pin_();
/// @brief Deletes the I2S channel and parks dout; the caller holds the bus lock
void delete_channel_();
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
virtual void on_task_stopped() {}
@@ -181,7 +177,6 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
optional<uint32_t> timeout_;
bool pause_state_{false};
bool keep_alive_{false};
// Smooths software gain changes. The main loop posts targets, the speaker task processes;
// GainRamp's mailbox makes that safe. The main loop is the only poster.
@@ -75,9 +75,6 @@ void I2SAudioSpeaker::dump_config() {
void I2SAudioSpeaker::run_speaker_task() {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
// Stale records from the previous run would pair with the wrong DMA events
xQueueReset(this->i2s_event_queue_);
xQueueReset(this->write_records_queue_);
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT;
// Ensure ring buffer duration is at least the duration of all DMA buffers
@@ -392,6 +389,8 @@ void I2SAudioSpeaker::run_speaker_task() {
}
esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) {
this->current_stream_info_ = audio_stream_info;
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
// Can't reconfigure I2S bus, so the sample rate must match the configured value
ESP_LOGE(TAG, "Incompatible stream settings");
@@ -429,16 +428,6 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
return ESP_ERR_INVALID_STATE;
}
// Reuse a kept channel while the stream format matches
if (this->tx_handle_ != nullptr) {
if (this->current_stream_info_ == audio_stream_info) {
return ESP_OK;
}
this->delete_channel_();
}
this->current_stream_info_ = audio_stream_info;
// The DMA buffers hold output-format (post-narrowing) samples, so size them from the output stream info.
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_);
+5 -1
View File
@@ -37,6 +37,7 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from esphome.types import ConfigType
AUTO_LOAD = ["split_buffer"]
@@ -347,8 +348,11 @@ def _final_validate(config: ConfigType) -> None:
config
)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if display.has_external_writer(config[CONF_ID]):
if LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
-1
View File
@@ -204,7 +204,6 @@ class IT8951Display : public Display,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
protected:
int get_height_internal() override { return this->height_; }
-11
View File
@@ -1,7 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE
from esphome.types import ConfigType
@@ -17,8 +16,6 @@ LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDev
CONF_LD2450_ID = "ld2450_id"
CONF_POLYGON_ZONE_ID = "polygon_zone_id"
CONF_POLYGON_ZONES = "polygon_zones"
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -53,14 +50,6 @@ _CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_DATA, "add_on_data_callback"),
)
# Polygon zone sources build only when zones are configured
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"polygon.cpp": "LD2450_POLYGON_ZONE_COUNT",
"polygon_zone.cpp": "LD2450_POLYGON_ZONE_COUNT",
}
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
+1 -35
View File
@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_FILTERS,
CONF_HAS_MOVING_TARGET,
CONF_HAS_STILL_TARGET,
CONF_HAS_TARGET,
@@ -12,14 +11,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_LD2450_ID,
CONF_POLYGON_ZONE_ID,
CONF_POLYGON_ZONES,
FILTER_SOURCE_FILES,
LD2450Component,
)
from .text import PolygonZone
from . import CONF_LD2450_ID, LD2450Component
DEPENDENCIES = ["ld2450"]
@@ -27,18 +19,6 @@ ICON_MEDITATION = "mdi:meditation"
ICON_SHIELD_ACCOUNT = "mdi:shield-account"
ICON_TARGET_ACCOUNT = "mdi:target-account"
def _reject_filters(config: ConfigType) -> ConfigType:
# Clearing a polygon makes presence unknown without going through filters, so they would get out of step
if CONF_FILTERS in config:
raise cv.Invalid(
"Polygon zone presence binary sensors can't have filters; use the ld2450 number platform's "
"'presence_timeout' to keep presence on after targets leave",
path=[CONF_FILTERS],
)
return config
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
@@ -57,16 +37,6 @@ CONFIG_SCHEMA = {
filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_MEDITATION,
),
cv.Optional(CONF_POLYGON_ZONES): cv.ensure_list(
binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
).extend(
{
cv.Required(CONF_POLYGON_ZONE_ID): cv.use_id(PolygonZone),
}
),
_reject_filters,
),
}
@@ -76,7 +46,3 @@ async def to_code(config: ConfigType) -> None:
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
for presence_conf in config.get(CONF_POLYGON_ZONES, []):
presence = await binary_sensor.new_binary_sensor(presence_conf)
zone = await cg.get_variable(presence_conf[CONF_POLYGON_ZONE_ID])
cg.add(zone.set_presence_binary_sensor(presence))
+3 -31
View File
@@ -179,11 +179,6 @@ void LD2450Component::setup() {
this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
this->set_presence_timeout();
}
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
for (PolygonZone *zone : this->polygon_zones_) {
zone->setup();
}
#endif
this->restart_and_read_all_info();
}
@@ -270,12 +265,6 @@ void LD2450Component::dump_config() {
LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
LOG_BUTTON(" ", "Restart", this->restart_button_);
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
ESP_LOGCONFIG(TAG, "Polygon Zones:");
for (PolygonZone *zone : this->polygon_zones_) {
zone->dump_config();
}
#endif
}
void LD2450Component::loop() {
@@ -476,15 +465,15 @@ void LD2450Component::handle_periodic_data_() {
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
// Loop thru targets
for (index = 0; index < MAX_TARGETS; index++) {
#ifdef USE_SENSOR
// X
start = TARGET_X + index * 8;
is_moving = false;
// tx and ty are used for further calculations, so always need to be populated
// tx is used for further calculations, so always needs to be populated
tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
// Y
start = TARGET_Y + index * 8;
ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
#ifdef USE_SENSOR
// RESOLUTION
start = TARGET_RESOLUTION + index * 8;
res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
@@ -553,23 +542,6 @@ void LD2450Component::handle_periodic_data_() {
still_target_count = target_count - moving_target_count;
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
const uint32_t now = App.get_loop_component_start_time();
for (PolygonZone *zone : this->polygon_zones_) {
if (!zone->has_polygon())
continue;
bool target_inside = false;
for (const Target &target : this->target_info_) {
// Untracked targets are stored as (0, 0)
if ((target.x != 0 || target.y != 0) && zone->contains(target.x, target.y)) {
target_inside = true;
break;
}
}
zone->update(target_inside, now, this->get_timeout_status_(zone->get_last_inside_ms()));
}
#endif
#ifdef USE_SENSOR
// Loop thru zones
uint8_t zone_still_targets = 0;
@@ -630,7 +602,7 @@ void LD2450Component::handle_periodic_data_() {
}
}
#endif
#ifdef USE_BINARY_SENSOR
#ifdef USE_SENSOR
// For presence timeout check
if (target_count > 0) {
this->presence_millis_ = App.get_loop_component_start_time();
+1 -12
View File
@@ -24,10 +24,6 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
#include "polygon_zone.h"
#endif
#include "esphome/components/ld24xx/ld24xx.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/helpers.h"
@@ -141,9 +137,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s);
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
void register_polygon_zone(PolygonZone *zone) { this->polygon_zones_.push_back(zone); }
#endif
void reset_radar_zone();
void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2,
@@ -174,7 +167,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0;
uint32_t timeout_ = DEFAULT_PRESENCE_TIMEOUT * 1000; // presence_timeout, in ms
uint32_t timeout_ = 5;
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t mac_address_[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
@@ -207,10 +200,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
StaticVector<PolygonZone *, LD2450_POLYGON_ZONE_COUNT> polygon_zones_;
#endif
LazyCallbackManager<void()> data_callback_;
};

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