mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into 5_4_2
This commit is contained in:
		| @@ -34,6 +34,7 @@ from esphome.const import ( | ||||
|     CONF_PORT, | ||||
|     CONF_SUBSTITUTIONS, | ||||
|     CONF_TOPIC, | ||||
|     ENV_NOGITIGNORE, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_RP2040, | ||||
| @@ -209,6 +210,9 @@ def wrap_to_code(name, comp): | ||||
|  | ||||
|  | ||||
| def write_cpp(config): | ||||
|     if not get_bool_env(ENV_NOGITIGNORE): | ||||
|         writer.write_gitignore() | ||||
|  | ||||
|     generate_cpp_contents(config) | ||||
|     return write_cpp_file() | ||||
|  | ||||
| @@ -225,10 +229,13 @@ def generate_cpp_contents(config): | ||||
|  | ||||
|  | ||||
| def write_cpp_file(): | ||||
|     writer.write_platformio_project() | ||||
|  | ||||
|     code_s = indent(CORE.cpp_main_section) | ||||
|     writer.write_cpp(code_s) | ||||
|  | ||||
|     from esphome.build_gen import platformio | ||||
|  | ||||
|     platformio.write_project() | ||||
|  | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/build_gen/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/build_gen/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										102
									
								
								esphome/build_gen/platformio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								esphome/build_gen/platformio.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| import os | ||||
|  | ||||
| from esphome.const import __version__ | ||||
| from esphome.core import CORE | ||||
| from esphome.helpers import mkdir_p, read_file, write_file_if_changed | ||||
| from esphome.writer import find_begin_end, update_storage_json | ||||
|  | ||||
| INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ===========" | ||||
| INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============" | ||||
|  | ||||
| INI_BASE_FORMAT = ( | ||||
|     """; Auto generated code by esphome | ||||
|  | ||||
| [common] | ||||
| lib_deps = | ||||
| build_flags = | ||||
| upload_flags = | ||||
|  | ||||
| """, | ||||
|     """ | ||||
|  | ||||
| """, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def format_ini(data: dict[str, str | list[str]]) -> str: | ||||
|     content = "" | ||||
|     for key, value in sorted(data.items()): | ||||
|         if isinstance(value, list): | ||||
|             content += f"{key} =\n" | ||||
|             for x in value: | ||||
|                 content += f"    {x}\n" | ||||
|         else: | ||||
|             content += f"{key} = {value}\n" | ||||
|     return content | ||||
|  | ||||
|  | ||||
| def get_ini_content(): | ||||
|     CORE.add_platformio_option( | ||||
|         "lib_deps", | ||||
|         [x.as_lib_dep for x in CORE.platformio_libraries.values()] | ||||
|         + ["${common.lib_deps}"], | ||||
|     ) | ||||
|     # Sort to avoid changing build flags order | ||||
|     CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) | ||||
|  | ||||
|     # Sort to avoid changing build unflags order | ||||
|     CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) | ||||
|  | ||||
|     # Add extra script for C++ flags | ||||
|     CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) | ||||
|  | ||||
|     content = "[platformio]\n" | ||||
|     content += f"description = ESPHome {__version__}\n" | ||||
|  | ||||
|     content += f"[env:{CORE.name}]\n" | ||||
|     content += format_ini(CORE.platformio_options) | ||||
|  | ||||
|     return content | ||||
|  | ||||
|  | ||||
| def write_ini(content): | ||||
|     update_storage_json() | ||||
|     path = CORE.relative_build_path("platformio.ini") | ||||
|  | ||||
|     if os.path.isfile(path): | ||||
|         text = read_file(path) | ||||
|         content_format = find_begin_end( | ||||
|             text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END | ||||
|         ) | ||||
|     else: | ||||
|         content_format = INI_BASE_FORMAT | ||||
|     full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}" | ||||
|     full_file += INI_AUTO_GENERATE_END + content_format[1] | ||||
|     write_file_if_changed(path, full_file) | ||||
|  | ||||
|  | ||||
| def write_project(): | ||||
|     mkdir_p(CORE.build_path) | ||||
|  | ||||
|     content = get_ini_content() | ||||
|     write_ini(content) | ||||
|  | ||||
|     # Write extra script for C++ specific flags | ||||
|     write_cxx_flags_script() | ||||
|  | ||||
|  | ||||
| CXX_FLAGS_FILE_NAME = "cxx_flags.py" | ||||
| CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags | ||||
| Import("env") | ||||
|  | ||||
| # Add C++ specific flags | ||||
| """ | ||||
|  | ||||
|  | ||||
| def write_cxx_flags_script() -> None: | ||||
|     path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) | ||||
|     contents = CXX_FLAGS_FILE_CONTENTS | ||||
|     if not CORE.is_host: | ||||
|         contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' | ||||
|         contents += "\n" | ||||
|     write_file_if_changed(path, contents) | ||||
| @@ -5,6 +5,7 @@ from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32C5, | ||||
|     VARIANT_ESP32C6, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32S2, | ||||
| @@ -51,82 +52,93 @@ SAMPLING_MODES = { | ||||
|     "max": sampling_mode.MAX, | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
| adc2_channel_t = cg.global_ns.enum("adc2_channel_t") | ||||
| adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True) | ||||
|  | ||||
| adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True) | ||||
|  | ||||
| # pin to adc1 channel mapping | ||||
| # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         36: adc_channel_t.ADC_CHANNEL_0, | ||||
|         37: adc_channel_t.ADC_CHANNEL_1, | ||||
|         38: adc_channel_t.ADC_CHANNEL_2, | ||||
|         39: adc_channel_t.ADC_CHANNEL_3, | ||||
|         32: adc_channel_t.ADC_CHANNEL_4, | ||||
|         33: adc_channel_t.ADC_CHANNEL_5, | ||||
|         34: adc_channel_t.ADC_CHANNEL_6, | ||||
|         35: adc_channel_t.ADC_CHANNEL_7, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # ESP32-C5 ADC1 pin mapping - based on official ESP-IDF documentation | ||||
|     # https://docs.espressif.com/projects/esp-idf/en/latest/esp32c5/api-reference/peripherals/gpio.html | ||||
|     VARIANT_ESP32C5: { | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|         6: adc_channel_t.ADC_CHANNEL_5, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C6: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|         5: adc_channel_t.ADC_CHANNEL_5, | ||||
|         6: adc_channel_t.ADC_CHANNEL_6, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32H2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|         6: adc_channel_t.ADC_CHANNEL_5, | ||||
|         7: adc_channel_t.ADC_CHANNEL_6, | ||||
|         8: adc_channel_t.ADC_CHANNEL_7, | ||||
|         9: adc_channel_t.ADC_CHANNEL_8, | ||||
|         10: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|         6: adc_channel_t.ADC_CHANNEL_5, | ||||
|         7: adc_channel_t.ADC_CHANNEL_6, | ||||
|         8: adc_channel_t.ADC_CHANNEL_7, | ||||
|         9: adc_channel_t.ADC_CHANNEL_8, | ||||
|         10: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| @@ -135,54 +147,56 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
| ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32: { | ||||
|         4: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         0: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         2: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         27: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         25: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         26: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         4: adc_channel_t.ADC_CHANNEL_0, | ||||
|         0: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         15: adc_channel_t.ADC_CHANNEL_3, | ||||
|         13: adc_channel_t.ADC_CHANNEL_4, | ||||
|         12: adc_channel_t.ADC_CHANNEL_5, | ||||
|         14: adc_channel_t.ADC_CHANNEL_6, | ||||
|         27: adc_channel_t.ADC_CHANNEL_7, | ||||
|         25: adc_channel_t.ADC_CHANNEL_8, | ||||
|         26: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C2: { | ||||
|         5: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         5: adc_channel_t.ADC_CHANNEL_0, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C3: { | ||||
|         5: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         5: adc_channel_t.ADC_CHANNEL_0, | ||||
|     }, | ||||
|     # ESP32-C5 has no ADC2 channels | ||||
|     VARIANT_ESP32C5: {},  # no ADC2 | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C6: {},  # no ADC2 | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32H2: {},  # no ADC2 | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S2: { | ||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         11: adc_channel_t.ADC_CHANNEL_0, | ||||
|         12: adc_channel_t.ADC_CHANNEL_1, | ||||
|         13: adc_channel_t.ADC_CHANNEL_2, | ||||
|         14: adc_channel_t.ADC_CHANNEL_3, | ||||
|         15: adc_channel_t.ADC_CHANNEL_4, | ||||
|         16: adc_channel_t.ADC_CHANNEL_5, | ||||
|         17: adc_channel_t.ADC_CHANNEL_6, | ||||
|         18: adc_channel_t.ADC_CHANNEL_7, | ||||
|         19: adc_channel_t.ADC_CHANNEL_8, | ||||
|         20: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S3: { | ||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         11: adc_channel_t.ADC_CHANNEL_0, | ||||
|         12: adc_channel_t.ADC_CHANNEL_1, | ||||
|         13: adc_channel_t.ADC_CHANNEL_2, | ||||
|         14: adc_channel_t.ADC_CHANNEL_3, | ||||
|         15: adc_channel_t.ADC_CHANNEL_4, | ||||
|         16: adc_channel_t.ADC_CHANNEL_5, | ||||
|         17: adc_channel_t.ADC_CHANNEL_6, | ||||
|         18: adc_channel_t.ADC_CHANNEL_7, | ||||
|         19: adc_channel_t.ADC_CHANNEL_8, | ||||
|         20: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,12 +3,15 @@ | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #include <esp_adc_cal.h> | ||||
| #include "driver/adc.h" | ||||
| #endif  // USE_ESP32 | ||||
| #include "esp_adc/adc_cali.h" | ||||
| #include "esp_adc/adc_cali_scheme.h" | ||||
| #include "esp_adc/adc_oneshot.h" | ||||
| #include "hal/adc_types.h"  // This defines ADC_CHANNEL_MAX | ||||
| #endif                      // USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
| @@ -49,36 +52,72 @@ class Aggregator { | ||||
|  | ||||
| class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | ||||
|  public: | ||||
| #ifdef USE_ESP32 | ||||
|   /// Set the attenuation for this pin. Only available on the ESP32. | ||||
|   void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||
|   void set_channel1(adc1_channel_t channel) { | ||||
|     this->channel1_ = channel; | ||||
|     this->channel2_ = ADC2_CHANNEL_MAX; | ||||
|   } | ||||
|   void set_channel2(adc2_channel_t channel) { | ||||
|     this->channel2_ = channel; | ||||
|     this->channel1_ = ADC1_CHANNEL_MAX; | ||||
|   } | ||||
|   void set_autorange(bool autorange) { this->autorange_ = autorange; } | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
|   /// Update ADC values | ||||
|   /// Update the sensor's state by reading the current ADC value. | ||||
|   /// This method is called periodically based on the update interval. | ||||
|   void update() override; | ||||
|   /// Setup ADC | ||||
|  | ||||
|   /// Set up the ADC sensor by initializing hardware and calibration parameters. | ||||
|   /// This method is called once during device initialization. | ||||
|   void setup() override; | ||||
|  | ||||
|   /// Output the configuration details of the ADC sensor for debugging purposes. | ||||
|   /// This method is called during the ESPHome setup process to log the configuration. | ||||
|   void dump_config() override; | ||||
|   /// `HARDWARE_LATE` setup priority | ||||
|  | ||||
|   /// Return the setup priority for this component. | ||||
|   /// Components with higher priority are initialized earlier during setup. | ||||
|   /// @return A float representing the setup priority. | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   /// Set the GPIO pin to be used by the ADC sensor. | ||||
|   /// @param pin Pointer to an InternalGPIOPin representing the ADC input pin. | ||||
|   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } | ||||
|  | ||||
|   /// Enable or disable the output of raw ADC values (unprocessed data). | ||||
|   /// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false). | ||||
|   void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } | ||||
|  | ||||
|   /// Set the number of samples to be taken for ADC readings to improve accuracy. | ||||
|   /// A higher sample count reduces noise but increases the reading time. | ||||
|   /// @param sample_count The number of samples (e.g., 1, 4, 8). | ||||
|   void set_sample_count(uint8_t sample_count); | ||||
|  | ||||
|   /// Set the sampling mode for how multiple ADC samples are combined into a single measurement. | ||||
|   /// | ||||
|   /// When multiple samples are taken (controlled by set_sample_count), they can be combined | ||||
|   /// in one of three ways: | ||||
|   ///   - SamplingMode::AVG: Compute the average (default) | ||||
|   ///   - SamplingMode::MIN: Use the lowest sample value | ||||
|   ///   - SamplingMode::MAX: Use the highest sample value | ||||
|   /// @param sampling_mode The desired sampling mode to use for aggregating ADC samples. | ||||
|   void set_sampling_mode(SamplingMode sampling_mode); | ||||
|  | ||||
|   /// Perform a single ADC sampling operation and return the measured value. | ||||
|   /// This function handles raw readings, calibration, and averaging as needed. | ||||
|   /// @return The sampled value as a float. | ||||
|   float sample() override; | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
|   std::string unique_id() override; | ||||
| #endif  // USE_ESP8266 | ||||
| #ifdef USE_ESP32 | ||||
|   /// Set the ADC attenuation level to adjust the input voltage range. | ||||
|   /// This determines how the ADC interprets input voltages, allowing for greater precision | ||||
|   /// or the ability to measure higher voltages depending on the chosen attenuation level. | ||||
|   /// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11). | ||||
|   void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||
|  | ||||
|   /// Configure the ADC to use a specific channel on a specific ADC unit. | ||||
|   /// This sets the channel for single-shot or continuous ADC measurements. | ||||
|   /// @param unit The ADC unit to use (ADC_UNIT_1 or ADC_UNIT_2). | ||||
|   /// @param channel The ADC channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc. | ||||
|   void set_channel(adc_unit_t unit, adc_channel_t channel) { | ||||
|     this->adc_unit_ = unit; | ||||
|     this->channel_ = channel; | ||||
|   } | ||||
|  | ||||
|   /// Set whether autoranging should be enabled for the ADC. | ||||
|   /// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages. | ||||
|   /// @param autorange Boolean indicating whether to enable autoranging. | ||||
|   void set_autorange(bool autorange) { this->autorange_ = autorange; } | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   void set_is_temperature() { this->is_temperature_ = true; } | ||||
| @@ -90,17 +129,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   InternalGPIOPin *pin_; | ||||
|   SamplingMode sampling_mode_{SamplingMode::AVG}; | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   float sample_autorange_(); | ||||
|   float sample_fixed_attenuation_(); | ||||
|   bool autorange_{false}; | ||||
|   adc_oneshot_unit_handle_t adc_handle_{nullptr}; | ||||
|   adc_cali_handle_t calibration_handle_{nullptr}; | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
|   adc_channel_t channel_; | ||||
|   adc_unit_t adc_unit_; | ||||
|   struct SetupFlags { | ||||
|     uint8_t init_complete : 1; | ||||
|     uint8_t config_complete : 1; | ||||
|     uint8_t handle_init_complete : 1; | ||||
|     uint8_t calibration_complete : 1; | ||||
|     uint8_t reserved : 4; | ||||
|   } setup_flags_{}; | ||||
|   static adc_oneshot_unit_handle_t shared_adc_handles[2]; | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   bool is_temperature_{false}; | ||||
| #endif  // USE_RP2040 | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
|   adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; | ||||
|   adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; | ||||
|   bool autorange_{false}; | ||||
|   esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; | ||||
| #endif  // USE_ESP32 | ||||
| }; | ||||
|  | ||||
| }  // namespace adc | ||||
|   | ||||
| @@ -8,145 +8,315 @@ namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc.esp32"; | ||||
|  | ||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | ||||
| adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr}; | ||||
|  | ||||
| #ifndef SOC_ADC_RTC_MAX_BITWIDTH | ||||
| #if USE_ESP32_VARIANT_ESP32S2 | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; | ||||
| #endif  // USE_ESP32_VARIANT_ESP32S2 | ||||
| #endif  // SOC_ADC_RTC_MAX_BITWIDTH | ||||
|  | ||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; | ||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); | ||||
|     if (!this->autorange_) { | ||||
|       adc1_config_channel_atten(this->channel1_, this->attenuation_); | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     if (!this->autorange_) { | ||||
|       adc2_config_channel_atten(this->channel2_, this->attenuation_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { | ||||
|     auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; | ||||
|     auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, | ||||
|                                               1100,  // default vref | ||||
|                                               &this->cal_characteristics_[i]); | ||||
|     switch (cal_value) { | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_VREF: | ||||
|         ESP_LOGV(TAG, "Using eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_TP: | ||||
|         ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_DEFAULT_VREF: | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
| const LogString *attenuation_to_str(adc_atten_t attenuation) { | ||||
|   switch (attenuation) { | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       return LOG_STR("0 dB"); | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       return LOG_STR("2.5 dB"); | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       return LOG_STR("6 dB"); | ||||
|     case ADC_ATTEN_DB_12_COMPAT: | ||||
|       return LOG_STR("12 dB"); | ||||
|     default: | ||||
|       return LOG_STR("Unknown Attenuation"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   static const char *const ATTEN_AUTO_STR = "auto"; | ||||
|   static const char *const ATTEN_0DB_STR = "0 db"; | ||||
|   static const char *const ATTEN_2_5DB_STR = "2.5 db"; | ||||
|   static const char *const ATTEN_6DB_STR = "6 db"; | ||||
|   static const char *const ATTEN_12DB_STR = "12 db"; | ||||
|   const char *atten_str = ATTEN_AUTO_STR; | ||||
| const LogString *adc_unit_to_str(adc_unit_t unit) { | ||||
|   switch (unit) { | ||||
|     case ADC_UNIT_1: | ||||
|       return LOG_STR("ADC1"); | ||||
|     case ADC_UNIT_2: | ||||
|       return LOG_STR("ADC2"); | ||||
|     default: | ||||
|       return LOG_STR("Unknown ADC Unit"); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|  | ||||
|   if (!this->autorange_) { | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         atten_str = ATTEN_0DB_STR; | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         atten_str = ATTEN_2_5DB_STR; | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         atten_str = ATTEN_6DB_STR; | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_12_COMPAT: | ||||
|         atten_str = ATTEN_12DB_STR; | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|   // Check if another sensor already initialized this ADC unit | ||||
|   if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) { | ||||
|     adc_oneshot_unit_init_cfg_t init_config = {};  // Zero initialize | ||||
|     init_config.unit_id = this->adc_unit_; | ||||
|     init_config.ulp_mode = ADC_ULP_MODE_DISABLE; | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; | ||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || | ||||
|         // USE_ESP32_VARIANT_ESP32H2 | ||||
|     esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_]; | ||||
|  | ||||
|   this->setup_flags_.handle_init_complete = true; | ||||
|  | ||||
|   adc_oneshot_chan_cfg_t config = { | ||||
|       .atten = this->attenuation_, | ||||
|       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
|   }; | ||||
|   esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Error configuring channel: %d", err); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   this->setup_flags_.config_complete = true; | ||||
|  | ||||
|   // Initialize ADC calibration | ||||
|   if (this->calibration_handle_ == nullptr) { | ||||
|     adc_cali_handle_t handle = nullptr; | ||||
|     esp_err_t err; | ||||
|  | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||
|     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     // RISC-V variants and S3 use curve fitting calibration | ||||
|     adc_cali_curve_fitting_config_t cali_config = {};  // Zero initialize first | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|     cali_config.chan = this->channel_; | ||||
| #endif  // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|     cali_config.unit_id = this->adc_unit_; | ||||
|     cali_config.atten = this->attenuation_; | ||||
|     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; | ||||
|  | ||||
|     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); | ||||
|     if (err == ESP_OK) { | ||||
|       this->calibration_handle_ = handle; | ||||
|       this->setup_flags_.calibration_complete = true; | ||||
|       ESP_LOGV(TAG, "Using curve fitting calibration"); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); | ||||
|       this->setup_flags_.calibration_complete = false; | ||||
|     } | ||||
| #else  // Other ESP32 variants use line fitting calibration | ||||
|     adc_cali_line_fitting_config_t cali_config = { | ||||
|       .unit_id = this->adc_unit_, | ||||
|       .atten = this->attenuation_, | ||||
|       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|       .default_vref = 1100,  // Default reference voltage in mV | ||||
| #endif  // !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|     }; | ||||
|     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); | ||||
|     if (err == ESP_OK) { | ||||
|       this->calibration_handle_ = handle; | ||||
|       this->setup_flags_.calibration_complete = true; | ||||
|       ESP_LOGV(TAG, "Using line fitting calibration"); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); | ||||
|       this->setup_flags_.calibration_complete = false; | ||||
|     } | ||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 | ||||
|   } | ||||
|  | ||||
|   this->setup_flags_.init_complete = true; | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Attenuation: %s\n" | ||||
|                 "  Samples: %i\n" | ||||
|                 "  Channel:       %d\n" | ||||
|                 "  Unit:          %s\n" | ||||
|                 "  Attenuation:   %s\n" | ||||
|                 "  Samples:       %i\n" | ||||
|                 "  Sampling mode: %s", | ||||
|                 atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|                 this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), | ||||
|                 this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, | ||||
|                 LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|  | ||||
|   ESP_LOGCONFIG( | ||||
|       TAG, | ||||
|       "  Setup Status:\n" | ||||
|       "    Handle Init:  %s\n" | ||||
|       "    Config:       %s\n" | ||||
|       "    Calibration:  %s\n" | ||||
|       "    Overall Init: %s", | ||||
|       this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", | ||||
|       this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); | ||||
|  | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample() { | ||||
|   if (!this->autorange_) { | ||||
|     auto aggr = Aggregator(this->sampling_mode_); | ||||
|   if (this->autorange_) { | ||||
|     return this->sample_autorange_(); | ||||
|   } else { | ||||
|     return this->sample_fixed_attenuation_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       int raw = -1; | ||||
|       if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|         raw = adc1_get_raw(this->channel1_); | ||||
|       } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); | ||||
|       } | ||||
|       if (raw == -1) { | ||||
|         return NAN; | ||||
|       } | ||||
| float ADCSensor::sample_fixed_attenuation_() { | ||||
|   auto aggr = Aggregator(this->sampling_mode_); | ||||
|  | ||||
|       aggr.add_sample(raw); | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|     int raw; | ||||
|     esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||
|  | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "ADC read failed with error %d", err); | ||||
|       continue; | ||||
|     } | ||||
|     if (this->output_raw_) { | ||||
|       return aggr.aggregate(); | ||||
|  | ||||
|     if (raw == -1) { | ||||
|       ESP_LOGW(TAG, "Invalid ADC reading"); | ||||
|       continue; | ||||
|     } | ||||
|     uint32_t mv = | ||||
|         esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); | ||||
|     return mv / 1000.0f; | ||||
|  | ||||
|     aggr.add_sample(raw); | ||||
|   } | ||||
|  | ||||
|   int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|   uint32_t final_value = aggr.aggregate(); | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     raw12 = adc1_get_raw(this->channel1_); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); | ||||
|       raw6 = adc1_get_raw(this->channel1_); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); | ||||
|         raw2 = adc1_get_raw(this->channel1_); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); | ||||
|           raw0 = adc1_get_raw(this->channel1_); | ||||
|         } | ||||
|   if (this->output_raw_) { | ||||
|     return final_value; | ||||
|   } | ||||
|  | ||||
|   if (this->calibration_handle_ != nullptr) { | ||||
|     int voltage_mv; | ||||
|     esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv); | ||||
|     if (err == ESP_OK) { | ||||
|       return voltage_mv / 1000.0f; | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); | ||||
|       if (this->calibration_handle_ != nullptr) { | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||
|     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|         adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||
| #else   // Other ESP32 variants use line fitting calibration | ||||
|         adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 | ||||
|         this->calibration_handle_ = nullptr; | ||||
|       } | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); | ||||
|       adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); | ||||
|           adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); | ||||
|         } | ||||
|   } | ||||
|  | ||||
|   return final_value * 3.3f / 4095.0f; | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample_autorange_() { | ||||
|   // Auto-range mode | ||||
|   auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> { | ||||
|     // First reconfigure the attenuation for this reading | ||||
|     adc_oneshot_chan_cfg_t config = { | ||||
|         .atten = atten, | ||||
|         .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
|     }; | ||||
|  | ||||
|     esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); | ||||
|  | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err); | ||||
|       return {-1, 0.0f}; | ||||
|     } | ||||
|  | ||||
|     // Need to recalibrate for the new attenuation | ||||
|     if (this->calibration_handle_ != nullptr) { | ||||
|       // Delete old calibration handle | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||
|     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|       adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||
| #else | ||||
|       adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||
| #endif | ||||
|       this->calibration_handle_ = nullptr; | ||||
|     } | ||||
|  | ||||
|     // Create new calibration handle for this attenuation | ||||
|     adc_cali_handle_t handle = nullptr; | ||||
|  | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||
|     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     adc_cali_curve_fitting_config_t cali_config = {}; | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|     cali_config.chan = this->channel_; | ||||
| #endif | ||||
|     cali_config.unit_id = this->adc_unit_; | ||||
|     cali_config.atten = atten; | ||||
|     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; | ||||
|  | ||||
|     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); | ||||
| #else | ||||
|     adc_cali_line_fitting_config_t cali_config = { | ||||
|       .unit_id = this->adc_unit_, | ||||
|       .atten = atten, | ||||
|       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|       .default_vref = 1100, | ||||
| #endif | ||||
|     }; | ||||
|     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); | ||||
| #endif | ||||
|  | ||||
|     int raw; | ||||
|     err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||
|  | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); | ||||
|       if (handle != nullptr) { | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||
|     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|         adc_cali_delete_scheme_curve_fitting(handle); | ||||
| #else | ||||
|         adc_cali_delete_scheme_line_fitting(handle); | ||||
| #endif | ||||
|       } | ||||
|       return {-1, 0.0f}; | ||||
|     } | ||||
|  | ||||
|     float voltage = 0.0f; | ||||
|     if (handle != nullptr) { | ||||
|       int voltage_mv; | ||||
|       err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); | ||||
|       if (err == ESP_OK) { | ||||
|         voltage = voltage_mv / 1000.0f; | ||||
|       } else { | ||||
|         voltage = raw * 3.3f / 4095.0f; | ||||
|       } | ||||
|       // Clean up calibration handle | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||
|     USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|       adc_cali_delete_scheme_curve_fitting(handle); | ||||
| #else | ||||
|       adc_cali_delete_scheme_line_fitting(handle); | ||||
| #endif | ||||
|     } else { | ||||
|       voltage = raw * 3.3f / 4095.0f; | ||||
|     } | ||||
|  | ||||
|     return {raw, voltage}; | ||||
|   }; | ||||
|  | ||||
|   auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12); | ||||
|   if (raw12 == -1) { | ||||
|     ESP_LOGE(TAG, "Failed to read ADC in autorange mode"); | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   int raw6 = 4095, raw2 = 4095, raw0 = 4095; | ||||
|   float mv6 = 0, mv2 = 0, mv0 = 0; | ||||
|  | ||||
|   if (raw12 < 4095) { | ||||
|     auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6); | ||||
|     raw6 = raw6_val; | ||||
|     mv6 = mv6_val; | ||||
|  | ||||
|     if (raw6 < 4095 && raw6 != -1) { | ||||
|       auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5); | ||||
|       raw2 = raw2_val; | ||||
|       mv2 = mv2_val; | ||||
|  | ||||
|       if (raw2 < 4095 && raw2 != -1) { | ||||
|         auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0); | ||||
|         raw0 = raw0_val; | ||||
|         mv0 = mv0_val; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -155,19 +325,19 @@ float ADCSensor::sample() { | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); | ||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   uint32_t c12 = std::min(raw12, ADC_HALF); | ||||
|   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); | ||||
|   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); | ||||
|   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); | ||||
|   const int adc_half = 2048; | ||||
|   uint32_t c12 = std::min(raw12, adc_half); | ||||
|   uint32_t c6 = adc_half - std::abs(raw6 - adc_half); | ||||
|   uint32_t c2 = adc_half - std::abs(raw2 - adc_half); | ||||
|   uint32_t c0 = std::min(4095 - raw0, adc_half); | ||||
|   uint32_t csum = c12 + c6 + c2 + c0; | ||||
|  | ||||
|   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
|   if (csum == 0) { | ||||
|     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; | ||||
| } | ||||
|  | ||||
| }  // namespace adc | ||||
|   | ||||
| @@ -56,8 +56,6 @@ float ADCSensor::sample() { | ||||
|   return aggr.aggregate() / 1024.0f; | ||||
| } | ||||
|  | ||||
| std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | ||||
|  | ||||
| }  // namespace adc | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -10,13 +10,11 @@ from esphome.const import ( | ||||
|     CONF_NUMBER, | ||||
|     CONF_PIN, | ||||
|     CONF_RAW, | ||||
|     CONF_WIFI, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| from . import ( | ||||
|     ATTENUATION_MODES, | ||||
| @@ -24,6 +22,7 @@ from . import ( | ||||
|     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, | ||||
|     SAMPLING_MODES, | ||||
|     adc_ns, | ||||
|     adc_unit_t, | ||||
|     validate_adc_pin, | ||||
| ) | ||||
|  | ||||
| @@ -57,21 +56,6 @@ def validate_config(config): | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def final_validate_config(config): | ||||
|     if CORE.is_esp32: | ||||
|         variant = get_esp32_variant() | ||||
|         if ( | ||||
|             CONF_WIFI in fv.full_config.get() | ||||
|             and config[CONF_PIN][CONF_NUMBER] | ||||
|             in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             raise cv.Invalid( | ||||
|                 f"{variant} doesn't support ADC on this pin when Wi-Fi is configured" | ||||
|             ) | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| ADCSensor = adc_ns.class_( | ||||
|     "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler | ||||
| ) | ||||
| @@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|     validate_config, | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = final_validate_config | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
| @@ -119,13 +101,13 @@ async def to_code(config): | ||||
|     cg.add(var.set_sample_count(config[CONF_SAMPLES])) | ||||
|     cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) | ||||
|  | ||||
|     if attenuation := config.get(CONF_ATTENUATION): | ||||
|         if attenuation == "auto": | ||||
|             cg.add(var.set_autorange(cg.global_ns.true)) | ||||
|         else: | ||||
|             cg.add(var.set_attenuation(attenuation)) | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         if attenuation := config.get(CONF_ATTENUATION): | ||||
|             if attenuation == "auto": | ||||
|                 cg.add(var.set_autorange(cg.global_ns.true)) | ||||
|             else: | ||||
|                 cg.add(var.set_attenuation(attenuation)) | ||||
|  | ||||
|         variant = get_esp32_variant() | ||||
|         pin_num = config[CONF_PIN][CONF_NUMBER] | ||||
|         if ( | ||||
| @@ -133,10 +115,10 @@ async def to_code(config): | ||||
|             and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] | ||||
|             cg.add(var.set_channel1(chan)) | ||||
|             cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan)) | ||||
|         elif ( | ||||
|             variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL | ||||
|             and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] | ||||
|             cg.add(var.set_channel2(chan)) | ||||
|             cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan)) | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| CODEOWNERS = ["@jeromelaban"] | ||||
| CODEOWNERS = ["@jeromelaban", "@precurse"] | ||||
|   | ||||
| @@ -73,11 +73,29 @@ void AirthingsWavePlus::dump_config() { | ||||
|   LOG_SENSOR("  ", "Illuminance", this->illuminance_sensor_); | ||||
| } | ||||
|  | ||||
| AirthingsWavePlus::AirthingsWavePlus() { | ||||
|   this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); | ||||
|   this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); | ||||
| void AirthingsWavePlus::setup() { | ||||
|   const char *service_uuid; | ||||
|   const char *characteristic_uuid; | ||||
|   const char *access_control_point_characteristic_uuid; | ||||
|  | ||||
|   // Change UUIDs for Wave Radon Gen2 | ||||
|   switch (this->wave_device_type_) { | ||||
|     case WaveDeviceType::WAVE_GEN2: | ||||
|       service_uuid = SERVICE_UUID_WAVE_RADON_GEN2; | ||||
|       characteristic_uuid = CHARACTERISTIC_UUID_WAVE_RADON_GEN2; | ||||
|       access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2; | ||||
|       break; | ||||
|     default: | ||||
|       // Wave Plus | ||||
|       service_uuid = SERVICE_UUID; | ||||
|       characteristic_uuid = CHARACTERISTIC_UUID; | ||||
|       access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID; | ||||
|   } | ||||
|  | ||||
|   this->service_uuid_ = espbt::ESPBTUUID::from_raw(service_uuid); | ||||
|   this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(characteristic_uuid); | ||||
|   this->access_control_point_characteristic_uuid_ = | ||||
|       espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); | ||||
|       espbt::ESPBTUUID::from_raw(access_control_point_characteristic_uuid); | ||||
| } | ||||
|  | ||||
| }  // namespace airthings_wave_plus | ||||
|   | ||||
| @@ -9,13 +9,20 @@ namespace airthings_wave_plus { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| enum WaveDeviceType : uint8_t { WAVE_PLUS = 0, WAVE_GEN2 = 1 }; | ||||
|  | ||||
| static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba"; | ||||
|  | ||||
| static const char *const SERVICE_UUID_WAVE_RADON_GEN2 = "b42e4a8e-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = "b42e4dcc-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = | ||||
|     "b42e50d8-ade7-11e4-89d3-123b93f75cba"; | ||||
|  | ||||
| class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { | ||||
|  public: | ||||
|   AirthingsWavePlus(); | ||||
|   void setup() override; | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
| @@ -23,12 +30,14 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { | ||||
|   void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } | ||||
|   void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||
|   void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } | ||||
|   void set_device_type(WaveDeviceType wave_device_type) { wave_device_type_ = wave_device_type; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_valid_radon_value_(uint16_t radon); | ||||
|   bool is_valid_co2_value_(uint16_t co2); | ||||
|  | ||||
|   void read_sensors(uint8_t *raw_value, uint16_t value_len) override; | ||||
|   WaveDeviceType wave_device_type_{WaveDeviceType::WAVE_PLUS}; | ||||
|  | ||||
|   sensor::Sensor *radon_sensor_{nullptr}; | ||||
|   sensor::Sensor *radon_long_term_sensor_{nullptr}; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ from esphome.const import ( | ||||
|     CONF_ILLUMINANCE, | ||||
|     CONF_RADON, | ||||
|     CONF_RADON_LONG_TERM, | ||||
|     CONF_TVOC, | ||||
|     DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     ICON_RADIOACTIVE, | ||||
| @@ -15,6 +16,7 @@ from esphome.const import ( | ||||
|     UNIT_LUX, | ||||
|     UNIT_PARTS_PER_MILLION, | ||||
| ) | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| DEPENDENCIES = airthings_wave_base.DEPENDENCIES | ||||
|  | ||||
| @@ -25,35 +27,59 @@ AirthingsWavePlus = airthings_wave_plus_ns.class_( | ||||
|     "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase | ||||
| ) | ||||
|  | ||||
| CONF_DEVICE_TYPE = "device_type" | ||||
| WaveDeviceType = airthings_wave_plus_ns.enum("WaveDeviceType") | ||||
| DEVICE_TYPES = { | ||||
|     "WAVE_PLUS": WaveDeviceType.WAVE_PLUS, | ||||
|     "WAVE_GEN2": WaveDeviceType.WAVE_GEN2, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||
|         cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|             icon=ICON_RADIOACTIVE, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|             icon=ICON_RADIOACTIVE, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||
|             accuracy_decimals=0, | ||||
|             device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_LUX, | ||||
|             accuracy_decimals=0, | ||||
|             device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|     } | ||||
|  | ||||
| def validate_wave_gen2_config(config: ConfigType) -> ConfigType: | ||||
|     """Validate that Wave Gen2 devices don't have CO2 or TVOC sensors.""" | ||||
|     if config[CONF_DEVICE_TYPE] == "WAVE_GEN2": | ||||
|         if CONF_CO2 in config: | ||||
|             raise cv.Invalid("Wave Gen2 devices do not support CO2 sensor") | ||||
|         # Check for TVOC in the base schema config | ||||
|         if CONF_TVOC in config: | ||||
|             raise cv.Invalid("Wave Gen2 devices do not support TVOC sensor") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     airthings_wave_base.BASE_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||
|             cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|                 icon=ICON_RADIOACTIVE, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|                 icon=ICON_RADIOACTIVE, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_LUX, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_DEVICE_TYPE, default="WAVE_PLUS"): cv.enum( | ||||
|                 DEVICE_TYPES, upper=True | ||||
|             ), | ||||
|         } | ||||
|     ), | ||||
|     validate_wave_gen2_config, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -73,3 +99,4 @@ async def to_code(config): | ||||
|     if config_illuminance := config.get(CONF_ILLUMINANCE): | ||||
|         sens = await sensor.new_sensor(config_illuminance) | ||||
|         cg.add(var.set_illuminance(sens)) | ||||
|     cg.add(var.set_device_type(config[CONF_DEVICE_TYPE])) | ||||
|   | ||||
| @@ -23,7 +23,7 @@ void APDS9960::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (id != 0xAB && id != 0x9C && id != 0xA8) {  // APDS9960 all should have one of these IDs | ||||
|   if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) {  // APDS9960 all should have one of these IDs | ||||
|     this->error_code_ = WRONG_ID; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   | ||||
| @@ -24,8 +24,9 @@ from esphome.const import ( | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VARIABLES, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
|  | ||||
| DOMAIN = "api" | ||||
| DEPENDENCIES = ["network"] | ||||
| AUTO_LOAD = ["socket"] | ||||
| CODEOWNERS = ["@OttoWinter"] | ||||
| @@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
| } | ||||
| CONF_ENCRYPTION = "encryption" | ||||
| CONF_BATCH_DELAY = "batch_delay" | ||||
| CONF_CUSTOM_SERVICES = "custom_services" | ||||
|  | ||||
|  | ||||
| def validate_encryption_key(value): | ||||
| @@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 cv.positive_time_period_milliseconds, | ||||
|                 cv.Range(max=cv.TimePeriod(milliseconds=65535)), | ||||
|             ), | ||||
|             cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
| @@ -139,8 +142,11 @@ async def to_code(config): | ||||
|     cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) | ||||
|     cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) | ||||
|  | ||||
|     # Set USE_API_SERVICES if any services are enabled | ||||
|     if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_SERVICES") | ||||
|  | ||||
|     if actions := config.get(CONF_ACTIONS, []): | ||||
|         cg.add_define("USE_API_YAML_SERVICES") | ||||
|         for conf in actions: | ||||
|             template_args = [] | ||||
|             func_args = [] | ||||
| @@ -317,7 +323,11 @@ async def api_connected_to_code(config, condition_id, template_arg, args): | ||||
|  | ||||
|  | ||||
| def FILTER_SOURCE_FILES() -> list[str]: | ||||
|     """Filter out api_pb2_dump.cpp when proto message dumping is not enabled.""" | ||||
|     """Filter out api_pb2_dump.cpp when proto message dumping is not enabled, | ||||
|     user_services.cpp when no services are defined, and protocol-specific | ||||
|     implementations based on encryption configuration.""" | ||||
|     files_to_filter: list[str] = [] | ||||
|  | ||||
|     # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined | ||||
|     # This is a particularly large file that still needs to be opened and read | ||||
|     # all the way to the end even when ifdef'd out | ||||
| @@ -325,6 +335,23 @@ def FILTER_SOURCE_FILES() -> list[str]: | ||||
|     # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set, | ||||
|     # which happens when the logger level is VERY_VERBOSE | ||||
|     if get_logger_level() != "VERY_VERBOSE": | ||||
|         return ["api_pb2_dump.cpp"] | ||||
|         files_to_filter.append("api_pb2_dump.cpp") | ||||
|  | ||||
|     return [] | ||||
|     # user_services.cpp is only needed when services are defined | ||||
|     config = CORE.config.get(DOMAIN, {}) | ||||
|     if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]: | ||||
|         files_to_filter.append("user_services.cpp") | ||||
|  | ||||
|     # Filter protocol-specific implementations based on encryption configuration | ||||
|     encryption_config = config.get(CONF_ENCRYPTION) if config else None | ||||
|  | ||||
|     # If encryption is not configured at all, we only need plaintext | ||||
|     if encryption_config is None: | ||||
|         files_to_filter.append("api_frame_helper_noise.cpp") | ||||
|     # If encryption is configured with a key, we only need noise | ||||
|     elif encryption_config.get(CONF_KEY): | ||||
|         files_to_filter.append("api_frame_helper_plaintext.cpp") | ||||
|     # If encryption is configured but no key is provided, we need both | ||||
|     # (this allows a plaintext client to provide a noise key) | ||||
|  | ||||
|     return files_to_filter | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -16,10 +16,34 @@ | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| // Client information structure | ||||
| struct ClientInfo { | ||||
|   std::string name;      // Client name from Hello message | ||||
|   std::string peername;  // IP:port from socket | ||||
|  | ||||
|   std::string get_combined_info() const { | ||||
|     if (name == peername) { | ||||
|       // Before Hello message, both are the same | ||||
|       return name; | ||||
|     } | ||||
|     return name + " (" + peername + ")"; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Keepalive timeout in milliseconds | ||||
| static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; | ||||
| // Maximum number of entities to process in a single batch during initial state/info sending | ||||
| static constexpr size_t MAX_INITIAL_PER_BATCH = 20; | ||||
| // This was increased from 20 to 24 after removing the unique_id field from entity info messages, | ||||
| // which reduced message sizes allowing more entities per batch without exceeding packet limits | ||||
| static constexpr size_t MAX_INITIAL_PER_BATCH = 24; | ||||
| // Maximum number of packets to process in a single batch (platform-dependent) | ||||
| // This limit exists to prevent stack overflow from the PacketInfo array in process_batch_ | ||||
| // Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes | ||||
| #if defined(USE_ESP32) || defined(USE_HOST) | ||||
| static constexpr size_t MAX_PACKETS_PER_BATCH = 64;  // ESP32 has 8KB+ stack, HOST has plenty | ||||
| #else | ||||
| static constexpr size_t MAX_PACKETS_PER_BATCH = 32;  // ESP8266/RP2040/etc have smaller stacks | ||||
| #endif | ||||
|  | ||||
| class APIConnection : public APIServerConnection { | ||||
|  public: | ||||
| @@ -33,7 +57,7 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
|   bool send_list_info_done() { | ||||
|     return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done, | ||||
|                                    ListEntitiesDoneResponse::MESSAGE_TYPE); | ||||
|                                    ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE); | ||||
|   } | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); | ||||
| @@ -111,12 +135,11 @@ class APIConnection : public APIServerConnection { | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
|     if (!this->flags_.service_call_subscription) | ||||
|       return; | ||||
|     this->send_message(call); | ||||
|     this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE); | ||||
|   } | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||
|   void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||
|   bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); | ||||
|  | ||||
|   void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; | ||||
|   void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; | ||||
| @@ -133,7 +156,7 @@ class APIConnection : public APIServerConnection { | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void send_time_request() { | ||||
|     GetTimeRequest req; | ||||
|     this->send_message(req); | ||||
|     this->send_message(req, GetTimeRequest::MESSAGE_TYPE); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| @@ -195,7 +218,9 @@ class APIConnection : public APIServerConnection { | ||||
|     // TODO | ||||
|     return {}; | ||||
|   } | ||||
| #ifdef USE_API_SERVICES | ||||
|   void execute_service(const ExecuteServiceRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|   NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; | ||||
| #endif | ||||
| @@ -207,6 +232,7 @@ class APIConnection : public APIServerConnection { | ||||
|     return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED || | ||||
|            this->is_authenticated(); | ||||
|   } | ||||
|   uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } | ||||
|   void on_fatal_error() override; | ||||
|   void on_unauthenticated_access() override; | ||||
|   void on_no_setup_connection() override; | ||||
| @@ -256,51 +282,54 @@ class APIConnection : public APIServerConnection { | ||||
|   } | ||||
|  | ||||
|   bool try_to_clear_buffer(bool log_out_of_space); | ||||
|   bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; | ||||
|   bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; | ||||
|  | ||||
|   std::string get_client_combined_info() const { | ||||
|     if (this->client_info_ == this->client_peername_) { | ||||
|       // Before Hello message, both are the same (just IP:port) | ||||
|       return this->client_info_; | ||||
|     } | ||||
|     return this->client_info_ + " (" + this->client_peername_ + ")"; | ||||
|   } | ||||
|   std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } | ||||
|  | ||||
|   // Buffer allocator methods for batch processing | ||||
|   ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); | ||||
|   ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); | ||||
|  | ||||
|  protected: | ||||
|   // Helper function to fill common entity info fields | ||||
|   static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { | ||||
|     // Set common fields that are shared by all entity types | ||||
|     response.key = entity->get_object_id_hash(); | ||||
|     response.object_id = entity->get_object_id(); | ||||
|  | ||||
|     if (entity->has_own_name()) | ||||
|       response.name = entity->get_name(); | ||||
|  | ||||
|     // Set common EntityBase properties | ||||
|     response.icon = entity->get_icon(); | ||||
|     response.disabled_by_default = entity->is_disabled_by_default(); | ||||
|     response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); | ||||
| #ifdef USE_DEVICES | ||||
|     response.device_id = entity->get_device_id(); | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   // Helper function to fill common entity state fields | ||||
|   static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) { | ||||
|     response.key = entity->get_object_id_hash(); | ||||
| #ifdef USE_DEVICES | ||||
|     response.device_id = entity->get_device_id(); | ||||
| #endif | ||||
|   } | ||||
|   // Helper function to handle authentication completion | ||||
|   void complete_authentication_(); | ||||
|  | ||||
|   // Non-template helper to encode any ProtoMessage | ||||
|   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, | ||||
|   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, | ||||
|                                            uint32_t remaining_size, bool is_single); | ||||
|  | ||||
|   // Helper to fill entity state base and encode message | ||||
|   static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, | ||||
|                                                APIConnection *conn, uint32_t remaining_size, bool is_single) { | ||||
|     msg.key = entity->get_object_id_hash(); | ||||
| #ifdef USE_DEVICES | ||||
|     msg.device_id = entity->get_device_id(); | ||||
| #endif | ||||
|     return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); | ||||
|   } | ||||
|  | ||||
|   // Helper to fill entity info base and encode message | ||||
|   static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, | ||||
|                                               APIConnection *conn, uint32_t remaining_size, bool is_single) { | ||||
|     // Set common fields that are shared by all entity types | ||||
|     msg.key = entity->get_object_id_hash(); | ||||
|     msg.object_id = entity->get_object_id(); | ||||
|  | ||||
|     if (entity->has_own_name()) | ||||
|       msg.name = entity->get_name(); | ||||
|  | ||||
|       // Set common EntityBase properties | ||||
| #ifdef USE_ENTITY_ICON | ||||
|     msg.icon = entity->get_icon(); | ||||
| #endif | ||||
|     msg.disabled_by_default = entity->is_disabled_by_default(); | ||||
|     msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); | ||||
| #ifdef USE_DEVICES | ||||
|     msg.device_id = entity->get_device_id(); | ||||
| #endif | ||||
|     return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   // Helper to check voice assistant validity and connection ownership | ||||
|   inline bool check_voice_assistant_api_connection_() const; | ||||
| @@ -443,9 +472,6 @@ class APIConnection : public APIServerConnection { | ||||
|   static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
|                                               bool is_single); | ||||
|  | ||||
|   // Helper function to get estimated message size for buffer pre-allocation | ||||
|   static uint16_t get_estimated_message_size(uint16_t message_type); | ||||
|  | ||||
|   // Batch message method for ping requests | ||||
|   static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
|                                         bool is_single); | ||||
| @@ -464,9 +490,8 @@ class APIConnection : public APIServerConnection { | ||||
|   std::unique_ptr<camera::CameraImageReader> image_reader_; | ||||
| #endif | ||||
|  | ||||
|   // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) | ||||
|   std::string client_info_; | ||||
|   std::string client_peername_; | ||||
|   // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) | ||||
|   ClientInfo client_info_; | ||||
|  | ||||
|   // Group 4: 4-byte types | ||||
|   uint32_t last_traffic_; | ||||
| @@ -505,10 +530,10 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
|     // Call operator - uses message_type to determine union type | ||||
|     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, | ||||
|                         uint16_t message_type) const; | ||||
|                         uint8_t message_type) const; | ||||
|  | ||||
|     // Manual cleanup method - must be called before destruction for string types | ||||
|     void cleanup(uint16_t message_type) { | ||||
|     void cleanup(uint8_t message_type) { | ||||
| #ifdef USE_EVENT | ||||
|       if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { | ||||
|         delete data_.string_ptr; | ||||
| @@ -529,11 +554,12 @@ class APIConnection : public APIServerConnection { | ||||
|     struct BatchItem { | ||||
|       EntityBase *entity;      // Entity pointer | ||||
|       MessageCreator creator;  // Function that creates the message when needed | ||||
|       uint16_t message_type;   // Message type for overhead calculation | ||||
|       uint8_t message_type;    // Message type for overhead calculation (max 255) | ||||
|       uint8_t estimated_size;  // Estimated message size (max 255 bytes) | ||||
|  | ||||
|       // Constructor for creating BatchItem | ||||
|       BatchItem(EntityBase *entity, MessageCreator creator, uint16_t message_type) | ||||
|           : entity(entity), creator(std::move(creator)), message_type(message_type) {} | ||||
|       BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) | ||||
|           : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {} | ||||
|     }; | ||||
|  | ||||
|     std::vector<BatchItem> items; | ||||
| @@ -559,9 +585,9 @@ class APIConnection : public APIServerConnection { | ||||
|     } | ||||
|  | ||||
|     // Add item to the batch | ||||
|     void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); | ||||
|     void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); | ||||
|     // Add item to the front of the batch (for high priority messages like ping) | ||||
|     void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); | ||||
|     void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); | ||||
|  | ||||
|     // Clear all items with proper cleanup | ||||
|     void clear() { | ||||
| @@ -630,7 +656,7 @@ class APIConnection : public APIServerConnection { | ||||
|   // to send in one go. This is the maximum size of a single packet | ||||
|   // that can be sent over the network. | ||||
|   // This is to avoid fragmentation of the packet. | ||||
|   static constexpr size_t MAX_PACKET_SIZE = 1390;  // MTU | ||||
|   static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390;  // MTU | ||||
|  | ||||
|   bool schedule_batch_(); | ||||
|   void process_batch_(); | ||||
| @@ -641,9 +667,9 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   // Helper to log a proto message from a MessageCreator object | ||||
|   void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) { | ||||
|   void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) { | ||||
|     this->flags_.log_only_mode = true; | ||||
|     creator(entity, this, MAX_PACKET_SIZE, true, message_type); | ||||
|     creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type); | ||||
|     this->flags_.log_only_mode = false; | ||||
|   } | ||||
|  | ||||
| @@ -654,7 +680,8 @@ class APIConnection : public APIServerConnection { | ||||
| #endif | ||||
|  | ||||
|   // Helper method to send a message either immediately or via batching | ||||
|   bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) { | ||||
|   bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, | ||||
|                            uint8_t estimated_size) { | ||||
|     // Try to send immediately if: | ||||
|     // 1. We should try to send immediately (should_try_send_immediately = true) | ||||
|     // 2. Batch delay is 0 (user has opted in to immediate sending) | ||||
| @@ -662,7 +689,7 @@ class APIConnection : public APIServerConnection { | ||||
|     if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 && | ||||
|         this->helper_->can_write_without_blocking()) { | ||||
|       // Now actually encode and send | ||||
|       if (creator(entity, this, MAX_PACKET_SIZE, true) && | ||||
|       if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && | ||||
|           this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|         // Log the message in verbose mode | ||||
| @@ -675,23 +702,25 @@ class APIConnection : public APIServerConnection { | ||||
|     } | ||||
|  | ||||
|     // Fall back to scheduled batching | ||||
|     return this->schedule_message_(entity, creator, message_type); | ||||
|     return this->schedule_message_(entity, creator, message_type, estimated_size); | ||||
|   } | ||||
|  | ||||
|   // Helper function to schedule a deferred message with known message type | ||||
|   bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { | ||||
|     this->deferred_batch_.add_item(entity, std::move(creator), message_type); | ||||
|   bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { | ||||
|     this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); | ||||
|     return this->schedule_batch_(); | ||||
|   } | ||||
|  | ||||
|   // Overload for function pointers (for info messages and current state reads) | ||||
|   bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { | ||||
|     return schedule_message_(entity, MessageCreator(function_ptr), message_type); | ||||
|   bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type, | ||||
|                          uint8_t estimated_size) { | ||||
|     return schedule_message_(entity, MessageCreator(function_ptr), message_type, estimated_size); | ||||
|   } | ||||
|  | ||||
|   // Helper function to schedule a high priority message at the front of the batch | ||||
|   bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { | ||||
|     this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type); | ||||
|   bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type, | ||||
|                                uint8_t estimated_size) { | ||||
|     this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size); | ||||
|     return this->schedule_batch_(); | ||||
|   } | ||||
| }; | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -8,17 +8,19 @@ | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_NOISE | ||||
| #include "noise/protocol.h" | ||||
| #endif | ||||
|  | ||||
| #include "api_noise_context.h" | ||||
| #include "esphome/components/socket/socket.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| // uncomment to log raw packets | ||||
| //#define HELPER_LOG_PACKETS | ||||
|  | ||||
| // Forward declaration | ||||
| struct ClientInfo; | ||||
|  | ||||
| class ProtoWriteBuffer; | ||||
|  | ||||
| struct ReadPacketBuffer { | ||||
| @@ -30,19 +32,16 @@ struct ReadPacketBuffer { | ||||
|  | ||||
| // Packed packet info structure to minimize memory usage | ||||
| struct PacketInfo { | ||||
|   uint16_t message_type;  // 2 bytes | ||||
|   uint16_t offset;        // 2 bytes (sufficient for packet size ~1460 bytes) | ||||
|   uint16_t payload_size;  // 2 bytes (up to 65535 bytes) | ||||
|   uint16_t padding;       // 2 byte (for alignment) | ||||
|   uint16_t offset;        // Offset in buffer where message starts | ||||
|   uint16_t payload_size;  // Size of the message payload | ||||
|   uint8_t message_type;   // Message type (0-255) | ||||
|  | ||||
|   PacketInfo(uint16_t type, uint16_t off, uint16_t size) | ||||
|       : message_type(type), offset(off), payload_size(size), padding(0) {} | ||||
|   PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} | ||||
| }; | ||||
|  | ||||
| enum class APIError : uint16_t { | ||||
|   OK = 0, | ||||
|   WOULD_BLOCK = 1001, | ||||
|   BAD_HANDSHAKE_PACKET_LEN = 1002, | ||||
|   BAD_INDICATOR = 1003, | ||||
|   BAD_DATA_PACKET = 1004, | ||||
|   TCP_NODELAY_FAILED = 1005, | ||||
| @@ -53,16 +52,19 @@ enum class APIError : uint16_t { | ||||
|   BAD_ARG = 1010, | ||||
|   SOCKET_READ_FAILED = 1011, | ||||
|   SOCKET_WRITE_FAILED = 1012, | ||||
|   OUT_OF_MEMORY = 1018, | ||||
|   CONNECTION_CLOSED = 1022, | ||||
| #ifdef USE_API_NOISE | ||||
|   BAD_HANDSHAKE_PACKET_LEN = 1002, | ||||
|   HANDSHAKESTATE_READ_FAILED = 1013, | ||||
|   HANDSHAKESTATE_WRITE_FAILED = 1014, | ||||
|   HANDSHAKESTATE_BAD_STATE = 1015, | ||||
|   CIPHERSTATE_DECRYPT_FAILED = 1016, | ||||
|   CIPHERSTATE_ENCRYPT_FAILED = 1017, | ||||
|   OUT_OF_MEMORY = 1018, | ||||
|   HANDSHAKESTATE_SETUP_FAILED = 1019, | ||||
|   HANDSHAKESTATE_SPLIT_FAILED = 1020, | ||||
|   BAD_HANDSHAKE_ERROR_BYTE = 1021, | ||||
|   CONNECTION_CLOSED = 1022, | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| const char *api_error_to_str(APIError err); | ||||
| @@ -70,7 +72,8 @@ const char *api_error_to_str(APIError err); | ||||
| class APIFrameHelper { | ||||
|  public: | ||||
|   APIFrameHelper() = default; | ||||
|   explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) { | ||||
|   explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) | ||||
|       : socket_owned_(std::move(socket)), client_info_(client_info) { | ||||
|     socket_ = socket_owned_.get(); | ||||
|   } | ||||
|   virtual ~APIFrameHelper() = default; | ||||
| @@ -96,9 +99,7 @@ class APIFrameHelper { | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } | ||||
|   // Give this helper a name for logging | ||||
|   void set_log_info(std::string info) { info_ = std::move(info); } | ||||
|   virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0; | ||||
|   virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; | ||||
|   // Write multiple protobuf packets in a single operation | ||||
|   // packets contains (message_type, offset, length) for each message in the buffer | ||||
|   // The buffer contains all messages with appropriate padding before each | ||||
| @@ -111,29 +112,28 @@ class APIFrameHelper { | ||||
|   bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } | ||||
|  | ||||
|  protected: | ||||
|   // Struct for holding parsed frame data | ||||
|   struct ParsedFrame { | ||||
|     std::vector<uint8_t> msg; | ||||
|   }; | ||||
|  | ||||
|   // Buffer containing data to be sent | ||||
|   struct SendBuffer { | ||||
|     std::vector<uint8_t> data; | ||||
|     uint16_t offset{0};  // Current offset within the buffer (uint16_t to reduce memory usage) | ||||
|     std::unique_ptr<uint8_t[]> data; | ||||
|     uint16_t size{0};    // Total size of the buffer | ||||
|     uint16_t offset{0};  // Current offset within the buffer | ||||
|  | ||||
|     // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes | ||||
|     uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; } | ||||
|     const uint8_t *current_data() const { return data.data() + offset; } | ||||
|     uint16_t remaining() const { return size - offset; } | ||||
|     const uint8_t *current_data() const { return data.get() + offset; } | ||||
|   }; | ||||
|  | ||||
|   // Common implementation for writing raw data to socket | ||||
|   APIError write_raw_(const struct iovec *iov, int iovcnt); | ||||
|   APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); | ||||
|  | ||||
|   // Try to send data from the tx buffer | ||||
|   APIError try_send_tx_buf_(); | ||||
|  | ||||
|   // Helper method to buffer data from IOVs | ||||
|   void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); | ||||
|   void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset); | ||||
|  | ||||
|   // Common socket write error handling | ||||
|   APIError handle_socket_write_error_(); | ||||
|   template<typename StateEnum> | ||||
|   APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, | ||||
|                       const std::string &info, StateEnum &state, StateEnum failed_state); | ||||
| @@ -163,10 +163,13 @@ class APIFrameHelper { | ||||
|  | ||||
|   // Containers (size varies, but typically 12+ bytes on 32-bit) | ||||
|   std::deque<SendBuffer> tx_buf_; | ||||
|   std::string info_; | ||||
|   std::vector<struct iovec> reusable_iovs_; | ||||
|   std::vector<uint8_t> rx_buf_; | ||||
|  | ||||
|   // Pointer to client info (4 bytes on 32-bit) | ||||
|   // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. | ||||
|   const ClientInfo *client_info_{nullptr}; | ||||
|  | ||||
|   // Group smaller types together | ||||
|   uint16_t rx_buf_len_ = 0; | ||||
|   State state_{State::INITIALIZE}; | ||||
| @@ -181,105 +184,7 @@ class APIFrameHelper { | ||||
|   APIError handle_socket_read_result_(ssize_t received); | ||||
| }; | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| class APINoiseFrameHelper : public APIFrameHelper { | ||||
|  public: | ||||
|   APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx) | ||||
|       : APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) { | ||||
|     // Noise header structure: | ||||
|     // Pos 0: indicator (0x01) | ||||
|     // Pos 1-2: encrypted payload size (16-bit big-endian) | ||||
|     // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) | ||||
|     // Pos 7+: actual payload data | ||||
|     frame_header_padding_ = 7; | ||||
|   } | ||||
|   ~APINoiseFrameHelper() override; | ||||
|   APIError init() override; | ||||
|   APIError loop() override; | ||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||
|   APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; | ||||
|   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; | ||||
|   // Get the frame header padding required by this protocol | ||||
|   uint8_t frame_header_padding() override { return frame_header_padding_; } | ||||
|   // Get the frame footer size required by this protocol | ||||
|   uint8_t frame_footer_size() override { return frame_footer_size_; } | ||||
|  | ||||
|  protected: | ||||
|   APIError state_action_(); | ||||
|   APIError try_read_frame_(ParsedFrame *frame); | ||||
|   APIError write_frame_(const uint8_t *data, uint16_t len); | ||||
|   APIError init_handshake_(); | ||||
|   APIError check_handshake_finished_(); | ||||
|   void send_explicit_handshake_reject_(const std::string &reason); | ||||
|  | ||||
|   // Pointers first (4 bytes each) | ||||
|   NoiseHandshakeState *handshake_{nullptr}; | ||||
|   NoiseCipherState *send_cipher_{nullptr}; | ||||
|   NoiseCipherState *recv_cipher_{nullptr}; | ||||
|  | ||||
|   // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) | ||||
|   std::shared_ptr<APINoiseContext> ctx_; | ||||
|  | ||||
|   // Vector (12 bytes on 32-bit) | ||||
|   std::vector<uint8_t> prologue_; | ||||
|  | ||||
|   // NoiseProtocolId (size depends on implementation) | ||||
|   NoiseProtocolId nid_; | ||||
|  | ||||
|   // Group small types together | ||||
|   // Fixed-size header buffer for noise protocol: | ||||
|   // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) | ||||
|   // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase | ||||
|   uint8_t rx_header_buf_[3]; | ||||
|   uint8_t rx_header_buf_len_ = 0; | ||||
|   // 4 bytes total, no padding | ||||
| }; | ||||
| #endif  // USE_API_NOISE | ||||
|  | ||||
| #ifdef USE_API_PLAINTEXT | ||||
| class APIPlaintextFrameHelper : public APIFrameHelper { | ||||
|  public: | ||||
|   APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) { | ||||
|     // Plaintext header structure (worst case): | ||||
|     // Pos 0: indicator (0x00) | ||||
|     // Pos 1-3: payload size varint (up to 3 bytes) | ||||
|     // Pos 4-5: message type varint (up to 2 bytes) | ||||
|     // Pos 6+: actual payload data | ||||
|     frame_header_padding_ = 6; | ||||
|   } | ||||
|   ~APIPlaintextFrameHelper() override = default; | ||||
|   APIError init() override; | ||||
|   APIError loop() override; | ||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||
|   APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; | ||||
|   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; | ||||
|   uint8_t frame_header_padding() override { return frame_header_padding_; } | ||||
|   // Get the frame footer size required by this protocol | ||||
|   uint8_t frame_footer_size() override { return frame_footer_size_; } | ||||
|  | ||||
|  protected: | ||||
|   APIError try_read_frame_(ParsedFrame *frame); | ||||
|  | ||||
|   // Group 2-byte aligned types | ||||
|   uint16_t rx_header_parsed_type_ = 0; | ||||
|   uint16_t rx_header_parsed_len_ = 0; | ||||
|  | ||||
|   // Group 1-byte types together | ||||
|   // Fixed-size header buffer for plaintext protocol: | ||||
|   // We now store the indicator byte + the two varints. | ||||
|   // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: | ||||
|   // 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint | ||||
|   // | ||||
|   // While varints could theoretically be up to 10 bytes each for 64-bit values, | ||||
|   // attempting to process messages with headers that large would likely crash the | ||||
|   // ESP32 due to memory constraints. | ||||
|   uint8_t rx_header_buf_[6];  // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) | ||||
|   uint8_t rx_header_buf_pos_ = 0; | ||||
|   bool rx_header_parsed_ = false; | ||||
|   // 8 bytes total, no padding needed | ||||
| }; | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|  | ||||
| #endif  // USE_API | ||||
|   | ||||
							
								
								
									
										577
									
								
								esphome/components/api/api_frame_helper_noise.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										577
									
								
								esphome/components/api/api_frame_helper_noise.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,577 @@ | ||||
| #include "api_frame_helper_noise.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_NOISE | ||||
| #include "api_connection.h"  // For ClientInfo struct | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "proto.h" | ||||
| #include <cstring> | ||||
| #include <cinttypes> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| static const char *const TAG = "api.noise"; | ||||
| static const char *const PROLOGUE_INIT = "NoiseAPIInit"; | ||||
|  | ||||
| #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) | ||||
|  | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
| #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) | ||||
| #define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) | ||||
| #else | ||||
| #define LOG_PACKET_RECEIVED(buffer) ((void) 0) | ||||
| #define LOG_PACKET_SENDING(data, len) ((void) 0) | ||||
| #endif | ||||
|  | ||||
| /// Convert a noise error code to a readable error | ||||
| std::string noise_err_to_str(int err) { | ||||
|   if (err == NOISE_ERROR_NO_MEMORY) | ||||
|     return "NO_MEMORY"; | ||||
|   if (err == NOISE_ERROR_UNKNOWN_ID) | ||||
|     return "UNKNOWN_ID"; | ||||
|   if (err == NOISE_ERROR_UNKNOWN_NAME) | ||||
|     return "UNKNOWN_NAME"; | ||||
|   if (err == NOISE_ERROR_MAC_FAILURE) | ||||
|     return "MAC_FAILURE"; | ||||
|   if (err == NOISE_ERROR_NOT_APPLICABLE) | ||||
|     return "NOT_APPLICABLE"; | ||||
|   if (err == NOISE_ERROR_SYSTEM) | ||||
|     return "SYSTEM"; | ||||
|   if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) | ||||
|     return "REMOTE_KEY_REQUIRED"; | ||||
|   if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) | ||||
|     return "LOCAL_KEY_REQUIRED"; | ||||
|   if (err == NOISE_ERROR_PSK_REQUIRED) | ||||
|     return "PSK_REQUIRED"; | ||||
|   if (err == NOISE_ERROR_INVALID_LENGTH) | ||||
|     return "INVALID_LENGTH"; | ||||
|   if (err == NOISE_ERROR_INVALID_PARAM) | ||||
|     return "INVALID_PARAM"; | ||||
|   if (err == NOISE_ERROR_INVALID_STATE) | ||||
|     return "INVALID_STATE"; | ||||
|   if (err == NOISE_ERROR_INVALID_NONCE) | ||||
|     return "INVALID_NONCE"; | ||||
|   if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) | ||||
|     return "INVALID_PRIVATE_KEY"; | ||||
|   if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) | ||||
|     return "INVALID_PUBLIC_KEY"; | ||||
|   if (err == NOISE_ERROR_INVALID_FORMAT) | ||||
|     return "INVALID_FORMAT"; | ||||
|   if (err == NOISE_ERROR_INVALID_SIGNATURE) | ||||
|     return "INVALID_SIGNATURE"; | ||||
|   return to_string(err); | ||||
| } | ||||
|  | ||||
| /// Initialize the frame helper, returns OK if successful. | ||||
| APIError APINoiseFrameHelper::init() { | ||||
|   APIError err = init_common_(); | ||||
|   if (err != APIError::OK) { | ||||
|     return err; | ||||
|   } | ||||
|  | ||||
|   // init prologue | ||||
|   prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); | ||||
|  | ||||
|   state_ = State::CLIENT_HELLO; | ||||
|   return APIError::OK; | ||||
| } | ||||
| // Helper for handling handshake frame errors | ||||
| APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { | ||||
|   if (aerr == APIError::BAD_INDICATOR) { | ||||
|     send_explicit_handshake_reject_("Bad indicator byte"); | ||||
|   } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||
|     send_explicit_handshake_reject_("Bad handshake packet len"); | ||||
|   } | ||||
|   return aerr; | ||||
| } | ||||
|  | ||||
| // Helper for handling noise library errors | ||||
| APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); | ||||
|     return api_err; | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| /// Run through handshake messages (if in that phase) | ||||
| APIError APINoiseFrameHelper::loop() { | ||||
|   // During handshake phase, process as many actions as possible until we can't progress | ||||
|   // socket_->ready() stays true until next main loop, but state_action() will return | ||||
|   // WOULD_BLOCK when no more data is available to read | ||||
|   while (state_ != State::DATA && this->socket_->ready()) { | ||||
|     APIError err = state_action_(); | ||||
|     if (err == APIError::WOULD_BLOCK) { | ||||
|       break; | ||||
|     } | ||||
|     if (err != APIError::OK) { | ||||
|       return err; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Use base class implementation for buffer sending | ||||
|   return APIFrameHelper::loop(); | ||||
| } | ||||
|  | ||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||
|  * | ||||
|  * @param frame: The struct to hold the frame information in. | ||||
|  *   msg_start: points to the start of the payload - this pointer is only valid until the next | ||||
|  *     try_receive_raw_ call | ||||
|  * | ||||
|  * @return 0 if a full packet is in rx_buf_ | ||||
|  * @return -1 if error, check errno. | ||||
|  * | ||||
|  * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. | ||||
|  * errno ENOMEM: Not enough memory for reading packet. | ||||
|  * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. | ||||
|  * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) { | ||||
|   if (frame == nullptr) { | ||||
|     HELPER_LOG("Bad argument for try_read_frame_"); | ||||
|     return APIError::BAD_ARG; | ||||
|   } | ||||
|  | ||||
|   // read header | ||||
|   if (rx_header_buf_len_ < 3) { | ||||
|     // no header information yet | ||||
|     uint8_t to_read = 3 - rx_header_buf_len_; | ||||
|     ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); | ||||
|     APIError err = handle_socket_read_result_(received); | ||||
|     if (err != APIError::OK) { | ||||
|       return err; | ||||
|     } | ||||
|     rx_header_buf_len_ += static_cast<uint8_t>(received); | ||||
|     if (static_cast<uint8_t>(received) != to_read) { | ||||
|       // not a full read | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } | ||||
|  | ||||
|     if (rx_header_buf_[0] != 0x01) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); | ||||
|       return APIError::BAD_INDICATOR; | ||||
|     } | ||||
|     // header reading done | ||||
|   } | ||||
|  | ||||
|   // read body | ||||
|   uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; | ||||
|  | ||||
|   if (state_ != State::DATA && msg_size > 128) { | ||||
|     // for handshake message only permit up to 128 bytes | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad packet len for handshake: %d", msg_size); | ||||
|     return APIError::BAD_HANDSHAKE_PACKET_LEN; | ||||
|   } | ||||
|  | ||||
|   // reserve space for body | ||||
|   if (rx_buf_.size() != msg_size) { | ||||
|     rx_buf_.resize(msg_size); | ||||
|   } | ||||
|  | ||||
|   if (rx_buf_len_ < msg_size) { | ||||
|     // more data to read | ||||
|     uint16_t to_read = msg_size - rx_buf_len_; | ||||
|     ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); | ||||
|     APIError err = handle_socket_read_result_(received); | ||||
|     if (err != APIError::OK) { | ||||
|       return err; | ||||
|     } | ||||
|     rx_buf_len_ += static_cast<uint16_t>(received); | ||||
|     if (static_cast<uint16_t>(received) != to_read) { | ||||
|       // not all read | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   LOG_PACKET_RECEIVED(rx_buf_); | ||||
|   *frame = std::move(rx_buf_); | ||||
|   // consume msg | ||||
|   rx_buf_ = {}; | ||||
|   rx_buf_len_ = 0; | ||||
|   rx_header_buf_len_ = 0; | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| /** To be called from read/write methods. | ||||
|  * | ||||
|  * This method runs through the internal handshake methods, if in that state. | ||||
|  * | ||||
|  * If the handshake is still active when this method returns and a read/write can't take place at | ||||
|  * the moment, returns WOULD_BLOCK. | ||||
|  * If an error occurred, returns that error. Only returns OK if the transport is ready for data | ||||
|  * traffic. | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::state_action_() { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|   if (state_ == State::INITIALIZE) { | ||||
|     HELPER_LOG("Bad state for method: %d", (int) state_); | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   if (state_ == State::CLIENT_HELLO) { | ||||
|     // waiting for client hello | ||||
|     std::vector<uint8_t> frame; | ||||
|     aerr = try_read_frame_(&frame); | ||||
|     if (aerr != APIError::OK) { | ||||
|       return handle_handshake_frame_error_(aerr); | ||||
|     } | ||||
|     // ignore contents, may be used in future for flags | ||||
|     // Reserve space for: existing prologue + 2 size bytes + frame data | ||||
|     prologue_.reserve(prologue_.size() + 2 + frame.size()); | ||||
|     prologue_.push_back((uint8_t) (frame.size() >> 8)); | ||||
|     prologue_.push_back((uint8_t) frame.size()); | ||||
|     prologue_.insert(prologue_.end(), frame.begin(), frame.end()); | ||||
|  | ||||
|     state_ = State::SERVER_HELLO; | ||||
|   } | ||||
|   if (state_ == State::SERVER_HELLO) { | ||||
|     // send server hello | ||||
|     const std::string &name = App.get_name(); | ||||
|     const std::string &mac = get_mac_address(); | ||||
|  | ||||
|     std::vector<uint8_t> msg; | ||||
|     // Reserve space for: 1 byte proto + name + null + mac + null | ||||
|     msg.reserve(1 + name.size() + 1 + mac.size() + 1); | ||||
|  | ||||
|     // chosen proto | ||||
|     msg.push_back(0x01); | ||||
|  | ||||
|     // node name, terminated by null byte | ||||
|     const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str()); | ||||
|     msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); | ||||
|     // node mac, terminated by null byte | ||||
|     const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str()); | ||||
|     msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); | ||||
|  | ||||
|     aerr = write_frame_(msg.data(), msg.size()); | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|  | ||||
|     // start handshake | ||||
|     aerr = init_handshake_(); | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|  | ||||
|     state_ = State::HANDSHAKE; | ||||
|   } | ||||
|   if (state_ == State::HANDSHAKE) { | ||||
|     int action = noise_handshakestate_get_action(handshake_); | ||||
|     if (action == NOISE_ACTION_READ_MESSAGE) { | ||||
|       // waiting for handshake msg | ||||
|       std::vector<uint8_t> frame; | ||||
|       aerr = try_read_frame_(&frame); | ||||
|       if (aerr != APIError::OK) { | ||||
|         return handle_handshake_frame_error_(aerr); | ||||
|       } | ||||
|  | ||||
|       if (frame.empty()) { | ||||
|         send_explicit_handshake_reject_("Empty handshake message"); | ||||
|         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||
|       } else if (frame[0] != 0x00) { | ||||
|         HELPER_LOG("Bad handshake error byte: %u", frame[0]); | ||||
|         send_explicit_handshake_reject_("Bad handshake error byte"); | ||||
|         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||
|       } | ||||
|  | ||||
|       NoiseBuffer mbuf; | ||||
|       noise_buffer_init(mbuf); | ||||
|       noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); | ||||
|       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); | ||||
|       if (err != 0) { | ||||
|         // Special handling for MAC failure | ||||
|         send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); | ||||
|         return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); | ||||
|       } | ||||
|  | ||||
|       aerr = check_handshake_finished_(); | ||||
|       if (aerr != APIError::OK) | ||||
|         return aerr; | ||||
|     } else if (action == NOISE_ACTION_WRITE_MESSAGE) { | ||||
|       uint8_t buffer[65]; | ||||
|       NoiseBuffer mbuf; | ||||
|       noise_buffer_init(mbuf); | ||||
|       noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); | ||||
|  | ||||
|       err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); | ||||
|       APIError aerr_write = | ||||
|           handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); | ||||
|       if (aerr_write != APIError::OK) | ||||
|         return aerr_write; | ||||
|       buffer[0] = 0x00;  // success | ||||
|  | ||||
|       aerr = write_frame_(buffer, mbuf.size + 1); | ||||
|       if (aerr != APIError::OK) | ||||
|         return aerr; | ||||
|       aerr = check_handshake_finished_(); | ||||
|       if (aerr != APIError::OK) | ||||
|         return aerr; | ||||
|     } else { | ||||
|       // bad state for action | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Bad action for handshake: %d", action); | ||||
|       return APIError::HANDSHAKESTATE_BAD_STATE; | ||||
|     } | ||||
|   } | ||||
|   if (state_ == State::CLOSED || state_ == State::FAILED) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
| void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { | ||||
|   std::vector<uint8_t> data; | ||||
|   data.resize(reason.length() + 1); | ||||
|   data[0] = 0x01;  // failure | ||||
|  | ||||
|   // Copy error message in bulk | ||||
|   if (!reason.empty()) { | ||||
|     std::memcpy(data.data() + 1, reason.c_str(), reason.length()); | ||||
|   } | ||||
|  | ||||
|   // temporarily remove failed state | ||||
|   auto orig_state = state_; | ||||
|   state_ = State::EXPLICIT_REJECT; | ||||
|   write_frame_(data.data(), data.size()); | ||||
|   state_ = orig_state; | ||||
| } | ||||
| APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|   aerr = state_action_(); | ||||
|   if (aerr != APIError::OK) { | ||||
|     return aerr; | ||||
|   } | ||||
|  | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::WOULD_BLOCK; | ||||
|   } | ||||
|  | ||||
|   std::vector<uint8_t> frame; | ||||
|   aerr = try_read_frame_(&frame); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|  | ||||
|   NoiseBuffer mbuf; | ||||
|   noise_buffer_init(mbuf); | ||||
|   noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); | ||||
|   err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); | ||||
|   APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); | ||||
|   if (decrypt_err != APIError::OK) | ||||
|     return decrypt_err; | ||||
|  | ||||
|   uint16_t msg_size = mbuf.size; | ||||
|   uint8_t *msg_data = frame.data(); | ||||
|   if (msg_size < 4) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad data packet: size %d too short", msg_size); | ||||
|     return APIError::BAD_DATA_PACKET; | ||||
|   } | ||||
|  | ||||
|   uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; | ||||
|   uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; | ||||
|   if (data_len > msg_size - 4) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); | ||||
|     return APIError::BAD_DATA_PACKET; | ||||
|   } | ||||
|  | ||||
|   buffer->container = std::move(frame); | ||||
|   buffer->data_offset = 4; | ||||
|   buffer->data_len = data_len; | ||||
|   buffer->type = type; | ||||
|   return APIError::OK; | ||||
| } | ||||
| APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { | ||||
|   // Resize to include MAC space (required for Noise encryption) | ||||
|   buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); | ||||
|   PacketInfo packet{type, 0, | ||||
|                     static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; | ||||
|   return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1)); | ||||
| } | ||||
|  | ||||
| APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) { | ||||
|   APIError aerr = state_action_(); | ||||
|   if (aerr != APIError::OK) { | ||||
|     return aerr; | ||||
|   } | ||||
|  | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::WOULD_BLOCK; | ||||
|   } | ||||
|  | ||||
|   if (packets.empty()) { | ||||
|     return APIError::OK; | ||||
|   } | ||||
|  | ||||
|   std::vector<uint8_t> *raw_buffer = buffer.get_buffer(); | ||||
|   uint8_t *buffer_data = raw_buffer->data();  // Cache buffer pointer | ||||
|  | ||||
|   this->reusable_iovs_.clear(); | ||||
|   this->reusable_iovs_.reserve(packets.size()); | ||||
|   uint16_t total_write_len = 0; | ||||
|  | ||||
|   // We need to encrypt each packet in place | ||||
|   for (const auto &packet : packets) { | ||||
|     // The buffer already has padding at offset | ||||
|     uint8_t *buf_start = buffer_data + packet.offset; | ||||
|  | ||||
|     // Write noise header | ||||
|     buf_start[0] = 0x01;  // indicator | ||||
|     // buf_start[1], buf_start[2] to be set after encryption | ||||
|  | ||||
|     // Write message header (to be encrypted) | ||||
|     const uint8_t msg_offset = 3; | ||||
|     buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8);      // type high byte | ||||
|     buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type);       // type low byte | ||||
|     buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8);  // data_len high byte | ||||
|     buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size);       // data_len low byte | ||||
|     // payload data is already in the buffer starting at offset + 7 | ||||
|  | ||||
|     // Make sure we have space for MAC | ||||
|     // The buffer should already have been sized appropriately | ||||
|  | ||||
|     // Encrypt the message in place | ||||
|     NoiseBuffer mbuf; | ||||
|     noise_buffer_init(mbuf); | ||||
|     noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, | ||||
|                            4 + packet.payload_size + frame_footer_size_); | ||||
|  | ||||
|     int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); | ||||
|     APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|  | ||||
|     // Fill in the encrypted size | ||||
|     buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8); | ||||
|     buf_start[2] = static_cast<uint8_t>(mbuf.size); | ||||
|  | ||||
|     // Add iovec for this encrypted packet | ||||
|     size_t packet_len = static_cast<size_t>(3 + mbuf.size);  // indicator + size + encrypted data | ||||
|     this->reusable_iovs_.push_back({buf_start, packet_len}); | ||||
|     total_write_len += packet_len; | ||||
|   } | ||||
|  | ||||
|   // Send all encrypted packets in one writev call | ||||
|   return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); | ||||
| } | ||||
|  | ||||
| APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { | ||||
|   uint8_t header[3]; | ||||
|   header[0] = 0x01;  // indicator | ||||
|   header[1] = (uint8_t) (len >> 8); | ||||
|   header[2] = (uint8_t) len; | ||||
|  | ||||
|   struct iovec iov[2]; | ||||
|   iov[0].iov_base = header; | ||||
|   iov[0].iov_len = 3; | ||||
|   if (len == 0) { | ||||
|     return this->write_raw_(iov, 1, 3);  // Just header | ||||
|   } | ||||
|   iov[1].iov_base = const_cast<uint8_t *>(data); | ||||
|   iov[1].iov_len = len; | ||||
|  | ||||
|   return this->write_raw_(iov, 2, 3 + len);  // Header + data | ||||
| } | ||||
|  | ||||
| /** Initiate the data structures for the handshake. | ||||
|  * | ||||
|  * @return 0 on success, -1 on error (check errno) | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::init_handshake_() { | ||||
|   int err; | ||||
|   memset(&nid_, 0, sizeof(nid_)); | ||||
|   // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; | ||||
|   // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); | ||||
|   nid_.pattern_id = NOISE_PATTERN_NN; | ||||
|   nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; | ||||
|   nid_.dh_id = NOISE_DH_CURVE25519; | ||||
|   nid_.prefix_id = NOISE_PREFIX_STANDARD; | ||||
|   nid_.hybrid_id = NOISE_DH_NONE; | ||||
|   nid_.hash_id = NOISE_HASH_SHA256; | ||||
|   nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; | ||||
|  | ||||
|   err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); | ||||
|   APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|  | ||||
|   const auto &psk = ctx_->get_psk(); | ||||
|   err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); | ||||
|   aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|  | ||||
|   err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); | ||||
|   aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|   // set_prologue copies it into handshakestate, so we can get rid of it now | ||||
|   prologue_ = {}; | ||||
|  | ||||
|   err = noise_handshakestate_start(handshake_); | ||||
|   aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| APIError APINoiseFrameHelper::check_handshake_finished_() { | ||||
|   assert(state_ == State::HANDSHAKE); | ||||
|  | ||||
|   int action = noise_handshakestate_get_action(handshake_); | ||||
|   if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) | ||||
|     return APIError::OK; | ||||
|   if (action != NOISE_ACTION_SPLIT) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad action for handshake: %d", action); | ||||
|     return APIError::HANDSHAKESTATE_BAD_STATE; | ||||
|   } | ||||
|   int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); | ||||
|   APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|  | ||||
|   frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); | ||||
|  | ||||
|   HELPER_LOG("Handshake complete!"); | ||||
|   noise_handshakestate_free(handshake_); | ||||
|   handshake_ = nullptr; | ||||
|   state_ = State::DATA; | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| APINoiseFrameHelper::~APINoiseFrameHelper() { | ||||
|   if (handshake_ != nullptr) { | ||||
|     noise_handshakestate_free(handshake_); | ||||
|     handshake_ = nullptr; | ||||
|   } | ||||
|   if (send_cipher_ != nullptr) { | ||||
|     noise_cipherstate_free(send_cipher_); | ||||
|     send_cipher_ = nullptr; | ||||
|   } | ||||
|   if (recv_cipher_ != nullptr) { | ||||
|     noise_cipherstate_free(recv_cipher_); | ||||
|     recv_cipher_ = nullptr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| extern "C" { | ||||
| // declare how noise generates random bytes (here with a good HWRNG based on the RF system) | ||||
| void noise_rand_bytes(void *output, size_t len) { | ||||
|   if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) { | ||||
|     ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting"); | ||||
|     arch_restart(); | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API_NOISE | ||||
| #endif  // USE_API | ||||
							
								
								
									
										70
									
								
								esphome/components/api/api_frame_helper_noise.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								esphome/components/api/api_frame_helper_noise.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| #pragma once | ||||
| #include "api_frame_helper.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_NOISE | ||||
| #include "noise/protocol.h" | ||||
| #include "api_noise_context.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| class APINoiseFrameHelper : public APIFrameHelper { | ||||
|  public: | ||||
|   APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx, | ||||
|                       const ClientInfo *client_info) | ||||
|       : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { | ||||
|     // Noise header structure: | ||||
|     // Pos 0: indicator (0x01) | ||||
|     // Pos 1-2: encrypted payload size (16-bit big-endian) | ||||
|     // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) | ||||
|     // Pos 7+: actual payload data | ||||
|     frame_header_padding_ = 7; | ||||
|   } | ||||
|   ~APINoiseFrameHelper() override; | ||||
|   APIError init() override; | ||||
|   APIError loop() override; | ||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||
|   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; | ||||
|   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; | ||||
|   // Get the frame header padding required by this protocol | ||||
|   uint8_t frame_header_padding() override { return frame_header_padding_; } | ||||
|   // Get the frame footer size required by this protocol | ||||
|   uint8_t frame_footer_size() override { return frame_footer_size_; } | ||||
|  | ||||
|  protected: | ||||
|   APIError state_action_(); | ||||
|   APIError try_read_frame_(std::vector<uint8_t> *frame); | ||||
|   APIError write_frame_(const uint8_t *data, uint16_t len); | ||||
|   APIError init_handshake_(); | ||||
|   APIError check_handshake_finished_(); | ||||
|   void send_explicit_handshake_reject_(const std::string &reason); | ||||
|   APIError handle_handshake_frame_error_(APIError aerr); | ||||
|   APIError handle_noise_error_(int err, const char *func_name, APIError api_err); | ||||
|  | ||||
|   // Pointers first (4 bytes each) | ||||
|   NoiseHandshakeState *handshake_{nullptr}; | ||||
|   NoiseCipherState *send_cipher_{nullptr}; | ||||
|   NoiseCipherState *recv_cipher_{nullptr}; | ||||
|  | ||||
|   // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) | ||||
|   std::shared_ptr<APINoiseContext> ctx_; | ||||
|  | ||||
|   // Vector (12 bytes on 32-bit) | ||||
|   std::vector<uint8_t> prologue_; | ||||
|  | ||||
|   // NoiseProtocolId (size depends on implementation) | ||||
|   NoiseProtocolId nid_; | ||||
|  | ||||
|   // Group small types together | ||||
|   // Fixed-size header buffer for noise protocol: | ||||
|   // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) | ||||
|   // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase | ||||
|   uint8_t rx_header_buf_[3]; | ||||
|   uint8_t rx_header_buf_len_ = 0; | ||||
|   // 4 bytes total, no padding | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API_NOISE | ||||
| #endif  // USE_API | ||||
							
								
								
									
										292
									
								
								esphome/components/api/api_frame_helper_plaintext.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								esphome/components/api/api_frame_helper_plaintext.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,292 @@ | ||||
| #include "api_frame_helper_plaintext.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_PLAINTEXT | ||||
| #include "api_connection.h"  // For ClientInfo struct | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "proto.h" | ||||
| #include <cstring> | ||||
| #include <cinttypes> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| static const char *const TAG = "api.plaintext"; | ||||
|  | ||||
| #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) | ||||
|  | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
| #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) | ||||
| #define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) | ||||
| #else | ||||
| #define LOG_PACKET_RECEIVED(buffer) ((void) 0) | ||||
| #define LOG_PACKET_SENDING(data, len) ((void) 0) | ||||
| #endif | ||||
|  | ||||
| /// Initialize the frame helper, returns OK if successful. | ||||
| APIError APIPlaintextFrameHelper::init() { | ||||
|   APIError err = init_common_(); | ||||
|   if (err != APIError::OK) { | ||||
|     return err; | ||||
|   } | ||||
|  | ||||
|   state_ = State::DATA; | ||||
|   return APIError::OK; | ||||
| } | ||||
| APIError APIPlaintextFrameHelper::loop() { | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   // Use base class implementation for buffer sending | ||||
|   return APIFrameHelper::loop(); | ||||
| } | ||||
|  | ||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||
|  * | ||||
|  * @param frame: The struct to hold the frame information in. | ||||
|  *   msg: store the parsed frame in that struct | ||||
|  * | ||||
|  * @return See APIError | ||||
|  * | ||||
|  * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. | ||||
|  */ | ||||
| APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) { | ||||
|   if (frame == nullptr) { | ||||
|     HELPER_LOG("Bad argument for try_read_frame_"); | ||||
|     return APIError::BAD_ARG; | ||||
|   } | ||||
|  | ||||
|   // read header | ||||
|   while (!rx_header_parsed_) { | ||||
|     // Now that we know when the socket is ready, we can read up to 3 bytes | ||||
|     // into the rx_header_buf_ before we have to switch back to reading | ||||
|     // one byte at a time to ensure we don't read past the message and | ||||
|     // into the next one. | ||||
|  | ||||
|     // Read directly into rx_header_buf_ at the current position | ||||
|     // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time | ||||
|     ssize_t received = | ||||
|         this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); | ||||
|     APIError err = handle_socket_read_result_(received); | ||||
|     if (err != APIError::OK) { | ||||
|       return err; | ||||
|     } | ||||
|  | ||||
|     // If this was the first read, validate the indicator byte | ||||
|     if (rx_header_buf_pos_ == 0 && received > 0) { | ||||
|       if (rx_header_buf_[0] != 0x00) { | ||||
|         state_ = State::FAILED; | ||||
|         HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); | ||||
|         return APIError::BAD_INDICATOR; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     rx_header_buf_pos_ += received; | ||||
|  | ||||
|     // Check for buffer overflow | ||||
|     if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Header buffer overflow"); | ||||
|       return APIError::BAD_DATA_PACKET; | ||||
|     } | ||||
|  | ||||
|     // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse | ||||
|     if (rx_header_buf_pos_ < 3) { | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     // At this point, we have at least 3 bytes total: | ||||
|     //   - Validated indicator byte (0x00) stored at position 0 | ||||
|     //   - At least 2 bytes in the buffer for the varints | ||||
|     // Buffer layout: | ||||
|     //   [0]: indicator byte (0x00) | ||||
|     //   [1-3]: Message size varint (variable length) | ||||
|     //     - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535) | ||||
|     //     - 3 bytes allows up to 2097151, ensuring we support at least as much as noise | ||||
|     //   [2-5]: Message type varint (variable length) | ||||
|     // We now attempt to parse both varints. If either is incomplete, | ||||
|     // we'll continue reading more bytes. | ||||
|  | ||||
|     // Skip indicator byte at position 0 | ||||
|     uint8_t varint_pos = 1; | ||||
|     uint32_t consumed = 0; | ||||
|  | ||||
|     auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); | ||||
|     if (!msg_size_varint.has_value()) { | ||||
|       // not enough data there yet | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), | ||||
|                  std::numeric_limits<uint16_t>::max()); | ||||
|       return APIError::BAD_DATA_PACKET; | ||||
|     } | ||||
|     rx_header_parsed_len_ = msg_size_varint->as_uint16(); | ||||
|  | ||||
|     // Move to next varint position | ||||
|     varint_pos += consumed; | ||||
|  | ||||
|     auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); | ||||
|     if (!msg_type_varint.has_value()) { | ||||
|       // not enough data there yet | ||||
|       continue; | ||||
|     } | ||||
|     if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), | ||||
|                  std::numeric_limits<uint16_t>::max()); | ||||
|       return APIError::BAD_DATA_PACKET; | ||||
|     } | ||||
|     rx_header_parsed_type_ = msg_type_varint->as_uint16(); | ||||
|     rx_header_parsed_ = true; | ||||
|   } | ||||
|   // header reading done | ||||
|  | ||||
|   // reserve space for body | ||||
|   if (rx_buf_.size() != rx_header_parsed_len_) { | ||||
|     rx_buf_.resize(rx_header_parsed_len_); | ||||
|   } | ||||
|  | ||||
|   if (rx_buf_len_ < rx_header_parsed_len_) { | ||||
|     // more data to read | ||||
|     uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; | ||||
|     ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); | ||||
|     APIError err = handle_socket_read_result_(received); | ||||
|     if (err != APIError::OK) { | ||||
|       return err; | ||||
|     } | ||||
|     rx_buf_len_ += static_cast<uint16_t>(received); | ||||
|     if (static_cast<uint16_t>(received) != to_read) { | ||||
|       // not all read | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   LOG_PACKET_RECEIVED(rx_buf_); | ||||
|   *frame = std::move(rx_buf_); | ||||
|   // consume msg | ||||
|   rx_buf_ = {}; | ||||
|   rx_buf_len_ = 0; | ||||
|   rx_header_buf_pos_ = 0; | ||||
|   rx_header_parsed_ = false; | ||||
|   return APIError::OK; | ||||
| } | ||||
| APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|   APIError aerr; | ||||
|  | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::WOULD_BLOCK; | ||||
|   } | ||||
|  | ||||
|   std::vector<uint8_t> frame; | ||||
|   aerr = try_read_frame_(&frame); | ||||
|   if (aerr != APIError::OK) { | ||||
|     if (aerr == APIError::BAD_INDICATOR) { | ||||
|       // Make sure to tell the remote that we don't | ||||
|       // understand the indicator byte so it knows | ||||
|       // we do not support it. | ||||
|       struct iovec iov[1]; | ||||
|       // The \x00 first byte is the marker for plaintext. | ||||
|       // | ||||
|       // The remote will know how to handle the indicator byte, | ||||
|       // but it likely won't understand the rest of the message. | ||||
|       // | ||||
|       // We must send at least 3 bytes to be read, so we add | ||||
|       // a message after the indicator byte to ensures its long | ||||
|       // enough and can aid in debugging. | ||||
|       const char msg[] = "\x00" | ||||
|                          "Bad indicator byte"; | ||||
|       iov[0].iov_base = (void *) msg; | ||||
|       iov[0].iov_len = 19; | ||||
|       this->write_raw_(iov, 1, 19); | ||||
|     } | ||||
|     return aerr; | ||||
|   } | ||||
|  | ||||
|   buffer->container = std::move(frame); | ||||
|   buffer->data_offset = 0; | ||||
|   buffer->data_len = rx_header_parsed_len_; | ||||
|   buffer->type = rx_header_parsed_type_; | ||||
|   return APIError::OK; | ||||
| } | ||||
| APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { | ||||
|   PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)}; | ||||
|   return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1)); | ||||
| } | ||||
|  | ||||
| APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) { | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|  | ||||
|   if (packets.empty()) { | ||||
|     return APIError::OK; | ||||
|   } | ||||
|  | ||||
|   std::vector<uint8_t> *raw_buffer = buffer.get_buffer(); | ||||
|   uint8_t *buffer_data = raw_buffer->data();  // Cache buffer pointer | ||||
|  | ||||
|   this->reusable_iovs_.clear(); | ||||
|   this->reusable_iovs_.reserve(packets.size()); | ||||
|   uint16_t total_write_len = 0; | ||||
|  | ||||
|   for (const auto &packet : packets) { | ||||
|     // Calculate varint sizes for header layout | ||||
|     uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size)); | ||||
|     uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type)); | ||||
|     uint8_t total_header_len = 1 + size_varint_len + type_varint_len; | ||||
|  | ||||
|     // Calculate where to start writing the header | ||||
|     // The header starts at the latest possible position to minimize unused padding | ||||
|     // | ||||
|     // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 | ||||
|     // [0-2]  - Unused padding | ||||
|     // [3]    - 0x00 indicator byte | ||||
|     // [4]    - Payload size varint (1 byte, for sizes 0-127) | ||||
|     // [5]    - Message type varint (1 byte, for types 0-127) | ||||
|     // [6...] - Actual payload data | ||||
|     // | ||||
|     // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 | ||||
|     // [0-1]  - Unused padding | ||||
|     // [2]    - 0x00 indicator byte | ||||
|     // [3-4]  - Payload size varint (2 bytes, for sizes 128-16383) | ||||
|     // [5]    - Message type varint (1 byte, for types 0-127) | ||||
|     // [6...] - Actual payload data | ||||
|     // | ||||
|     // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 | ||||
|     // [0]    - 0x00 indicator byte | ||||
|     // [1-3]  - Payload size varint (3 bytes, for sizes 16384-2097151) | ||||
|     // [4-5]  - Message type varint (2 bytes, for types 128-32767) | ||||
|     // [6...] - Actual payload data | ||||
|     // | ||||
|     // The message starts at offset + frame_header_padding_ | ||||
|     // So we write the header starting at offset + frame_header_padding_ - total_header_len | ||||
|     uint8_t *buf_start = buffer_data + packet.offset; | ||||
|     uint32_t header_offset = frame_header_padding_ - total_header_len; | ||||
|  | ||||
|     // Write the plaintext header | ||||
|     buf_start[header_offset] = 0x00;  // indicator | ||||
|  | ||||
|     // Encode varints directly into buffer | ||||
|     ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); | ||||
|     ProtoVarInt(packet.message_type) | ||||
|         .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); | ||||
|  | ||||
|     // Add iovec for this packet (header + payload) | ||||
|     size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size); | ||||
|     this->reusable_iovs_.push_back({buf_start + header_offset, packet_len}); | ||||
|     total_write_len += packet_len; | ||||
|   } | ||||
|  | ||||
|   // Send all packets in one writev call | ||||
|   return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); | ||||
| } | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API_PLAINTEXT | ||||
| #endif  // USE_API | ||||
							
								
								
									
										55
									
								
								esphome/components/api/api_frame_helper_plaintext.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								esphome/components/api/api_frame_helper_plaintext.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| #pragma once | ||||
| #include "api_frame_helper.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_PLAINTEXT | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| class APIPlaintextFrameHelper : public APIFrameHelper { | ||||
|  public: | ||||
|   APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) | ||||
|       : APIFrameHelper(std::move(socket), client_info) { | ||||
|     // Plaintext header structure (worst case): | ||||
|     // Pos 0: indicator (0x00) | ||||
|     // Pos 1-3: payload size varint (up to 3 bytes) | ||||
|     // Pos 4-5: message type varint (up to 2 bytes) | ||||
|     // Pos 6+: actual payload data | ||||
|     frame_header_padding_ = 6; | ||||
|   } | ||||
|   ~APIPlaintextFrameHelper() override = default; | ||||
|   APIError init() override; | ||||
|   APIError loop() override; | ||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||
|   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; | ||||
|   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; | ||||
|   uint8_t frame_header_padding() override { return frame_header_padding_; } | ||||
|   // Get the frame footer size required by this protocol | ||||
|   uint8_t frame_footer_size() override { return frame_footer_size_; } | ||||
|  | ||||
|  protected: | ||||
|   APIError try_read_frame_(std::vector<uint8_t> *frame); | ||||
|  | ||||
|   // Group 2-byte aligned types | ||||
|   uint16_t rx_header_parsed_type_ = 0; | ||||
|   uint16_t rx_header_parsed_len_ = 0; | ||||
|  | ||||
|   // Group 1-byte types together | ||||
|   // Fixed-size header buffer for plaintext protocol: | ||||
|   // We now store the indicator byte + the two varints. | ||||
|   // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: | ||||
|   // 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint | ||||
|   // | ||||
|   // While varints could theoretically be up to 10 bytes each for 64-bit values, | ||||
|   // attempting to process messages with headers that large would likely crash the | ||||
|   // ESP32 due to memory constraints. | ||||
|   uint8_t rx_header_buf_[6];  // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) | ||||
|   uint8_t rx_header_buf_pos_ = 0; | ||||
|   bool rx_header_parsed_ = false; | ||||
|   // 8 bytes total, no padding needed | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API_PLAINTEXT | ||||
| #endif  // USE_API | ||||
| @@ -23,3 +23,8 @@ extend google.protobuf.MessageOptions { | ||||
|     optional bool no_delay = 1040 [default=false]; | ||||
|     optional string base_class = 1041; | ||||
| } | ||||
|  | ||||
| extend google.protobuf.FieldOptions { | ||||
|     optional string field_ifdef = 1042; | ||||
|     optional uint32 fixed_array_size = 50007; | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -195,6 +195,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_home_assistant_state_response(msg); | ||||
|       break; | ||||
|     } | ||||
| #ifdef USE_API_SERVICES | ||||
|     case 42: { | ||||
|       ExecuteServiceRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| @@ -204,6 +205,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_execute_service_request(msg); | ||||
|       break; | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
|     case 45: { | ||||
|       CameraImageRequest msg; | ||||
| @@ -596,32 +598,32 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|  | ||||
| void APIServerConnection::on_hello_request(const HelloRequest &msg) { | ||||
|   HelloResponse ret = this->hello(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|   if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_connect_request(const ConnectRequest &msg) { | ||||
|   ConnectResponse ret = this->connect(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|   if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { | ||||
|   DisconnectResponse ret = this->disconnect(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|   if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_ping_request(const PingRequest &msg) { | ||||
|   PingResponse ret = this->ping(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|   if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { | ||||
|   if (this->check_connection_setup_()) { | ||||
|     DeviceInfoResponse ret = this->device_info(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|     if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| @@ -655,21 +657,23 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc | ||||
| void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { | ||||
|   if (this->check_connection_setup_()) { | ||||
|     GetTimeResponse ret = this->get_time(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|     if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #ifdef USE_API_SERVICES | ||||
| void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->execute_service(msg); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
| void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|     if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| @@ -863,7 +867,7 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request( | ||||
|     const SubscribeBluetoothConnectionsFreeRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|     if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| @@ -895,7 +899,7 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo | ||||
| void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|     if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -18,11 +18,11 @@ class APIServerConnectionBase : public ProtoService { | ||||
|  public: | ||||
| #endif | ||||
|  | ||||
|   template<typename T> bool send_message(const T &msg) { | ||||
|   bool send_message(const ProtoMessage &msg, uint8_t message_type) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|     this->log_send_message_(msg.message_name(), msg.dump()); | ||||
| #endif | ||||
|     return this->send_message_(msg, T::MESSAGE_TYPE); | ||||
|     return this->send_message_(msg, message_type); | ||||
|   } | ||||
|  | ||||
|   virtual void on_hello_request(const HelloRequest &value){}; | ||||
| @@ -69,7 +69,9 @@ class APIServerConnectionBase : public ProtoService { | ||||
|   virtual void on_get_time_request(const GetTimeRequest &value){}; | ||||
|   virtual void on_get_time_response(const GetTimeResponse &value){}; | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
|   virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CAMERA | ||||
|   virtual void on_camera_image_request(const CameraImageRequest &value){}; | ||||
| @@ -216,7 +218,9 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; | ||||
|   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; | ||||
|   virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; | ||||
| #ifdef USE_API_SERVICES | ||||
|   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|   virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; | ||||
| #endif | ||||
| @@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; | ||||
|   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||
|   void on_get_time_request(const GetTimeRequest &msg) override; | ||||
| #ifdef USE_API_SERVICES | ||||
|   void on_execute_service_request(const ExecuteServiceRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|   void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; | ||||
| #endif | ||||
|   | ||||
| @@ -1,359 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "proto.h" | ||||
| #include <cstdint> | ||||
| #include <string> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| class ProtoSize { | ||||
|  public: | ||||
|   /** | ||||
|    * @brief ProtoSize class for Protocol Buffer serialization size calculation | ||||
|    * | ||||
|    * This class provides static methods to calculate the exact byte counts needed | ||||
|    * for encoding various Protocol Buffer field types. All methods are designed to be | ||||
|    * efficient for the common case where many fields have default values. | ||||
|    * | ||||
|    * Implements Protocol Buffer encoding size calculation according to: | ||||
|    * https://protobuf.dev/programming-guides/encoding/ | ||||
|    * | ||||
|    * Key features: | ||||
|    * - Early-return optimization for zero/default values | ||||
|    * - Direct total_size updates to avoid unnecessary additions | ||||
|    * - Specialized handling for different field types according to protobuf spec | ||||
|    * - Templated helpers for repeated fields and messages | ||||
|    */ | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint | ||||
|    * | ||||
|    * @param value The uint32_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(uint32_t value) { | ||||
|     // Optimized varint size calculation using leading zeros | ||||
|     // Each 7 bits requires one byte in the varint encoding | ||||
|     if (value < 128) | ||||
|       return 1;  // 7 bits, common case for small values | ||||
|  | ||||
|     // For larger values, count bytes needed based on the position of the highest bit set | ||||
|     if (value < 16384) { | ||||
|       return 2;  // 14 bits | ||||
|     } else if (value < 2097152) { | ||||
|       return 3;  // 21 bits | ||||
|     } else if (value < 268435456) { | ||||
|       return 4;  // 28 bits | ||||
|     } else { | ||||
|       return 5;  // 32 bits (maximum for uint32_t) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint | ||||
|    * | ||||
|    * @param value The uint64_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(uint64_t value) { | ||||
|     // Handle common case of values fitting in uint32_t (vast majority of use cases) | ||||
|     if (value <= UINT32_MAX) { | ||||
|       return varint(static_cast<uint32_t>(value)); | ||||
|     } | ||||
|  | ||||
|     // For larger values, determine size based on highest bit position | ||||
|     if (value < (1ULL << 35)) { | ||||
|       return 5;  // 35 bits | ||||
|     } else if (value < (1ULL << 42)) { | ||||
|       return 6;  // 42 bits | ||||
|     } else if (value < (1ULL << 49)) { | ||||
|       return 7;  // 49 bits | ||||
|     } else if (value < (1ULL << 56)) { | ||||
|       return 8;  // 56 bits | ||||
|     } else if (value < (1ULL << 63)) { | ||||
|       return 9;  // 63 bits | ||||
|     } else { | ||||
|       return 10;  // 64 bits (maximum for uint64_t) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode an int32_t value as a varint | ||||
|    * | ||||
|    * Special handling is needed for negative values, which are sign-extended to 64 bits | ||||
|    * in Protocol Buffers, resulting in a 10-byte varint. | ||||
|    * | ||||
|    * @param value The int32_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(int32_t value) { | ||||
|     // Negative values are sign-extended to 64 bits in protocol buffers, | ||||
|     // which always results in a 10-byte varint for negative int32 | ||||
|     if (value < 0) { | ||||
|       return 10;  // Negative int32 is always 10 bytes long | ||||
|     } | ||||
|     // For non-negative values, use the uint32_t implementation | ||||
|     return varint(static_cast<uint32_t>(value)); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode an int64_t value as a varint | ||||
|    * | ||||
|    * @param value The int64_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(int64_t value) { | ||||
|     // For int64_t, we convert to uint64_t and calculate the size | ||||
|     // This works because the bit pattern determines the encoding size, | ||||
|     // and we've handled negative int32 values as a special case above | ||||
|     return varint(static_cast<uint64_t>(value)); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode a field ID and wire type | ||||
|    * | ||||
|    * @param field_id The field identifier | ||||
|    * @param type The wire type value (from the WireType enum in the protobuf spec) | ||||
|    * @return The number of bytes needed to encode the field ID and wire type | ||||
|    */ | ||||
|   static inline uint32_t field(uint32_t field_id, uint32_t type) { | ||||
|     uint32_t tag = (field_id << 3) | (type & 0b111); | ||||
|     return varint(tag); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Common parameters for all add_*_field methods | ||||
|    * | ||||
|    * All add_*_field methods follow these common patterns: | ||||
|    * | ||||
|    * @param total_size Reference to the total message size to update | ||||
|    * @param field_id_size Pre-calculated size of the field ID in bytes | ||||
|    * @param value The value to calculate size for (type varies) | ||||
|    * @param force Whether to calculate size even if the value is default/zero/empty | ||||
|    * | ||||
|    * Each method follows this implementation pattern: | ||||
|    * 1. Skip calculation if value is default (0, false, empty) and not forced | ||||
|    * 2. Calculate the size based on the field's encoding rules | ||||
|    * 3. Add the field_id_size + calculated value size to total_size | ||||
|    */ | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an int32 field to the total message size | ||||
|    */ | ||||
|   static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (value == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     if (value < 0) { | ||||
|       // Negative values are encoded as 10-byte varints in protobuf | ||||
|       total_size += field_id_size + 10; | ||||
|     } else { | ||||
|       // For non-negative values, use the standard varint size | ||||
|       total_size += field_id_size + varint(static_cast<uint32_t>(value)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a uint32 field to the total message size | ||||
|    */ | ||||
|   static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, | ||||
|                                       bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (value == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a boolean field to the total message size | ||||
|    */ | ||||
|   static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) { | ||||
|     // Skip calculation if value is false and not forced | ||||
|     if (!value && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Boolean fields always use 1 byte when true | ||||
|     total_size += field_id_size + 1; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a fixed field to the total message size | ||||
|    * | ||||
|    * Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double). | ||||
|    * | ||||
|    * @tparam NumBytes The number of bytes for this fixed field (4 or 8) | ||||
|    * @param is_nonzero Whether the value is non-zero | ||||
|    */ | ||||
|   template<uint32_t NumBytes> | ||||
|   static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero, | ||||
|                                      bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (!is_nonzero && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Fixed fields always take exactly NumBytes | ||||
|     total_size += field_id_size + NumBytes; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an enum field to the total message size | ||||
|    * | ||||
|    * Enum fields are encoded as uint32 varints. | ||||
|    */ | ||||
|   static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (value == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Enums are encoded as uint32 | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a sint32 field to the total message size | ||||
|    * | ||||
|    * Sint32 fields use ZigZag encoding, which is more efficient for negative values. | ||||
|    */ | ||||
|   static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (value == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) | ||||
|     uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31)); | ||||
|     total_size += field_id_size + varint(zigzag); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an int64 field to the total message size | ||||
|    */ | ||||
|   static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (value == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a uint64 field to the total message size | ||||
|    */ | ||||
|   static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value, | ||||
|                                       bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (value == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a sint64 field to the total message size | ||||
|    * | ||||
|    * Sint64 fields use ZigZag encoding, which is more efficient for negative values. | ||||
|    */ | ||||
|   static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) { | ||||
|     // Skip calculation if value is zero and not forced | ||||
|     if (value == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) | ||||
|     uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63)); | ||||
|     total_size += field_id_size + varint(zigzag); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a string/bytes field to the total message size | ||||
|    */ | ||||
|   static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str, | ||||
|                                       bool force = false) { | ||||
|     // Skip calculation if string is empty and not forced | ||||
|     if (str.empty() && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     const uint32_t str_size = static_cast<uint32_t>(str.size()); | ||||
|     total_size += field_id_size + varint(str_size) + str_size; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a nested message field to the total message size | ||||
|    * | ||||
|    * This helper function directly updates the total_size reference if the nested size | ||||
|    * is greater than zero or force is true. | ||||
|    * | ||||
|    * @param nested_size The pre-calculated size of the nested message | ||||
|    */ | ||||
|   static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size, | ||||
|                                        bool force = false) { | ||||
|     // Skip calculation if nested message is empty and not forced | ||||
|     if (nested_size == 0 && !force) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     // Field ID + length varint + nested message content | ||||
|     total_size += field_id_size + varint(nested_size) + nested_size; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a nested message field to the total message size | ||||
|    * | ||||
|    * This version takes a ProtoMessage object, calculates its size internally, | ||||
|    * and updates the total_size reference. This eliminates the need for a temporary variable | ||||
|    * at the call site. | ||||
|    * | ||||
|    * @param message The nested message object | ||||
|    */ | ||||
|   static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message, | ||||
|                                         bool force = false) { | ||||
|     uint32_t nested_size = 0; | ||||
|     message.calculate_size(nested_size); | ||||
|  | ||||
|     // Use the base implementation with the calculated nested_size | ||||
|     add_message_field(total_size, field_id_size, nested_size, force); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size | ||||
|    * | ||||
|    * This helper processes a vector of message objects, calculating the size for each message | ||||
|    * and adding it to the total size. | ||||
|    * | ||||
|    * @tparam MessageType The type of the nested messages in the vector | ||||
|    * @param messages Vector of message objects | ||||
|    */ | ||||
|   template<typename MessageType> | ||||
|   static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size, | ||||
|                                           const std::vector<MessageType> &messages) { | ||||
|     // Skip if the vector is empty | ||||
|     if (messages.empty()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // For repeated fields, always use force=true | ||||
|     for (const auto &message : messages) { | ||||
|       add_message_object(total_size, field_id_size, message, true); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| @@ -24,14 +24,6 @@ static const char *const TAG = "api"; | ||||
| // APIServer | ||||
| APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| #ifndef USE_API_YAML_SERVICES | ||||
| // Global empty vector to avoid guard variables (saves 8 bytes) | ||||
| // This is initialized at program startup before any threads | ||||
| static const std::vector<UserServiceDescriptor *> empty_user_services{}; | ||||
|  | ||||
| const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance() { return empty_user_services; } | ||||
| #endif | ||||
|  | ||||
| APIServer::APIServer() { | ||||
|   global_api_server = this; | ||||
|   // Pre-allocate shared write buffer | ||||
| @@ -39,7 +31,6 @@ APIServer::APIServer() { | ||||
| } | ||||
|  | ||||
| void APIServer::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->setup_controller(); | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| @@ -113,7 +104,7 @@ void APIServer::setup() { | ||||
|             return; | ||||
|           } | ||||
|           for (auto &c : this->clients_) { | ||||
|             if (!c->flags_.remove) | ||||
|             if (!c->flags_.remove && c->get_log_subscription_level() >= level) | ||||
|               c->try_send_log_message(level, tag, message, message_len); | ||||
|           } | ||||
|         }); | ||||
| @@ -193,9 +184,9 @@ void APIServer::loop() { | ||||
|  | ||||
|     // Rare case: handle disconnection | ||||
| #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER | ||||
|     this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); | ||||
|     this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername); | ||||
| #endif | ||||
|     ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); | ||||
|     ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str()); | ||||
|  | ||||
|     // Swap with the last element and pop (avoids expensive vector shifts) | ||||
|     if (client_index < this->clients_.size() - 1) { | ||||
| @@ -213,22 +204,20 @@ void APIServer::loop() { | ||||
|  | ||||
| void APIServer::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "API Server:\n" | ||||
|                 "Server:\n" | ||||
|                 "  Address: %s:%u", | ||||
|                 network::get_use_address().c_str(), this->port_); | ||||
| #ifdef USE_API_NOISE | ||||
|   ESP_LOGCONFIG(TAG, "  Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); | ||||
|   ESP_LOGCONFIG(TAG, "  Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); | ||||
|   if (!this->noise_ctx_->has_psk()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Supports noise encryption: YES"); | ||||
|     ESP_LOGCONFIG(TAG, "  Supports encryption: YES"); | ||||
|   } | ||||
| #else | ||||
|   ESP_LOGCONFIG(TAG, "  Using noise encryption: NO"); | ||||
|   ESP_LOGCONFIG(TAG, "  Noise encryption: NO"); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| #ifdef USE_API_PASSWORD | ||||
| bool APIServer::uses_password() const { return !this->password_.empty(); } | ||||
|  | ||||
| bool APIServer::check_password(const std::string &password) const { | ||||
|   // depend only on input password length | ||||
|   const char *a = this->password_.c_str(); | ||||
| @@ -436,10 +425,11 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
|   ESP_LOGD(TAG, "Noise PSK saved"); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         c->send_message(DisconnectRequest()); | ||||
|         DisconnectRequest req; | ||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| @@ -472,10 +462,12 @@ void APIServer::on_shutdown() { | ||||
|  | ||||
|   // Send disconnect requests to all connected clients | ||||
|   for (auto &c : this->clients_) { | ||||
|     if (!c->send_message(DisconnectRequest())) { | ||||
|     DisconnectRequest req; | ||||
|     if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { | ||||
|       // If we can't send the disconnect request directly (tx_buffer full), | ||||
|       // schedule it at the front of the batch so it will be sent with priority | ||||
|       c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); | ||||
|       c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE, | ||||
|                                  DisconnectRequest::ESTIMATED_SIZE); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,9 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "list_entities.h" | ||||
| #include "subscribe_state.h" | ||||
| #ifdef USE_API_SERVICES | ||||
| #include "user_services.h" | ||||
| #endif | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| @@ -25,11 +27,6 @@ struct SavedNoisePsk { | ||||
| } PACKED;  // NOLINT | ||||
| #endif | ||||
|  | ||||
| #ifndef USE_API_YAML_SERVICES | ||||
| // Forward declaration of helper function | ||||
| const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance(); | ||||
| #endif | ||||
|  | ||||
| class APIServer : public Component, public Controller { | ||||
|  public: | ||||
|   APIServer(); | ||||
| @@ -42,7 +39,6 @@ class APIServer : public Component, public Controller { | ||||
|   bool teardown() override; | ||||
| #ifdef USE_API_PASSWORD | ||||
|   bool check_password(const std::string &password) const; | ||||
|   bool uses_password() const; | ||||
|   void set_password(const std::string &password); | ||||
| #endif | ||||
|   void set_port(uint16_t port); | ||||
| @@ -112,18 +108,9 @@ class APIServer : public Component, public Controller { | ||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||
| #endif | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { | ||||
| #ifdef USE_API_YAML_SERVICES | ||||
|     // Vector is pre-allocated when services are defined in YAML | ||||
|     this->user_services_.push_back(descriptor); | ||||
| #else | ||||
|     // Lazy allocate vector on first use for CustomAPIDevice | ||||
|     if (!this->user_services_) { | ||||
|       this->user_services_ = std::make_unique<std::vector<UserServiceDescriptor *>>(); | ||||
|     } | ||||
|     this->user_services_->push_back(descriptor); | ||||
| #ifdef USE_API_SERVICES | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #endif | ||||
|   } | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void request_time(); | ||||
| #endif | ||||
| @@ -152,17 +139,9 @@ class APIServer : public Component, public Controller { | ||||
|   void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                 std::function<void(std::string)> f); | ||||
|   const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; | ||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { | ||||
| #ifdef USE_API_YAML_SERVICES | ||||
|     return this->user_services_; | ||||
| #else | ||||
|     if (this->user_services_) { | ||||
|       return *this->user_services_; | ||||
|     } | ||||
|     // Return reference to global empty instance (no guard needed) | ||||
|     return get_empty_user_services_instance(); | ||||
| #ifdef USE_API_SERVICES | ||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } | ||||
| #endif | ||||
|   } | ||||
|  | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|   Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; } | ||||
| @@ -194,14 +173,8 @@ class APIServer : public Component, public Controller { | ||||
| #endif | ||||
|   std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections | ||||
|   std::vector<HomeAssistantStateSubscription> state_subs_; | ||||
| #ifdef USE_API_YAML_SERVICES | ||||
|   // When services are defined in YAML, we know at compile time that services will be registered | ||||
| #ifdef USE_API_SERVICES | ||||
|   std::vector<UserServiceDescriptor *> user_services_; | ||||
| #else | ||||
|   // Services can still be registered at runtime by CustomAPIDevice components even when not | ||||
|   // defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common | ||||
|   // case where no services (YAML or custom) are used. | ||||
|   std::unique_ptr<std::vector<UserServiceDescriptor *>> user_services_; | ||||
| #endif | ||||
|  | ||||
|   // Group smaller types together | ||||
|   | ||||
| @@ -3,10 +3,13 @@ | ||||
| #include <map> | ||||
| #include "api_server.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_SERVICES | ||||
| #include "user_services.h" | ||||
| #endif | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { | ||||
|  public: | ||||
|   CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, | ||||
| @@ -19,6 +22,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS | ||||
|   T *obj_; | ||||
|   void (T::*callback_)(Ts...); | ||||
| }; | ||||
| #endif  // USE_API_SERVICES | ||||
|  | ||||
| class CustomAPIDevice { | ||||
|  public: | ||||
| @@ -46,12 +50,14 @@ class CustomAPIDevice { | ||||
|    * @param name The name of the service to register. | ||||
|    * @param arg_names The name of the arguments for the service, must match the arguments of the function. | ||||
|    */ | ||||
| #ifdef USE_API_SERVICES | ||||
|   template<typename T, typename... Ts> | ||||
|   void register_service(void (T::*callback)(Ts...), const std::string &name, | ||||
|                         const std::array<std::string, sizeof...(Ts)> &arg_names) { | ||||
|     auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   /** Register a custom native API service that will show up in Home Assistant. | ||||
|    * | ||||
| @@ -71,10 +77,12 @@ class CustomAPIDevice { | ||||
|    * @param callback The member function to call when the service is triggered. | ||||
|    * @param name The name of the arguments for the service, must match the arguments of the function. | ||||
|    */ | ||||
| #ifdef USE_API_SERVICES | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
|     auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   /** Subscribe to the state (or attribute state) of an entity from Home Assistant. | ||||
|    * | ||||
|   | ||||
| @@ -11,6 +11,18 @@ namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { | ||||
|  private: | ||||
|   // Helper to convert value to string - handles the case where value is already a string | ||||
|   template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); } | ||||
|  | ||||
|   // Overloads for string types - needed because std::to_string doesn't support them | ||||
|   static std::string value_to_string(char *val) { | ||||
|     return val ? std::string(val) : std::string(); | ||||
|   }  // For lambdas returning char* (e.g., itoa) | ||||
|   static std::string value_to_string(const char *val) { return std::string(val); }  // For lambdas returning .c_str() | ||||
|   static std::string value_to_string(const std::string &val) { return val; } | ||||
|   static std::string value_to_string(std::string &&val) { return std::move(val); } | ||||
|  | ||||
|  public: | ||||
|   TemplatableStringValue() : TemplatableValue<std::string, X...>() {} | ||||
|  | ||||
| @@ -19,7 +31,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s | ||||
|  | ||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> | ||||
|   TemplatableStringValue(F f) | ||||
|       : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {} | ||||
|       : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {} | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class TemplatableKeyValuePair { | ||||
|   | ||||
| @@ -83,10 +83,12 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done( | ||||
|  | ||||
| ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||
|   auto resp = service->encode_list_service_response(); | ||||
|   return this->client_->send_message(resp); | ||||
|   return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class APIConnection; | ||||
| #define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \ | ||||
|   bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ | ||||
|     return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \ | ||||
|                                             ResponseType::MESSAGE_TYPE); \ | ||||
|                                             ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \ | ||||
|   } | ||||
|  | ||||
| class ListEntitiesIterator : public ComponentIterator { | ||||
| @@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   bool on_text_sensor(text_sensor::TextSensor *entity) override; | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
|   bool on_service(UserServiceDescriptor *service) override; | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
|   bool on_camera(camera::Camera *entity) override; | ||||
| #endif | ||||
|   | ||||
| @@ -8,7 +8,7 @@ namespace api { | ||||
|  | ||||
| static const char *const TAG = "api.proto"; | ||||
|  | ||||
| void ProtoMessage::decode(const uint8_t *buffer, size_t length) { | ||||
| void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | ||||
|   uint32_t i = 0; | ||||
|   bool error = false; | ||||
|   while (i < length) { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <cassert> | ||||
| #include <vector> | ||||
|  | ||||
| #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE | ||||
| @@ -59,7 +60,6 @@ class ProtoVarInt { | ||||
|   uint32_t as_uint32() const { return this->value_; } | ||||
|   uint64_t as_uint64() const { return this->value_; } | ||||
|   bool as_bool() const { return this->value_; } | ||||
|   template<typename T> T as_enum() const { return static_cast<T>(this->as_uint32()); } | ||||
|   int32_t as_int32() const { | ||||
|     // Not ZigZag encoded | ||||
|     return static_cast<int32_t>(this->as_int64()); | ||||
| @@ -133,15 +133,25 @@ class ProtoVarInt { | ||||
|   uint64_t value_; | ||||
| }; | ||||
|  | ||||
| // Forward declaration for decode_to_message and encode_to_writer | ||||
| class ProtoMessage; | ||||
| class ProtoDecodableMessage; | ||||
|  | ||||
| class ProtoLengthDelimited { | ||||
|  public: | ||||
|   explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} | ||||
|   std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); } | ||||
|   template<class C> C as_message() const { | ||||
|     auto msg = C(); | ||||
|     msg.decode(this->value_, this->length_); | ||||
|     return msg; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Decode the length-delimited data into an existing ProtoDecodableMessage instance. | ||||
|    * | ||||
|    * This method allows decoding without templates, enabling use in contexts | ||||
|    * where the message type is not known at compile time. The ProtoDecodableMessage's | ||||
|    * decode() method will be called with the raw data and length. | ||||
|    * | ||||
|    * @param msg The ProtoDecodableMessage instance to decode into | ||||
|    */ | ||||
|   void decode_to_message(ProtoDecodableMessage &msg) const; | ||||
|  | ||||
|  protected: | ||||
|   const uint8_t *const value_; | ||||
| @@ -166,23 +176,7 @@ class Proto32Bit { | ||||
|   const uint32_t value_; | ||||
| }; | ||||
|  | ||||
| class Proto64Bit { | ||||
|  public: | ||||
|   explicit Proto64Bit(uint64_t value) : value_(value) {} | ||||
|   uint64_t as_fixed64() const { return this->value_; } | ||||
|   int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); } | ||||
|   double as_double() const { | ||||
|     union { | ||||
|       uint64_t raw; | ||||
|       double value; | ||||
|     } s{}; | ||||
|     s.raw = this->value_; | ||||
|     return s.value; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   const uint64_t value_; | ||||
| }; | ||||
| // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported | ||||
|  | ||||
| class ProtoWriteBuffer { | ||||
|  public: | ||||
| @@ -196,9 +190,9 @@ class ProtoWriteBuffer { | ||||
|    * @param field_id Field number (tag) in the protobuf message | ||||
|    * @param type Wire type value: | ||||
|    *   - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) | ||||
|    *   - 1: 64-bit (fixed64, sfixed64, double) | ||||
|    *   - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields) | ||||
|    *   - 5: 32-bit (fixed32, sfixed32, float) | ||||
|    *   - Note: Wire type 1 (64-bit fixed) is not supported | ||||
|    * | ||||
|    * Following https://protobuf.dev/programming-guides/encoding/#structure | ||||
|    */ | ||||
| @@ -249,23 +243,10 @@ class ProtoWriteBuffer { | ||||
|     this->write((value >> 16) & 0xFF); | ||||
|     this->write((value >> 24) & 0xFF); | ||||
|   } | ||||
|   void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { | ||||
|     if (value == 0 && !force) | ||||
|       return; | ||||
|  | ||||
|     this->encode_field_raw(field_id, 1);  // type 1: 64-bit fixed64 | ||||
|     this->write((value >> 0) & 0xFF); | ||||
|     this->write((value >> 8) & 0xFF); | ||||
|     this->write((value >> 16) & 0xFF); | ||||
|     this->write((value >> 24) & 0xFF); | ||||
|     this->write((value >> 32) & 0xFF); | ||||
|     this->write((value >> 40) & 0xFF); | ||||
|     this->write((value >> 48) & 0xFF); | ||||
|     this->write((value >> 56) & 0xFF); | ||||
|   } | ||||
|   template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) { | ||||
|     this->encode_uint32(field_id, static_cast<uint32_t>(value), force); | ||||
|   } | ||||
|   // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally | ||||
|   // not supported to reduce overhead on embedded systems. All ESPHome devices are | ||||
|   // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support | ||||
|   // is needed in the future, the necessary encoding/decoding functions must be added. | ||||
|   void encode_float(uint32_t field_id, float value, bool force = false) { | ||||
|     if (value == 0.0f && !force) | ||||
|       return; | ||||
| @@ -306,18 +287,7 @@ class ProtoWriteBuffer { | ||||
|     } | ||||
|     this->encode_uint64(field_id, uvalue, force); | ||||
|   } | ||||
|   template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { | ||||
|     this->encode_field_raw(field_id, 2);  // type 2: Length-delimited message | ||||
|     size_t begin = this->buffer_->size(); | ||||
|  | ||||
|     value.encode(*this); | ||||
|  | ||||
|     const uint32_t nested_length = this->buffer_->size() - begin; | ||||
|     // add size varint | ||||
|     std::vector<uint8_t> var; | ||||
|     ProtoVarInt(nested_length).encode(var); | ||||
|     this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end()); | ||||
|   } | ||||
|   void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); | ||||
|   std::vector<uint8_t> *get_buffer() const { return buffer_; } | ||||
|  | ||||
|  protected: | ||||
| @@ -329,7 +299,6 @@ class ProtoMessage { | ||||
|   virtual ~ProtoMessage() = default; | ||||
|   // Default implementation for messages with no fields | ||||
|   virtual void encode(ProtoWriteBuffer buffer) const {} | ||||
|   void decode(const uint8_t *buffer, size_t length); | ||||
|   // Default implementation for messages with no fields | ||||
|   virtual void calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -337,14 +306,519 @@ class ProtoMessage { | ||||
|   virtual void dump_to(std::string &out) const = 0; | ||||
|   virtual const char *message_name() const { return "unknown"; } | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| // Base class for messages that support decoding | ||||
| class ProtoDecodableMessage : public ProtoMessage { | ||||
|  public: | ||||
|   void decode(const uint8_t *buffer, size_t length); | ||||
|  | ||||
|  protected: | ||||
|   virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } | ||||
|   virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } | ||||
|   virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } | ||||
|   virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } | ||||
|   // NOTE: decode_64bit removed - wire type 1 not supported | ||||
| }; | ||||
|  | ||||
| class ProtoSize { | ||||
|  public: | ||||
|   /** | ||||
|    * @brief ProtoSize class for Protocol Buffer serialization size calculation | ||||
|    * | ||||
|    * This class provides static methods to calculate the exact byte counts needed | ||||
|    * for encoding various Protocol Buffer field types. All methods are designed to be | ||||
|    * efficient for the common case where many fields have default values. | ||||
|    * | ||||
|    * Implements Protocol Buffer encoding size calculation according to: | ||||
|    * https://protobuf.dev/programming-guides/encoding/ | ||||
|    * | ||||
|    * Key features: | ||||
|    * - Early-return optimization for zero/default values | ||||
|    * - Direct total_size updates to avoid unnecessary additions | ||||
|    * - Specialized handling for different field types according to protobuf spec | ||||
|    * - Templated helpers for repeated fields and messages | ||||
|    */ | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint | ||||
|    * | ||||
|    * @param value The uint32_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(uint32_t value) { | ||||
|     // Optimized varint size calculation using leading zeros | ||||
|     // Each 7 bits requires one byte in the varint encoding | ||||
|     if (value < 128) | ||||
|       return 1;  // 7 bits, common case for small values | ||||
|  | ||||
|     // For larger values, count bytes needed based on the position of the highest bit set | ||||
|     if (value < 16384) { | ||||
|       return 2;  // 14 bits | ||||
|     } else if (value < 2097152) { | ||||
|       return 3;  // 21 bits | ||||
|     } else if (value < 268435456) { | ||||
|       return 4;  // 28 bits | ||||
|     } else { | ||||
|       return 5;  // 32 bits (maximum for uint32_t) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint | ||||
|    * | ||||
|    * @param value The uint64_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(uint64_t value) { | ||||
|     // Handle common case of values fitting in uint32_t (vast majority of use cases) | ||||
|     if (value <= UINT32_MAX) { | ||||
|       return varint(static_cast<uint32_t>(value)); | ||||
|     } | ||||
|  | ||||
|     // For larger values, determine size based on highest bit position | ||||
|     if (value < (1ULL << 35)) { | ||||
|       return 5;  // 35 bits | ||||
|     } else if (value < (1ULL << 42)) { | ||||
|       return 6;  // 42 bits | ||||
|     } else if (value < (1ULL << 49)) { | ||||
|       return 7;  // 49 bits | ||||
|     } else if (value < (1ULL << 56)) { | ||||
|       return 8;  // 56 bits | ||||
|     } else if (value < (1ULL << 63)) { | ||||
|       return 9;  // 63 bits | ||||
|     } else { | ||||
|       return 10;  // 64 bits (maximum for uint64_t) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode an int32_t value as a varint | ||||
|    * | ||||
|    * Special handling is needed for negative values, which are sign-extended to 64 bits | ||||
|    * in Protocol Buffers, resulting in a 10-byte varint. | ||||
|    * | ||||
|    * @param value The int32_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(int32_t value) { | ||||
|     // Negative values are sign-extended to 64 bits in protocol buffers, | ||||
|     // which always results in a 10-byte varint for negative int32 | ||||
|     if (value < 0) { | ||||
|       return 10;  // Negative int32 is always 10 bytes long | ||||
|     } | ||||
|     // For non-negative values, use the uint32_t implementation | ||||
|     return varint(static_cast<uint32_t>(value)); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode an int64_t value as a varint | ||||
|    * | ||||
|    * @param value The int64_t value to calculate size for | ||||
|    * @return The number of bytes needed to encode the value | ||||
|    */ | ||||
|   static inline uint32_t varint(int64_t value) { | ||||
|     // For int64_t, we convert to uint64_t and calculate the size | ||||
|     // This works because the bit pattern determines the encoding size, | ||||
|     // and we've handled negative int32 values as a special case above | ||||
|     return varint(static_cast<uint64_t>(value)); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates the size in bytes needed to encode a field ID and wire type | ||||
|    * | ||||
|    * @param field_id The field identifier | ||||
|    * @param type The wire type value (from the WireType enum in the protobuf spec) | ||||
|    * @return The number of bytes needed to encode the field ID and wire type | ||||
|    */ | ||||
|   static inline uint32_t field(uint32_t field_id, uint32_t type) { | ||||
|     uint32_t tag = (field_id << 3) | (type & 0b111); | ||||
|     return varint(tag); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Common parameters for all add_*_field methods | ||||
|    * | ||||
|    * All add_*_field methods follow these common patterns: | ||||
|    * | ||||
|    * @param total_size Reference to the total message size to update | ||||
|    * @param field_id_size Pre-calculated size of the field ID in bytes | ||||
|    * @param value The value to calculate size for (type varies) | ||||
|    * @param force Whether to calculate size even if the value is default/zero/empty | ||||
|    * | ||||
|    * Each method follows this implementation pattern: | ||||
|    * 1. Skip calculation if value is default (0, false, empty) and not forced | ||||
|    * 2. Calculate the size based on the field's encoding rules | ||||
|    * 3. Add the field_id_size + calculated value size to total_size | ||||
|    */ | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an int32 field to the total message size | ||||
|    */ | ||||
|   static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (value == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     if (value < 0) { | ||||
|       // Negative values are encoded as 10-byte varints in protobuf | ||||
|       total_size += field_id_size + 10; | ||||
|     } else { | ||||
|       // For non-negative values, use the standard varint size | ||||
|       total_size += field_id_size + varint(static_cast<uint32_t>(value)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an int32 field to the total message size (repeated field version) | ||||
|    */ | ||||
|   static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     if (value < 0) { | ||||
|       // Negative values are encoded as 10-byte varints in protobuf | ||||
|       total_size += field_id_size + 10; | ||||
|     } else { | ||||
|       // For non-negative values, use the standard varint size | ||||
|       total_size += field_id_size + varint(static_cast<uint32_t>(value)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a uint32 field to the total message size | ||||
|    */ | ||||
|   static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (value == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version) | ||||
|    */ | ||||
|   static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a boolean field to the total message size | ||||
|    */ | ||||
|   static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) { | ||||
|     // Skip calculation if value is false | ||||
|     if (!value) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Boolean fields always use 1 byte when true | ||||
|     total_size += field_id_size + 1; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a boolean field to the total message size (repeated field version) | ||||
|    */ | ||||
|   static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     // Boolean fields always use 1 byte | ||||
|     total_size += field_id_size + 1; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a fixed field to the total message size | ||||
|    * | ||||
|    * Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double). | ||||
|    * | ||||
|    * @tparam NumBytes The number of bytes for this fixed field (4 or 8) | ||||
|    * @param is_nonzero Whether the value is non-zero | ||||
|    */ | ||||
|   template<uint32_t NumBytes> | ||||
|   static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (!is_nonzero) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Fixed fields always take exactly NumBytes | ||||
|     total_size += field_id_size + NumBytes; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a float field to the total message size | ||||
|    */ | ||||
|   static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) { | ||||
|     if (value != 0.0f) { | ||||
|       total_size += field_id_size + 4; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported | ||||
|   // to reduce overhead on embedded systems | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a fixed32 field to the total message size | ||||
|    */ | ||||
|   static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { | ||||
|     if (value != 0) { | ||||
|       total_size += field_id_size + 4; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported | ||||
|   // to reduce overhead on embedded systems | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a sfixed32 field to the total message size | ||||
|    */ | ||||
|   static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { | ||||
|     if (value != 0) { | ||||
|       total_size += field_id_size + 4; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported | ||||
|   // to reduce overhead on embedded systems | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an enum field to the total message size | ||||
|    * | ||||
|    * Enum fields are encoded as uint32 varints. | ||||
|    */ | ||||
|   static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (value == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Enums are encoded as uint32 | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an enum field to the total message size (repeated field version) | ||||
|    * | ||||
|    * Enum fields are encoded as uint32 varints. | ||||
|    */ | ||||
|   static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     // Enums are encoded as uint32 | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a sint32 field to the total message size | ||||
|    * | ||||
|    * Sint32 fields use ZigZag encoding, which is more efficient for negative values. | ||||
|    */ | ||||
|   static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (value == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) | ||||
|     uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31)); | ||||
|     total_size += field_id_size + varint(zigzag); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version) | ||||
|    * | ||||
|    * Sint32 fields use ZigZag encoding, which is more efficient for negative values. | ||||
|    */ | ||||
|   static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) | ||||
|     uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31)); | ||||
|     total_size += field_id_size + varint(zigzag); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an int64 field to the total message size | ||||
|    */ | ||||
|   static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (value == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of an int64 field to the total message size (repeated field version) | ||||
|    */ | ||||
|   static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a uint64 field to the total message size | ||||
|    */ | ||||
|   static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (value == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version) | ||||
|    */ | ||||
|   static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed | ||||
|   // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a string/bytes field to the total message size | ||||
|    */ | ||||
|   static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) { | ||||
|     // Skip calculation if string is empty | ||||
|     if (str.empty()) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     const uint32_t str_size = static_cast<uint32_t>(str.size()); | ||||
|     total_size += field_id_size + varint(str_size) + str_size; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a string/bytes field to the total message size (repeated field version) | ||||
|    */ | ||||
|   static inline void add_string_field_repeated(uint32_t &total_size, uint32_t field_id_size, const std::string &str) { | ||||
|     // Always calculate size for repeated fields | ||||
|     const uint32_t str_size = static_cast<uint32_t>(str.size()); | ||||
|     total_size += field_id_size + varint(str_size) + str_size; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a nested message field to the total message size | ||||
|    * | ||||
|    * This helper function directly updates the total_size reference if the nested size | ||||
|    * is greater than zero. | ||||
|    * | ||||
|    * @param nested_size The pre-calculated size of the nested message | ||||
|    */ | ||||
|   static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) { | ||||
|     // Skip calculation if nested message is empty | ||||
|     if (nested_size == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     // Field ID + length varint + nested message content | ||||
|     total_size += field_id_size + varint(nested_size) + nested_size; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a nested message field to the total message size (repeated field version) | ||||
|    * | ||||
|    * @param nested_size The pre-calculated size of the nested message | ||||
|    */ | ||||
|   static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) { | ||||
|     // Always calculate size for repeated fields | ||||
|     // Field ID + length varint + nested message content | ||||
|     total_size += field_id_size + varint(nested_size) + nested_size; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a nested message field to the total message size | ||||
|    * | ||||
|    * This version takes a ProtoMessage object, calculates its size internally, | ||||
|    * and updates the total_size reference. This eliminates the need for a temporary variable | ||||
|    * at the call site. | ||||
|    * | ||||
|    * @param message The nested message object | ||||
|    */ | ||||
|   static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) { | ||||
|     uint32_t nested_size = 0; | ||||
|     message.calculate_size(nested_size); | ||||
|  | ||||
|     // Use the base implementation with the calculated nested_size | ||||
|     add_message_field(total_size, field_id_size, nested_size); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a nested message field to the total message size (repeated field version) | ||||
|    * | ||||
|    * @param message The nested message object | ||||
|    */ | ||||
|   static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size, | ||||
|                                                  const ProtoMessage &message) { | ||||
|     uint32_t nested_size = 0; | ||||
|     message.calculate_size(nested_size); | ||||
|  | ||||
|     // Use the base implementation with the calculated nested_size | ||||
|     add_message_field_repeated(total_size, field_id_size, nested_size); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size | ||||
|    * | ||||
|    * This helper processes a vector of message objects, calculating the size for each message | ||||
|    * and adding it to the total size. | ||||
|    * | ||||
|    * @tparam MessageType The type of the nested messages in the vector | ||||
|    * @param messages Vector of message objects | ||||
|    */ | ||||
|   template<typename MessageType> | ||||
|   static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size, | ||||
|                                           const std::vector<MessageType> &messages) { | ||||
|     // Skip if the vector is empty | ||||
|     if (messages.empty()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Use the repeated field version for all messages | ||||
|     for (const auto &message : messages) { | ||||
|       add_message_object_repeated(total_size, field_id_size, message); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Implementation of encode_message - must be after ProtoMessage is defined | ||||
| inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { | ||||
|   this->encode_field_raw(field_id, 2);  // type 2: Length-delimited message | ||||
|  | ||||
|   // Calculate the message size first | ||||
|   uint32_t msg_length_bytes = 0; | ||||
|   value.calculate_size(msg_length_bytes); | ||||
|  | ||||
|   // Calculate how many bytes the length varint needs | ||||
|   uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); | ||||
|  | ||||
|   // Reserve exact space for the length varint | ||||
|   size_t begin = this->buffer_->size(); | ||||
|   this->buffer_->resize(this->buffer_->size() + varint_length_bytes); | ||||
|  | ||||
|   // Write the length varint directly | ||||
|   ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes); | ||||
|  | ||||
|   // Now encode the message content - it will append to the buffer | ||||
|   value.encode(*this); | ||||
|  | ||||
|   // Verify that the encoded size matches what we calculated | ||||
|   assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); | ||||
| } | ||||
|  | ||||
| // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined | ||||
| inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const { | ||||
|   msg.decode(this->value_, this->length_); | ||||
| } | ||||
|  | ||||
| template<typename T> const char *proto_enum_to_string(T value); | ||||
|  | ||||
| class ProtoService { | ||||
| @@ -363,11 +837,11 @@ class ProtoService { | ||||
|    * @return A ProtoWriteBuffer object with the reserved size. | ||||
|    */ | ||||
|   virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; | ||||
|   virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0; | ||||
|   virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; | ||||
|   virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; | ||||
|  | ||||
|   // Optimized method that pre-allocates buffer based on message size | ||||
|   bool send_message_(const ProtoMessage &msg, uint16_t message_type) { | ||||
|   bool send_message_(const ProtoMessage &msg, uint8_t message_type) { | ||||
|     uint32_t msg_size = 0; | ||||
|     msg.calculate_size(msg_size); | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "api_pb2.h" | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| @@ -15,6 +16,8 @@ class UserServiceDescriptor { | ||||
|   virtual ListEntitiesServicesResponse encode_list_service_response() = 0; | ||||
|  | ||||
|   virtual bool execute_service(const ExecuteServiceRequest &req) = 0; | ||||
|  | ||||
|   bool is_internal() { return false; } | ||||
| }; | ||||
|  | ||||
| template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg); | ||||
| @@ -73,3 +76,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts... | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API_SERVICES | ||||
|   | ||||
| @@ -3,8 +3,6 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/as3935/as3935.h" | ||||
| #include "esphome/components/spi/spi.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace as3935_spi { | ||||
|   | ||||
| @@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     if CORE.is_esp32 or CORE.is_libretiny: | ||||
|         # https://github.com/ESP32Async/AsyncTCP | ||||
|         cg.add_library("ESP32Async/AsyncTCP", "3.4.4") | ||||
|         cg.add_library("ESP32Async/AsyncTCP", "3.4.5") | ||||
|     elif CORE.is_esp8266: | ||||
|         # https://github.com/ESP32Async/ESPAsyncTCP | ||||
|         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") | ||||
|   | ||||
| @@ -85,13 +85,13 @@ async def to_code(config): | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     cg.add(var.set_active(config[CONF_ACTIVE])) | ||||
|     await esp32_ble_tracker.register_ble_device(var, config) | ||||
|     await esp32_ble_tracker.register_raw_ble_device(var, config) | ||||
|  | ||||
|     for connection_conf in config.get(CONF_CONNECTIONS, []): | ||||
|         connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) | ||||
|         await cg.register_component(connection_var, connection_conf) | ||||
|         cg.add(var.register_connection(connection_var)) | ||||
|         await esp32_ble_tracker.register_client(connection_var, connection_conf) | ||||
|         await esp32_ble_tracker.register_raw_client(connection_var, connection_conf) | ||||
|  | ||||
|     if config.get(CONF_CACHE_SERVICES): | ||||
|         add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True) | ||||
|   | ||||
| @@ -13,11 +13,179 @@ namespace bluetooth_proxy { | ||||
|  | ||||
| static const char *const TAG = "bluetooth_proxy.connection"; | ||||
|  | ||||
| static std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { | ||||
|   esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); | ||||
|   return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), | ||||
|                                ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; | ||||
| } | ||||
|  | ||||
| void BluetoothConnection::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "BLE Connection:"); | ||||
|   BLEClientBase::dump_config(); | ||||
| } | ||||
|  | ||||
| void BluetoothConnection::loop() { | ||||
|   BLEClientBase::loop(); | ||||
|  | ||||
|   // Early return if no active connection or not in service discovery phase | ||||
|   if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Handle service discovery | ||||
|   this->send_service_for_discovery_(); | ||||
| } | ||||
|  | ||||
| void BluetoothConnection::reset_connection_(esp_err_t reason) { | ||||
|   // Send disconnection notification | ||||
|   this->proxy_->send_device_connection(this->address_, false, 0, reason); | ||||
|  | ||||
|   // Important: If we were in the middle of sending services, we do NOT send | ||||
|   // send_gatt_services_done() here. This ensures the client knows that | ||||
|   // the service discovery was interrupted and can retry. The client | ||||
|   // (aioesphomeapi) implements a 30-second timeout (DEFAULT_BLE_TIMEOUT) | ||||
|   // to detect incomplete service discovery rather than relying on us to | ||||
|   // tell them about a partial list. | ||||
|   this->set_address(0); | ||||
|   this->send_service_ = DONE_SENDING_SERVICES; | ||||
|   this->proxy_->send_connections_free(); | ||||
| } | ||||
|  | ||||
| void BluetoothConnection::send_service_for_discovery_() { | ||||
|   if (this->send_service_ == this->service_count_) { | ||||
|     this->send_service_ = DONE_SENDING_SERVICES; | ||||
|     this->proxy_->send_gatt_services_done(this->address_); | ||||
|     if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || | ||||
|         this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { | ||||
|       this->release_services(); | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Early return if no API connection | ||||
|   auto *api_conn = this->proxy_->get_api_connection(); | ||||
|   if (api_conn == nullptr) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Send next service | ||||
|   esp_gattc_service_elem_t service_result; | ||||
|   uint16_t service_count = 1; | ||||
|   esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, | ||||
|                                                                &service_result, &service_count, this->send_service_); | ||||
|   this->send_service_++; | ||||
|  | ||||
|   if (service_status != ESP_GATT_OK) { | ||||
|     ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_, | ||||
|              this->address_str().c_str(), this->send_service_ - 1, service_status); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (service_count == 0) { | ||||
|     ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_, | ||||
|              this->address_str().c_str(), service_count); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   api::BluetoothGATTGetServicesResponse resp; | ||||
|   resp.address = this->address_; | ||||
|   resp.services.emplace_back(); | ||||
|   auto &service_resp = resp.services.back(); | ||||
|   service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); | ||||
|   service_resp.handle = service_result.start_handle; | ||||
|  | ||||
|   // Get the number of characteristics directly with one call | ||||
|   uint16_t total_char_count = 0; | ||||
|   esp_gatt_status_t char_count_status = | ||||
|       esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC, | ||||
|                                    service_result.start_handle, service_result.end_handle, 0, &total_char_count); | ||||
|  | ||||
|   if (char_count_status == ESP_GATT_OK && total_char_count > 0) { | ||||
|     // Only reserve if we successfully got a count | ||||
|     service_resp.characteristics.reserve(total_char_count); | ||||
|   } else if (char_count_status != ESP_GATT_OK) { | ||||
|     ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_, | ||||
|              this->address_str().c_str(), char_count_status); | ||||
|   } | ||||
|  | ||||
|   // Now process characteristics | ||||
|   uint16_t char_offset = 0; | ||||
|   esp_gattc_char_elem_t char_result; | ||||
|   while (true) {  // characteristics | ||||
|     uint16_t char_count = 1; | ||||
|     esp_gatt_status_t char_status = | ||||
|         esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle, | ||||
|                                    service_result.end_handle, &char_result, &char_count, char_offset); | ||||
|     if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) { | ||||
|       break; | ||||
|     } | ||||
|     if (char_status != ESP_GATT_OK) { | ||||
|       ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, | ||||
|                this->address_str().c_str(), char_status); | ||||
|       break; | ||||
|     } | ||||
|     if (char_count == 0) { | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     service_resp.characteristics.emplace_back(); | ||||
|     auto &characteristic_resp = service_resp.characteristics.back(); | ||||
|     characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); | ||||
|     characteristic_resp.handle = char_result.char_handle; | ||||
|     characteristic_resp.properties = char_result.properties; | ||||
|     char_offset++; | ||||
|  | ||||
|     // Get the number of descriptors directly with one call | ||||
|     uint16_t total_desc_count = 0; | ||||
|     esp_gatt_status_t desc_count_status = | ||||
|         esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle, | ||||
|                                      service_result.end_handle, 0, &total_desc_count); | ||||
|  | ||||
|     if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) { | ||||
|       // Only reserve if we successfully got a count | ||||
|       characteristic_resp.descriptors.reserve(total_desc_count); | ||||
|     } else if (desc_count_status != ESP_GATT_OK) { | ||||
|       ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_, | ||||
|                this->address_str().c_str(), char_result.char_handle, desc_count_status); | ||||
|     } | ||||
|  | ||||
|     // Now process descriptors | ||||
|     uint16_t desc_offset = 0; | ||||
|     esp_gattc_descr_elem_t desc_result; | ||||
|     while (true) {  // descriptors | ||||
|       uint16_t desc_count = 1; | ||||
|       esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr( | ||||
|           this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset); | ||||
|       if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) { | ||||
|         break; | ||||
|       } | ||||
|       if (desc_status != ESP_GATT_OK) { | ||||
|         ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_, | ||||
|                  this->address_str().c_str(), desc_status); | ||||
|         break; | ||||
|       } | ||||
|       if (desc_count == 0) { | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       characteristic_resp.descriptors.emplace_back(); | ||||
|       auto &descriptor_resp = characteristic_resp.descriptors.back(); | ||||
|       descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); | ||||
|       descriptor_resp.handle = desc_result.handle; | ||||
|       desc_offset++; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Send the message (we already checked api_conn is not null at the beginning) | ||||
|   api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                               esp_ble_gattc_cb_param_t *param) { | ||||
|   if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) | ||||
| @@ -25,22 +193,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|  | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); | ||||
|       this->set_address(0); | ||||
|       this->proxy_->send_connections_free(); | ||||
|       this->reset_connection_(param->disconnect.reason); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_CLOSE_EVT: { | ||||
|       this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason); | ||||
|       this->set_address(0); | ||||
|       this->proxy_->send_connections_free(); | ||||
|       this->reset_connection_(param->close.reason); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { | ||||
|         this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); | ||||
|         this->set_address(0); | ||||
|         this->proxy_->send_connections_free(); | ||||
|         this->reset_connection_(param->open.status); | ||||
|       } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { | ||||
|         this->proxy_->send_device_connection(this->address_, true, this->mtu_); | ||||
|         this->proxy_->send_connections_free(); | ||||
| @@ -75,7 +237,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|       resp.data.reserve(param->read.value_len); | ||||
|       // Use bulk insert instead of individual push_backs | ||||
|       resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len); | ||||
|       this->proxy_->get_api_connection()->send_message(resp); | ||||
|       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_WRITE_CHAR_EVT: | ||||
| @@ -89,7 +251,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|       api::BluetoothGATTWriteResponse resp; | ||||
|       resp.address = this->address_; | ||||
|       resp.handle = param->write.handle; | ||||
|       this->proxy_->get_api_connection()->send_message(resp); | ||||
|       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { | ||||
| @@ -103,7 +265,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|       api::BluetoothGATTNotifyResponse resp; | ||||
|       resp.address = this->address_; | ||||
|       resp.handle = param->unreg_for_notify.handle; | ||||
|       this->proxy_->get_api_connection()->send_message(resp); | ||||
|       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||
| @@ -116,7 +278,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|       api::BluetoothGATTNotifyResponse resp; | ||||
|       resp.address = this->address_; | ||||
|       resp.handle = param->reg_for_notify.handle; | ||||
|       this->proxy_->get_api_connection()->send_message(resp); | ||||
|       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_NOTIFY_EVT: { | ||||
| @@ -128,7 +290,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|       resp.data.reserve(param->notify.value_len); | ||||
|       // Use bulk insert instead of individual push_backs | ||||
|       resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len); | ||||
|       this->proxy_->get_api_connection()->send_message(resp); | ||||
|       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|   | ||||
| @@ -12,6 +12,7 @@ class BluetoothProxy; | ||||
| class BluetoothConnection : public esp32_ble_client::BLEClientBase { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
|   bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|   void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; | ||||
| @@ -27,6 +28,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { | ||||
|  protected: | ||||
|   friend class BluetoothProxy; | ||||
|  | ||||
|   void send_service_for_discovery_(); | ||||
|   void reset_connection_(esp_err_t reason); | ||||
|  | ||||
|   // Memory optimized layout for 32-bit systems | ||||
|   // Group 1: Pointers (4 bytes each, naturally aligned) | ||||
|   BluetoothProxy *proxy_; | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/macros.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include <cstring> | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| @@ -10,23 +11,31 @@ namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
|  | ||||
| static const char *const TAG = "bluetooth_proxy"; | ||||
| static const int DONE_SENDING_SERVICES = -2; | ||||
|  | ||||
| std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { | ||||
|   esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); | ||||
|   return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), | ||||
|                                ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | | ||||
|                                    ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; | ||||
| } | ||||
| // Batch size for BLE advertisements to maximize WiFi efficiency | ||||
| // Each advertisement is up to 80 bytes when packaged (including protocol overhead) | ||||
| // Most advertisements are 20-30 bytes, allowing even more to fit per packet | ||||
| // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload | ||||
| // This achieves ~97% WiFi MTU utilization while staying under the limit | ||||
| static constexpr size_t FLUSH_BATCH_SIZE = 16; | ||||
|  | ||||
| // Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response) | ||||
| static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62, | ||||
|               "BLE advertisement data array size mismatch"); | ||||
|  | ||||
| BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } | ||||
|  | ||||
| void BluetoothProxy::setup() { | ||||
|   // Pre-allocate response object | ||||
|   this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>(); | ||||
|  | ||||
|   // Reserve capacity but start with size 0 | ||||
|   // Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE | ||||
|   this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2); | ||||
|  | ||||
|   // Don't pre-allocate pool - let it grow only if needed in busy environments | ||||
|   // Many devices in quiet areas will never need the overflow pool | ||||
|  | ||||
|   this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { | ||||
|     if (this->api_connection_ != nullptr) { | ||||
|       this->send_bluetooth_scanner_state_(state); | ||||
| @@ -39,128 +48,91 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta | ||||
|   resp.state = static_cast<api::enums::BluetoothScannerState>(state); | ||||
|   resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE | ||||
|                                                : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; | ||||
|   this->api_connection_->send_message(resp); | ||||
|   this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
| bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) | ||||
|     return false; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), | ||||
|            device.get_rssi()); | ||||
|   this->send_api_packet_(device); | ||||
|   return true; | ||||
|   // This method should never be called since bluetooth_proxy always uses raw advertisements | ||||
|   // but we need to provide an implementation to satisfy the virtual method requirement | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| // Batch size for BLE advertisements to maximize WiFi efficiency | ||||
| // Each advertisement is up to 80 bytes when packaged (including protocol overhead) | ||||
| // Most advertisements are 20-30 bytes, allowing even more to fit per packet | ||||
| // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload | ||||
| // This achieves ~97% WiFi MTU utilization while staying under the limit | ||||
| static constexpr size_t FLUSH_BATCH_SIZE = 16; | ||||
|  | ||||
| namespace { | ||||
| // Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes) | ||||
| // This is initialized at program startup before any threads | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| std::vector<api::BluetoothLERawAdvertisement> batch_buffer; | ||||
| }  // namespace | ||||
|  | ||||
| static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; } | ||||
| #endif | ||||
|  | ||||
| bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { | ||||
|   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) | ||||
|   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) | ||||
|     return false; | ||||
|  | ||||
|   // Get the batch buffer reference | ||||
|   auto &batch_buffer = get_batch_buffer(); | ||||
|   auto &advertisements = this->response_->advertisements; | ||||
|  | ||||
|   // Reserve additional capacity if needed | ||||
|   size_t new_size = batch_buffer.size() + count; | ||||
|   if (batch_buffer.capacity() < new_size) { | ||||
|     batch_buffer.reserve(new_size); | ||||
|   } | ||||
|  | ||||
|   // Add new advertisements to the batch buffer | ||||
|   for (size_t i = 0; i < count; i++) { | ||||
|     auto &result = scan_results[i]; | ||||
|     uint8_t length = result.adv_data_len + result.scan_rsp_len; | ||||
|  | ||||
|     batch_buffer.emplace_back(); | ||||
|     auto &adv = batch_buffer.back(); | ||||
|     // Check if we need to expand the vector | ||||
|     if (this->advertisement_count_ >= advertisements.size()) { | ||||
|       if (this->advertisement_pool_.empty()) { | ||||
|         // No room in pool, need to allocate | ||||
|         advertisements.emplace_back(); | ||||
|       } else { | ||||
|         // Pull from pool | ||||
|         advertisements.push_back(std::move(this->advertisement_pool_.back())); | ||||
|         this->advertisement_pool_.pop_back(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Fill in the data directly at current position | ||||
|     auto &adv = advertisements[this->advertisement_count_]; | ||||
|     adv.address = esp32_ble::ble_addr_to_uint64(result.bda); | ||||
|     adv.rssi = result.rssi; | ||||
|     adv.address_type = result.ble_addr_type; | ||||
|     adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); | ||||
|     adv.data_len = length; | ||||
|     std::memcpy(adv.data, result.ble_adv, length); | ||||
|  | ||||
|     this->advertisement_count_++; | ||||
|  | ||||
|     ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], | ||||
|              result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); | ||||
|   } | ||||
|  | ||||
|   // Only send if we've accumulated a good batch size to maximize batching efficiency | ||||
|   // https://github.com/esphome/backlog/issues/21 | ||||
|   if (batch_buffer.size() >= FLUSH_BATCH_SIZE) { | ||||
|     this->flush_pending_advertisements(); | ||||
|     // Flush if we have reached FLUSH_BATCH_SIZE | ||||
|     if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) { | ||||
|       this->flush_pending_advertisements(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::flush_pending_advertisements() { | ||||
|   auto &batch_buffer = get_batch_buffer(); | ||||
|   if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) | ||||
|   if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   api::BluetoothLERawAdvertisementsResponse resp; | ||||
|   resp.advertisements.swap(batch_buffer); | ||||
|   this->api_connection_->send_message(resp); | ||||
| } | ||||
|   auto &advertisements = this->response_->advertisements; | ||||
|  | ||||
| void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|   api::BluetoothLEAdvertisementResponse resp; | ||||
|   resp.address = device.address_uint64(); | ||||
|   resp.address_type = device.get_address_type(); | ||||
|   if (!device.get_name().empty()) | ||||
|     resp.name = device.get_name(); | ||||
|   resp.rssi = device.get_rssi(); | ||||
|   // Return any items beyond advertisement_count_ to the pool | ||||
|   if (advertisements.size() > this->advertisement_count_) { | ||||
|     // Move unused items back to pool | ||||
|     this->advertisement_pool_.insert(this->advertisement_pool_.end(), | ||||
|                                      std::make_move_iterator(advertisements.begin() + this->advertisement_count_), | ||||
|                                      std::make_move_iterator(advertisements.end())); | ||||
|  | ||||
|   // Pre-allocate vectors based on known sizes | ||||
|   auto service_uuids = device.get_service_uuids(); | ||||
|   resp.service_uuids.reserve(service_uuids.size()); | ||||
|   for (auto &uuid : service_uuids) { | ||||
|     resp.service_uuids.emplace_back(uuid.to_string()); | ||||
|     // Resize to actual count | ||||
|     advertisements.resize(this->advertisement_count_); | ||||
|   } | ||||
|  | ||||
|   // Pre-allocate service data vector | ||||
|   auto service_datas = device.get_service_datas(); | ||||
|   resp.service_data.reserve(service_datas.size()); | ||||
|   for (auto &data : service_datas) { | ||||
|     resp.service_data.emplace_back(); | ||||
|     auto &service_data = resp.service_data.back(); | ||||
|     service_data.uuid = data.uuid.to_string(); | ||||
|     service_data.data.assign(data.data.begin(), data.data.end()); | ||||
|   } | ||||
|   // Send the message | ||||
|   this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); | ||||
|  | ||||
|   // Pre-allocate manufacturer data vector | ||||
|   auto manufacturer_datas = device.get_manufacturer_datas(); | ||||
|   resp.manufacturer_data.reserve(manufacturer_datas.size()); | ||||
|   for (auto &data : manufacturer_datas) { | ||||
|     resp.manufacturer_data.emplace_back(); | ||||
|     auto &manufacturer_data = resp.manufacturer_data.back(); | ||||
|     manufacturer_data.uuid = data.uuid.to_string(); | ||||
|     manufacturer_data.data.assign(data.data.begin(), data.data.end()); | ||||
|   } | ||||
|  | ||||
|   this->api_connection_->send_message(resp); | ||||
|   // Reset count - existing items will be overwritten in next batch | ||||
|   this->advertisement_count_ = 0; | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Active: %s\n" | ||||
|                 "  Connections: %d\n" | ||||
|                 "  Raw advertisements: %s", | ||||
|                 YESNO(this->active_), this->connections_.size(), YESNO(this->raw_advertisements_)); | ||||
|                 "  Connections: %d", | ||||
|                 YESNO(this->active_), this->connections_.size()); | ||||
| } | ||||
|  | ||||
| int BluetoothProxy::get_bluetooth_connections_free() { | ||||
| @@ -188,139 +160,17 @@ void BluetoothProxy::loop() { | ||||
|   } | ||||
|  | ||||
|   // Flush any pending BLE advertisements that have been accumulated but not yet sent | ||||
|   if (this->raw_advertisements_) { | ||||
|     static uint32_t last_flush_time = 0; | ||||
|     uint32_t now = App.get_loop_component_start_time(); | ||||
|   uint32_t now = App.get_loop_component_start_time(); | ||||
|  | ||||
|     // Flush accumulated advertisements every 100ms | ||||
|     if (now - last_flush_time >= 100) { | ||||
|       this->flush_pending_advertisements(); | ||||
|       last_flush_time = now; | ||||
|     } | ||||
|   } | ||||
|   for (auto *connection : this->connections_) { | ||||
|     if (connection->send_service_ == connection->service_count_) { | ||||
|       connection->send_service_ = DONE_SENDING_SERVICES; | ||||
|       this->send_gatt_services_done(connection->get_address()); | ||||
|       if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || | ||||
|           connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { | ||||
|         connection->release_services(); | ||||
|       } | ||||
|     } else if (connection->send_service_ >= 0) { | ||||
|       esp_gattc_service_elem_t service_result; | ||||
|       uint16_t service_count = 1; | ||||
|       esp_gatt_status_t service_status = | ||||
|           esp_ble_gattc_get_service(connection->get_gattc_if(), connection->get_conn_id(), nullptr, &service_result, | ||||
|                                     &service_count, connection->send_service_); | ||||
|       connection->send_service_++; | ||||
|       if (service_status != ESP_GATT_OK) { | ||||
|         ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", | ||||
|                  connection->get_connection_index(), connection->address_str().c_str(), connection->send_service_ - 1, | ||||
|                  service_status); | ||||
|         continue; | ||||
|       } | ||||
|       if (service_count == 0) { | ||||
|         ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", | ||||
|                  connection->get_connection_index(), connection->address_str().c_str(), service_count); | ||||
|         continue; | ||||
|       } | ||||
|       api::BluetoothGATTGetServicesResponse resp; | ||||
|       resp.address = connection->get_address(); | ||||
|       resp.services.reserve(1);  // Always one service per response in this implementation | ||||
|       api::BluetoothGATTService service_resp; | ||||
|       service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); | ||||
|       service_resp.handle = service_result.start_handle; | ||||
|       uint16_t char_offset = 0; | ||||
|       esp_gattc_char_elem_t char_result; | ||||
|       // Get the number of characteristics directly with one call | ||||
|       uint16_t total_char_count = 0; | ||||
|       esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count( | ||||
|           connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC, | ||||
|           service_result.start_handle, service_result.end_handle, 0, &total_char_count); | ||||
|  | ||||
|       if (char_count_status == ESP_GATT_OK && total_char_count > 0) { | ||||
|         // Only reserve if we successfully got a count | ||||
|         service_resp.characteristics.reserve(total_char_count); | ||||
|       } else if (char_count_status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(), | ||||
|                  connection->address_str().c_str(), char_count_status); | ||||
|       } | ||||
|  | ||||
|       // Now process characteristics | ||||
|       while (true) {  // characteristics | ||||
|         uint16_t char_count = 1; | ||||
|         esp_gatt_status_t char_status = esp_ble_gattc_get_all_char( | ||||
|             connection->get_gattc_if(), connection->get_conn_id(), service_result.start_handle, | ||||
|             service_result.end_handle, &char_result, &char_count, char_offset); | ||||
|         if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) { | ||||
|           break; | ||||
|         } | ||||
|         if (char_status != ESP_GATT_OK) { | ||||
|           ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", connection->get_connection_index(), | ||||
|                    connection->address_str().c_str(), char_status); | ||||
|           break; | ||||
|         } | ||||
|         if (char_count == 0) { | ||||
|           break; | ||||
|         } | ||||
|         api::BluetoothGATTCharacteristic characteristic_resp; | ||||
|         characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); | ||||
|         characteristic_resp.handle = char_result.char_handle; | ||||
|         characteristic_resp.properties = char_result.properties; | ||||
|         char_offset++; | ||||
|  | ||||
|         // Get the number of descriptors directly with one call | ||||
|         uint16_t total_desc_count = 0; | ||||
|         esp_gatt_status_t desc_count_status = | ||||
|             esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR, | ||||
|                                          char_result.char_handle, service_result.end_handle, 0, &total_desc_count); | ||||
|  | ||||
|         if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) { | ||||
|           // Only reserve if we successfully got a count | ||||
|           characteristic_resp.descriptors.reserve(total_desc_count); | ||||
|         } else if (desc_count_status != ESP_GATT_OK) { | ||||
|           ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", | ||||
|                    connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle, | ||||
|                    desc_count_status); | ||||
|         } | ||||
|  | ||||
|         // Now process descriptors | ||||
|         uint16_t desc_offset = 0; | ||||
|         esp_gattc_descr_elem_t desc_result; | ||||
|         while (true) {  // descriptors | ||||
|           uint16_t desc_count = 1; | ||||
|           esp_gatt_status_t desc_status = | ||||
|               esp_ble_gattc_get_all_descr(connection->get_gattc_if(), connection->get_conn_id(), | ||||
|                                           char_result.char_handle, &desc_result, &desc_count, desc_offset); | ||||
|           if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) { | ||||
|             break; | ||||
|           } | ||||
|           if (desc_status != ESP_GATT_OK) { | ||||
|             ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", connection->get_connection_index(), | ||||
|                      connection->address_str().c_str(), desc_status); | ||||
|             break; | ||||
|           } | ||||
|           if (desc_count == 0) { | ||||
|             break; | ||||
|           } | ||||
|           api::BluetoothGATTDescriptor descriptor_resp; | ||||
|           descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); | ||||
|           descriptor_resp.handle = desc_result.handle; | ||||
|           characteristic_resp.descriptors.push_back(std::move(descriptor_resp)); | ||||
|           desc_offset++; | ||||
|         } | ||||
|         service_resp.characteristics.push_back(std::move(characteristic_resp)); | ||||
|       } | ||||
|       resp.services.push_back(std::move(service_resp)); | ||||
|       this->api_connection_->send_message(resp); | ||||
|     } | ||||
|   // Flush accumulated advertisements every 100ms | ||||
|   if (now - this->last_advertisement_flush_time_ >= 100) { | ||||
|     this->flush_pending_advertisements(); | ||||
|     this->last_advertisement_flush_time_ = now; | ||||
|   } | ||||
| } | ||||
|  | ||||
| esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { | ||||
|   if (this->raw_advertisements_) | ||||
|     return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; | ||||
|   return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS; | ||||
|   return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; | ||||
| } | ||||
|  | ||||
| BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { | ||||
| @@ -465,7 +315,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | ||||
|       call.success = ret == ESP_OK; | ||||
|       call.error = ret; | ||||
|  | ||||
|       this->api_connection_->send_message(call); | ||||
|       this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); | ||||
|  | ||||
|       break; | ||||
|     } | ||||
| @@ -565,7 +415,6 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection | ||||
|     return; | ||||
|   } | ||||
|   this->api_connection_ = api_connection; | ||||
|   this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; | ||||
|   this->parent_->recalculate_advertisement_parser_types(); | ||||
|  | ||||
|   this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state()); | ||||
| @@ -577,7 +426,6 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti | ||||
|     return; | ||||
|   } | ||||
|   this->api_connection_ = nullptr; | ||||
|   this->raw_advertisements_ = false; | ||||
|   this->parent_->recalculate_advertisement_parser_types(); | ||||
| } | ||||
|  | ||||
| @@ -589,7 +437,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui | ||||
|   call.connected = connected; | ||||
|   call.mtu = mtu; | ||||
|   call.error = error; | ||||
|   this->api_connection_->send_message(call); | ||||
|   this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); | ||||
| } | ||||
| void BluetoothProxy::send_connections_free() { | ||||
|   if (this->api_connection_ == nullptr) | ||||
| @@ -602,7 +450,7 @@ void BluetoothProxy::send_connections_free() { | ||||
|       call.allocated.push_back(connection->address_); | ||||
|     } | ||||
|   } | ||||
|   this->api_connection_->send_message(call); | ||||
|   this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::send_gatt_services_done(uint64_t address) { | ||||
| @@ -610,7 +458,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { | ||||
|     return; | ||||
|   api::BluetoothGATTGetServicesDoneResponse call; | ||||
|   call.address = address; | ||||
|   this->api_connection_->send_message(call); | ||||
|   this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { | ||||
| @@ -620,7 +468,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ | ||||
|   call.address = address; | ||||
|   call.handle = handle; | ||||
|   call.error = error; | ||||
|   this->api_connection_->send_message(call); | ||||
|   this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { | ||||
| @@ -629,7 +477,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ | ||||
|   call.paired = paired; | ||||
|   call.error = error; | ||||
|  | ||||
|   this->api_connection_->send_message(call); | ||||
|   this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { | ||||
| @@ -638,7 +486,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e | ||||
|   call.success = success; | ||||
|   call.error = error; | ||||
|  | ||||
|   this->api_connection_->send_message(call); | ||||
|   this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { | ||||
|   | ||||
| @@ -22,6 +22,7 @@ namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
|  | ||||
| static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; | ||||
| static const int DONE_SENDING_SERVICES = -2; | ||||
|  | ||||
| using namespace esp32_ble_client; | ||||
|  | ||||
| @@ -51,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t { | ||||
| class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { | ||||
|  public: | ||||
|   BluetoothProxy(); | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; | ||||
| #endif | ||||
|   bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; | ||||
|   void dump_config() override; | ||||
|   void setup() override; | ||||
| @@ -129,7 +132,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); | ||||
|   void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); | ||||
|  | ||||
|   BluetoothConnection *get_connection_(uint64_t address, bool reserve); | ||||
| @@ -141,9 +143,16 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|   // Group 2: Container types (typically 12 bytes on 32-bit) | ||||
|   std::vector<BluetoothConnection *> connections_{}; | ||||
|  | ||||
|   // Group 3: 1-byte types grouped together | ||||
|   // BLE advertisement batching | ||||
|   std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_; | ||||
|   std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_; | ||||
|  | ||||
|   // Group 3: 4-byte types | ||||
|   uint32_t last_advertisement_flush_time_{0}; | ||||
|  | ||||
|   // Group 4: 1-byte types grouped together | ||||
|   bool active_; | ||||
|   bool raw_advertisements_{false}; | ||||
|   uint8_t advertisement_count_{0}; | ||||
|   // 2 bytes used, 2 bytes padding | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| CONF_BYTE_ORDER = "byte_order" | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_DRAW_ROUNDING = "draw_rounding" | ||||
| CONF_ON_STATE_CHANGE = "on_state_change" | ||||
| CONF_REQUEST_HEADERS = "request_headers" | ||||
|   | ||||
| @@ -53,6 +53,7 @@ void DebugComponent::on_shutdown() { | ||||
|   auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); | ||||
|   if (component != nullptr) { | ||||
|     strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1); | ||||
|     buffer[REBOOT_MAX_LEN - 1] = '\0'; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Storing reboot source: %s", buffer); | ||||
|   pref.save(&buffer); | ||||
| @@ -68,6 +69,7 @@ std::string DebugComponent::get_reset_reason_() { | ||||
|       auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); | ||||
|       char buffer[REBOOT_MAX_LEN]{}; | ||||
|       if (pref.load(&buffer)) { | ||||
|         buffer[REBOOT_MAX_LEN - 1] = '\0'; | ||||
|         reset_reason = "Reboot request from " + std::string(buffer); | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from esphome import automation, pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import time | ||||
| from esphome.components import esp32, time | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
| @@ -116,12 +116,20 @@ def validate_pin_number(value): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def validate_config(config): | ||||
|     if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: | ||||
|         raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") | ||||
|     if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: | ||||
|         raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") | ||||
|     return config | ||||
| def _validate_ex1_wakeup_mode(value): | ||||
|     if value == "ALL_LOW": | ||||
|         esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) | ||||
|     if value == "ANY_LOW": | ||||
|         esp32.only_on_variant( | ||||
|             supported=[ | ||||
|                 VARIANT_ESP32S2, | ||||
|                 VARIANT_ESP32S3, | ||||
|                 VARIANT_ESP32C6, | ||||
|                 VARIANT_ESP32H2, | ||||
|             ], | ||||
|             msg_prefix="ANY_LOW", | ||||
|         )(value) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") | ||||
| @@ -148,6 +156,7 @@ WAKEUP_PIN_MODES = { | ||||
| esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t") | ||||
| Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup") | ||||
| EXT1_WAKEUP_MODES = { | ||||
|     "ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW, | ||||
|     "ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW, | ||||
|     "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, | ||||
| } | ||||
| @@ -187,16 +196,28 @@ CONFIG_SCHEMA = cv.All( | ||||
|             ), | ||||
|             cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( | ||||
|                 cv.only_on_esp32, | ||||
|                 esp32.only_on_variant( | ||||
|                     unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1" | ||||
|                 ), | ||||
|                 cv.Schema( | ||||
|                     { | ||||
|                         cv.Required(CONF_PINS): cv.ensure_list( | ||||
|                             pins.internal_gpio_input_pin_schema, validate_pin_number | ||||
|                         ), | ||||
|                         cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), | ||||
|                         cv.Required(CONF_MODE): cv.All( | ||||
|                             cv.enum(EXT1_WAKEUP_MODES, upper=True), | ||||
|                             _validate_ex1_wakeup_mode, | ||||
|                         ), | ||||
|                     } | ||||
|                 ), | ||||
|             ), | ||||
|             cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), | ||||
|             cv.Optional(CONF_TOUCH_WAKEUP): cv.All( | ||||
|                 cv.only_on_esp32, | ||||
|                 esp32.only_on_variant( | ||||
|                     unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch" | ||||
|                 ), | ||||
|                 cv.boolean, | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), | ||||
|   | ||||
| @@ -17,6 +17,7 @@ from esphome.const import ( | ||||
|     CONF_MODE, | ||||
|     CONF_NUMBER, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_SWITCH, | ||||
|     CONF_TEXT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
| @@ -33,7 +34,6 @@ CONF_LABEL = "label" | ||||
| CONF_MENU = "menu" | ||||
| CONF_BACK = "back" | ||||
| CONF_SELECT = "select" | ||||
| CONF_SWITCH = "switch" | ||||
| CONF_ON_TEXT = "on_text" | ||||
| CONF_OFF_TEXT = "off_text" | ||||
| CONF_VALUE_LAMBDA = "value_lambda" | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include "esphome/components/network/ip_address.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/util.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include <lwip/igmp.h> | ||||
| #include <lwip/init.h> | ||||
| @@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() { | ||||
|     ip4_addr_t multicast_addr = | ||||
|         network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); | ||||
|  | ||||
|     auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); | ||||
|     err_t err; | ||||
|     { | ||||
|       LwIPLock lock; | ||||
|       err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); | ||||
|     } | ||||
|  | ||||
|     if (err) { | ||||
|       ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); | ||||
| @@ -104,6 +109,7 @@ void E131Component::leave_(int universe) { | ||||
|   if (listen_method_ == E131_MULTICAST) { | ||||
|     ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); | ||||
|  | ||||
|     LwIPLock lock; | ||||
|     igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -31,6 +31,7 @@ from esphome.const import ( | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PLATFORM_ESP32, | ||||
|     CoreModel, | ||||
|     __version__, | ||||
| ) | ||||
| from esphome.core import CORE, HexInt, TimePeriod | ||||
| @@ -39,7 +40,7 @@ import esphome.final_validate as fv | ||||
| from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| from .boards import BOARDS | ||||
| from .boards import BOARDS, STANDARD_BOARDS | ||||
| from .const import (  # noqa | ||||
|     KEY_BOARD, | ||||
|     KEY_COMPONENTS, | ||||
| @@ -189,7 +190,7 @@ def get_download_types(storage_json): | ||||
|     ] | ||||
|  | ||||
|  | ||||
| def only_on_variant(*, supported=None, unsupported=None): | ||||
| def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"): | ||||
|     """Config validator for features only available on some ESP32 variants.""" | ||||
|     if supported is not None and not isinstance(supported, list): | ||||
|         supported = [supported] | ||||
| @@ -200,11 +201,11 @@ def only_on_variant(*, supported=None, unsupported=None): | ||||
|         variant = get_esp32_variant() | ||||
|         if supported is not None and variant not in supported: | ||||
|             raise cv.Invalid( | ||||
|                 f"This feature is only available on {', '.join(supported)}" | ||||
|                 f"{msg_prefix} is only available on {', '.join(supported)}" | ||||
|             ) | ||||
|         if unsupported is not None and variant in unsupported: | ||||
|             raise cv.Invalid( | ||||
|                 f"This feature is not available on {', '.join(unsupported)}" | ||||
|                 f"{msg_prefix} is not available on {', '.join(unsupported)}" | ||||
|             ) | ||||
|         return obj | ||||
|  | ||||
| @@ -487,25 +488,32 @@ def _platform_is_platformio(value): | ||||
|  | ||||
|  | ||||
| def _detect_variant(value): | ||||
|     board = value[CONF_BOARD] | ||||
|     if board in BOARDS: | ||||
|         variant = BOARDS[board][KEY_VARIANT] | ||||
|         if CONF_VARIANT in value and variant != value[CONF_VARIANT]: | ||||
|     board = value.get(CONF_BOARD) | ||||
|     variant = value.get(CONF_VARIANT) | ||||
|     if variant and board is None: | ||||
|         # If variant is set, we can derive the board from it | ||||
|         # variant has already been validated against the known set | ||||
|         value = value.copy() | ||||
|         value[CONF_BOARD] = STANDARD_BOARDS[variant] | ||||
|     elif board in BOARDS: | ||||
|         variant = variant or BOARDS[board][KEY_VARIANT] | ||||
|         if variant != BOARDS[board][KEY_VARIANT]: | ||||
|             raise cv.Invalid( | ||||
|                 f"Option '{CONF_VARIANT}' does not match selected board.", | ||||
|                 path=[CONF_VARIANT], | ||||
|             ) | ||||
|         value = value.copy() | ||||
|         value[CONF_VARIANT] = variant | ||||
|     elif not variant: | ||||
|         raise cv.Invalid( | ||||
|             "This board is unknown, if you are sure you want to compile with this board selection, " | ||||
|             f"override with option '{CONF_VARIANT}'", | ||||
|             path=[CONF_BOARD], | ||||
|         ) | ||||
|     else: | ||||
|         if CONF_VARIANT not in value: | ||||
|             raise cv.Invalid( | ||||
|                 "This board is unknown, if you are sure you want to compile with this board selection, " | ||||
|                 f"override with option '{CONF_VARIANT}'", | ||||
|                 path=[CONF_BOARD], | ||||
|             ) | ||||
|         _LOGGER.warning( | ||||
|             "This board is unknown. Make sure the chosen chip component is correct.", | ||||
|             "This board is unknown; the specified variant '%s' will be used but this may not work as expected.", | ||||
|             variant, | ||||
|         ) | ||||
|     return value | ||||
|  | ||||
| @@ -676,7 +684,7 @@ CONF_PARTITIONS = "partitions" | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_BOARD): cv.string_strict, | ||||
|             cv.Optional(CONF_BOARD): cv.string_strict, | ||||
|             cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( | ||||
|                 *FULL_CPU_FREQUENCIES, upper=True | ||||
|             ), | ||||
| @@ -691,6 +699,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|     _detect_variant, | ||||
|     _set_default_framework, | ||||
|     set_core_data, | ||||
|     cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -705,8 +714,10 @@ async def to_code(config): | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") | ||||
|     cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) | ||||
|     cg.add_define(CoreModel.MULTI_ATOMICS) | ||||
|  | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|     cg.add_platformio_option("lib_compat_mode", "strict") | ||||
|  | ||||
|     framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] | ||||
|  | ||||
|   | ||||
| @@ -2,13 +2,30 @@ from .const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32C5, | ||||
|     VARIANT_ESP32C6, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32P4, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
|     VARIANTS, | ||||
| ) | ||||
|  | ||||
| STANDARD_BOARDS = { | ||||
|     VARIANT_ESP32: "esp32dev", | ||||
|     VARIANT_ESP32C2: "esp32-c2-devkitm-1", | ||||
|     VARIANT_ESP32C3: "esp32-c3-devkitm-1", | ||||
|     VARIANT_ESP32C5: "esp32-c5-devkitc-1", | ||||
|     VARIANT_ESP32C6: "esp32-c6-devkitm-1", | ||||
|     VARIANT_ESP32H2: "esp32-h2-devkitm-1", | ||||
|     VARIANT_ESP32P4: "esp32-p4-evboard", | ||||
|     VARIANT_ESP32S2: "esp32-s2-kaluga-1", | ||||
|     VARIANT_ESP32S3: "esp32-s3-devkitc-1", | ||||
| } | ||||
|  | ||||
| # Make sure not missed here if a new variant added. | ||||
| assert all(v in STANDARD_BOARDS for v in VARIANTS) | ||||
|  | ||||
| ESP32_BASE_PINS = { | ||||
|     "TX": 1, | ||||
|     "RX": 3, | ||||
|   | ||||
| @@ -114,7 +114,6 @@ void ESP32InternalGPIOPin::setup() { | ||||
|   if (flags_ & gpio::FLAG_OUTPUT) { | ||||
|     gpio_set_drive_capability(pin_, drive_strength_); | ||||
|   } | ||||
|   ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT); | ||||
| } | ||||
|  | ||||
| void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| @@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } | ||||
| IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } | ||||
| IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } | ||||
|  | ||||
| #ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING | ||||
| #include "lwip/priv/tcpip_priv.h" | ||||
| #endif | ||||
|  | ||||
| LwIPLock::LwIPLock() { | ||||
| #ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING | ||||
|   // When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect | ||||
|   // its internal state. Any thread can take this lock to safely access lwIP APIs. | ||||
|   // | ||||
|   // sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread | ||||
|   // already holds the lwIP core lock. This prevents recursive locking attempts and | ||||
|   // allows nested LwIPLock instances to work correctly. | ||||
|   // | ||||
|   // If we don't already hold the lock, acquire it. This will block until the lock | ||||
|   // is available if another thread currently holds it. | ||||
|   if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { | ||||
|     LOCK_TCPIP_CORE(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| LwIPLock::~LwIPLock() { | ||||
| #ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING | ||||
|   // Only release the lwIP core lock if this thread currently holds it. | ||||
|   // | ||||
|   // sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock | ||||
|   // ownership tracking. It returns true only if the current thread is registered | ||||
|   // as the lock holder. | ||||
|   // | ||||
|   // This check is essential because: | ||||
|   // 1. We may not have acquired the lock in the constructor (if we already held it) | ||||
|   // 2. The lock might have been released by other means between constructor and destructor | ||||
|   // 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior | ||||
|   if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { | ||||
|     UNLOCK_TCPIP_CORE(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||
| #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) | ||||
|   // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default | ||||
|   | ||||
| @@ -105,6 +105,7 @@ void BLEClientBase::dump_config() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
| bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { | ||||
|   if (!this->auto_connect_) | ||||
|     return false; | ||||
| @@ -122,6 +123,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { | ||||
|   this->remote_addr_type_ = device.get_address_type(); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void BLEClientBase::connect() { | ||||
|   ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), | ||||
|   | ||||
| @@ -31,7 +31,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void run_later(std::function<void()> &&f);  // NOLINT | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
|   bool parse_device(const espbt::ESPBTDevice &device) override; | ||||
| #endif | ||||
|   void on_scan_end() override {} | ||||
|   bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|   | ||||
| @@ -31,6 +31,8 @@ from esphome.const import ( | ||||
|     CONF_TRIGGER_ID, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.enum import StrEnum | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| AUTO_LOAD = ["esp32_ble"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
| @@ -50,6 +52,25 @@ IDF_MAX_CONNECTIONS = 9 | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| # Enum for BLE features | ||||
| class BLEFeatures(StrEnum): | ||||
|     ESP_BT_DEVICE = "ESP_BT_DEVICE" | ||||
|  | ||||
|  | ||||
| # Set to track which features are needed by components | ||||
| _required_features: set[BLEFeatures] = set() | ||||
|  | ||||
|  | ||||
| def register_ble_features(features: set[BLEFeatures]) -> None: | ||||
|     """Register BLE features that a component needs. | ||||
|  | ||||
|     Args: | ||||
|         features: Set of BLEFeatures enum members | ||||
|     """ | ||||
|     _required_features.update(features) | ||||
|  | ||||
|  | ||||
| esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") | ||||
| ESP32BLETracker = esp32_ble_tracker_ns.class_( | ||||
|     "ESP32BLETracker", | ||||
| @@ -277,6 +298,15 @@ async def to_code(config): | ||||
|     cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) | ||||
|     cg.add(var.set_scan_active(params[CONF_ACTIVE])) | ||||
|     cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) | ||||
|  | ||||
|     # Register ESP_BT_DEVICE feature if any of the automation triggers are used | ||||
|     if ( | ||||
|         config.get(CONF_ON_BLE_ADVERTISE) | ||||
|         or config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE) | ||||
|         or config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE) | ||||
|     ): | ||||
|         register_ble_features({BLEFeatures.ESP_BT_DEVICE}) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_BLE_ADVERTISE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         if CONF_MAC_ADDRESS in conf: | ||||
| @@ -334,6 +364,11 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts | ||||
|     cg.add_define("USE_ESP32_BLE_CLIENT") | ||||
|  | ||||
|     # Add feature-specific defines based on what's needed | ||||
|     if BLEFeatures.ESP_BT_DEVICE in _required_features: | ||||
|         cg.add_define("USE_ESP32_BLE_DEVICE") | ||||
|  | ||||
|     if config.get(CONF_SOFTWARE_COEXISTENCE): | ||||
|         cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") | ||||
|  | ||||
| @@ -382,13 +417,43 @@ async def esp32_ble_tracker_stop_scan_action_to_code( | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def register_ble_device(var, config): | ||||
| async def register_ble_device( | ||||
|     var: cg.SafeExpType, config: ConfigType | ||||
| ) -> cg.SafeExpType: | ||||
|     register_ble_features({BLEFeatures.ESP_BT_DEVICE}) | ||||
|     paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) | ||||
|     cg.add(paren.register_listener(var)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def register_client(var, config): | ||||
| async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType: | ||||
|     register_ble_features({BLEFeatures.ESP_BT_DEVICE}) | ||||
|     paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) | ||||
|     cg.add(paren.register_client(var)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def register_raw_ble_device( | ||||
|     var: cg.SafeExpType, config: ConfigType | ||||
| ) -> cg.SafeExpType: | ||||
|     """Register a BLE device listener that only needs raw advertisement data. | ||||
|  | ||||
|     This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice | ||||
|     will not be compiled in if this is the only registration method used. | ||||
|     """ | ||||
|     paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) | ||||
|     cg.add(paren.register_listener(var)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def register_raw_client( | ||||
|     var: cg.SafeExpType, config: ConfigType | ||||
| ) -> cg.SafeExpType: | ||||
|     """Register a BLE client that only needs raw advertisement data. | ||||
|  | ||||
|     This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice | ||||
|     will not be compiled in if this is the only registration method used. | ||||
|     """ | ||||
|     paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) | ||||
|     cg.add(paren.register_client(var)) | ||||
|     return var | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble_tracker { | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
| class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener { | ||||
|  public: | ||||
|   explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } | ||||
| @@ -87,6 +88,7 @@ class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { | ||||
|   bool parse_device(const ESPBTDevice &device) override { return false; } | ||||
|   void on_scan_end() override { this->trigger(); } | ||||
| }; | ||||
| #endif  // USE_ESP32_BLE_DEVICE | ||||
|  | ||||
| template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> { | ||||
|  public: | ||||
|   | ||||
| @@ -128,44 +128,53 @@ void ESP32BLETracker::loop() { | ||||
|     uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); | ||||
|  | ||||
|     while (read_idx != write_idx) { | ||||
|       // Process one result at a time directly from ring buffer | ||||
|       BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; | ||||
|       // Calculate how many contiguous results we can process in one batch | ||||
|       // If write > read: process all results from read to write | ||||
|       // If write <= read (wraparound): process from read to end of buffer first | ||||
|       size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx); | ||||
|  | ||||
|       // Process the batch for raw advertisements | ||||
|       if (this->raw_advertisements_) { | ||||
|         for (auto *listener : this->listeners_) { | ||||
|           listener->parse_devices(&scan_result, 1); | ||||
|           listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); | ||||
|         } | ||||
|         for (auto *client : this->clients_) { | ||||
|           client->parse_devices(&scan_result, 1); | ||||
|           client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Process individual results for parsed advertisements | ||||
|       if (this->parse_advertisements_) { | ||||
|         ESPBTDevice device; | ||||
|         device.parse_scan_rst(scan_result); | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
|         for (size_t i = 0; i < batch_size; i++) { | ||||
|           BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i]; | ||||
|           ESPBTDevice device; | ||||
|           device.parse_scan_rst(scan_result); | ||||
|  | ||||
|         bool found = false; | ||||
|         for (auto *listener : this->listeners_) { | ||||
|           if (listener->parse_device(device)) | ||||
|             found = true; | ||||
|         } | ||||
|           bool found = false; | ||||
|           for (auto *listener : this->listeners_) { | ||||
|             if (listener->parse_device(device)) | ||||
|               found = true; | ||||
|           } | ||||
|  | ||||
|         for (auto *client : this->clients_) { | ||||
|           if (client->parse_device(device)) { | ||||
|             found = true; | ||||
|             if (!connecting && client->state() == ClientState::DISCOVERED) { | ||||
|               promote_to_connecting = true; | ||||
|           for (auto *client : this->clients_) { | ||||
|             if (client->parse_device(device)) { | ||||
|               found = true; | ||||
|               if (!connecting && client->state() == ClientState::DISCOVERED) { | ||||
|                 promote_to_connecting = true; | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (!found && !this->scan_continuous_) { | ||||
|           this->print_bt_device_info(device); | ||||
|           if (!found && !this->scan_continuous_) { | ||||
|             this->print_bt_device_info(device); | ||||
|           } | ||||
|         } | ||||
| #endif  // USE_ESP32_BLE_DEVICE | ||||
|       } | ||||
|  | ||||
|       // Move to next entry in ring buffer | ||||
|       read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; | ||||
|       // Update read index for entire batch | ||||
|       read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE; | ||||
|  | ||||
|       // Store with release to ensure reads complete before index update | ||||
|       this->ring_read_index_.store(read_idx, std::memory_order_release); | ||||
| @@ -511,6 +520,7 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) { | ||||
|   this->scanner_state_callbacks_.call(state); | ||||
| } | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
| ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } | ||||
| optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { | ||||
|   if (!data.uuid.contains(0x4C, 0x00)) | ||||
| @@ -751,13 +761,16 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| std::string ESPBTDevice::address_str() const { | ||||
|   char mac[24]; | ||||
|   snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2], | ||||
|            this->address_[3], this->address_[4], this->address_[5]); | ||||
|   return mac; | ||||
| } | ||||
|  | ||||
| uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } | ||||
| #endif  // USE_ESP32_BLE_DEVICE | ||||
|  | ||||
| void ESP32BLETracker::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "BLE Tracker:"); | ||||
| @@ -796,6 +809,7 @@ void ESP32BLETracker::dump_config() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
| void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { | ||||
|   const uint64_t address = device.address_uint64(); | ||||
|   for (auto &disc : this->already_discovered_) { | ||||
| @@ -866,8 +880,9 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { | ||||
|   return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && | ||||
|          ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); | ||||
| } | ||||
| #endif  // USE_ESP32_BLE_DEVICE | ||||
|  | ||||
| }  // namespace esp32_ble_tracker | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
|   | ||||
| @@ -39,6 +39,7 @@ struct ServiceData { | ||||
|   adv_data_t data; | ||||
| }; | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
| class ESPBLEiBeacon { | ||||
|  public: | ||||
|   ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } | ||||
| @@ -116,13 +117,16 @@ class ESPBTDevice { | ||||
|   std::vector<ServiceData> service_datas_{}; | ||||
|   const BLEScanResult *scan_result_{nullptr}; | ||||
| }; | ||||
| #endif  // USE_ESP32_BLE_DEVICE | ||||
|  | ||||
| class ESP32BLETracker; | ||||
|  | ||||
| class ESPBTDeviceListener { | ||||
|  public: | ||||
|   virtual void on_scan_end() {} | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
|   virtual bool parse_device(const ESPBTDevice &device) = 0; | ||||
| #endif | ||||
|   virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; | ||||
|   virtual AdvertisementParserType get_advertisement_parser_type() { | ||||
|     return AdvertisementParserType::PARSED_ADVERTISEMENTS; | ||||
| @@ -237,7 +241,9 @@ class ESP32BLETracker : public Component, | ||||
|   void register_client(ESPBTClient *client); | ||||
|   void recalculate_advertisement_parser_types(); | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_DEVICE | ||||
|   void print_bt_device_info(const ESPBTDevice &device); | ||||
| #endif | ||||
|  | ||||
|   void start_scan(); | ||||
|   void stop_scan(); | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import automation, pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c | ||||
| @@ -8,6 +10,7 @@ from esphome.const import ( | ||||
|     CONF_CONTRAST, | ||||
|     CONF_DATA_PINS, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_I2C, | ||||
|     CONF_I2C_ID, | ||||
|     CONF_ID, | ||||
|     CONF_PIN, | ||||
| @@ -20,6 +23,9 @@ from esphome.const import ( | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.core.entity_helpers import setup_entity | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| DEPENDENCIES = ["esp32"] | ||||
|  | ||||
| @@ -113,6 +119,12 @@ ENUM_SPECIAL_EFFECT = { | ||||
|     "SEPIA": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_SEPIA, | ||||
| } | ||||
|  | ||||
| camera_fb_location_t = cg.global_ns.enum("camera_fb_location_t") | ||||
| ENUM_FB_LOCATION = { | ||||
|     "PSRAM": cg.global_ns.CAMERA_FB_IN_PSRAM, | ||||
|     "DRAM": cg.global_ns.CAMERA_FB_IN_DRAM, | ||||
| } | ||||
|  | ||||
| # pin assignment | ||||
| CONF_HREF_PIN = "href_pin" | ||||
| CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin" | ||||
| @@ -143,6 +155,7 @@ CONF_MAX_FRAMERATE = "max_framerate" | ||||
| CONF_IDLE_FRAMERATE = "idle_framerate" | ||||
| # frame buffer | ||||
| CONF_FRAME_BUFFER_COUNT = "frame_buffer_count" | ||||
| CONF_FRAME_BUFFER_LOCATION = "frame_buffer_location" | ||||
|  | ||||
| # stream trigger | ||||
| CONF_ON_STREAM_START = "on_stream_start" | ||||
| @@ -224,6 +237,9 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 cv.framerate, cv.Range(min=0, max=1) | ||||
|             ), | ||||
|             cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), | ||||
|             cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum( | ||||
|                 ENUM_FB_LOCATION, upper=True | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
| @@ -250,6 +266,22 @@ CONFIG_SCHEMA = cv.All( | ||||
|     cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _final_validate(config): | ||||
|     if CONF_I2C_PINS not in config: | ||||
|         return | ||||
|     fconf = fv.full_config.get() | ||||
|     if fconf.get(CONF_I2C): | ||||
|         raise cv.Invalid( | ||||
|             "The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead" | ||||
|         ) | ||||
|     _LOGGER.warning( | ||||
|         "The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead." | ||||
|     ) | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|  | ||||
| SETTERS = { | ||||
|     # pin assignment | ||||
|     CONF_DATA_PINS: "set_data_pins", | ||||
| @@ -279,6 +311,7 @@ SETTERS = { | ||||
|     CONF_WB_MODE: "set_wb_mode", | ||||
|     # test pattern | ||||
|     CONF_TEST_PATTERN: "set_test_pattern", | ||||
|     CONF_FRAME_BUFFER_LOCATION: "set_frame_buffer_location", | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -306,9 +339,10 @@ async def to_code(config): | ||||
|     else: | ||||
|         cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) | ||||
|     cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT])) | ||||
|     cg.add(var.set_frame_buffer_location(config[CONF_FRAME_BUFFER_LOCATION])) | ||||
|     cg.add(var.set_frame_size(config[CONF_RESOLUTION])) | ||||
|  | ||||
|     cg.add_define("USE_ESP32_CAMERA") | ||||
|     cg.add_define("USE_CAMERA") | ||||
|  | ||||
|     if CORE.using_esp_idf: | ||||
|         add_idf_component(name="espressif/esp32-camera", ref="2.0.15") | ||||
|   | ||||
| @@ -133,6 +133,7 @@ void ESP32Camera::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  JPEG Quality: %u\n" | ||||
|                 "  Framebuffer Count: %u\n" | ||||
|                 "  Framebuffer Location: %s\n" | ||||
|                 "  Contrast: %d\n" | ||||
|                 "  Brightness: %d\n" | ||||
|                 "  Saturation: %d\n" | ||||
| @@ -140,8 +141,9 @@ void ESP32Camera::dump_config() { | ||||
|                 "  Horizontal Mirror: %s\n" | ||||
|                 "  Special Effect: %u\n" | ||||
|                 "  White Balance Mode: %u", | ||||
|                 st.quality, conf.fb_count, st.contrast, st.brightness, st.saturation, ONOFF(st.vflip), | ||||
|                 ONOFF(st.hmirror), st.special_effect, st.wb_mode); | ||||
|                 st.quality, conf.fb_count, this->config_.fb_location == CAMERA_FB_IN_PSRAM ? "PSRAM" : "DRAM", | ||||
|                 st.contrast, st.brightness, st.saturation, ONOFF(st.vflip), ONOFF(st.hmirror), st.special_effect, | ||||
|                 st.wb_mode); | ||||
|   // ESP_LOGCONFIG(TAG, "  Auto White Balance: %u", st.awb); | ||||
|   // ESP_LOGCONFIG(TAG, "  Auto White Balance Gain: %u", st.awb_gain); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
| @@ -350,6 +352,9 @@ void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) { | ||||
|   this->config_.fb_count = fb_count; | ||||
|   this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY); | ||||
| } | ||||
| void ESP32Camera::set_frame_buffer_location(camera_fb_location_t fb_location) { | ||||
|   this->config_.fb_location = fb_location; | ||||
| } | ||||
|  | ||||
| /* ---------------- public API (specific) ---------------- */ | ||||
| void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<camera::CameraImage>)> &&callback) { | ||||
|   | ||||
| @@ -152,6 +152,7 @@ class ESP32Camera : public camera::Camera { | ||||
|   /* -- frame buffer */ | ||||
|   void set_frame_buffer_mode(camera_grab_mode_t mode); | ||||
|   void set_frame_buffer_count(uint8_t fb_count); | ||||
|   void set_frame_buffer_location(camera_fb_location_t fb_location); | ||||
|  | ||||
|   /* public API (derivated) */ | ||||
|   void setup() override; | ||||
|   | ||||
| @@ -109,6 +109,7 @@ void ESP32TouchComponent::loop() { | ||||
|  | ||||
|       // Only publish if state changed - this filters out repeated events | ||||
|       if (new_state != child->last_state_) { | ||||
|         child->initial_state_published_ = true; | ||||
|         child->last_state_ = new_state; | ||||
|         child->publish_state(new_state); | ||||
|         // Original ESP32: ISR only fires when touched, release is detected by timeout | ||||
| @@ -175,6 +176,9 @@ void ESP32TouchComponent::on_shutdown() { | ||||
| void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { | ||||
|   ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg); | ||||
|  | ||||
|   uint32_t mask = 0; | ||||
|   touch_ll_read_trigger_status_mask(&mask); | ||||
|   touch_ll_clear_trigger_status_mask(); | ||||
|   touch_pad_clear_status(); | ||||
|  | ||||
|   // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured | ||||
| @@ -184,6 +188,11 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { | ||||
|   // as any pad remains touched. This allows us to detect both new touches and | ||||
|   // continued touches, but releases must be detected by timeout in the main loop. | ||||
|  | ||||
|   // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! | ||||
|   // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE | ||||
|   // Therefore: touched = (value < threshold) | ||||
|   // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) | ||||
|  | ||||
|   // Process all configured pads to check their current state | ||||
|   // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, | ||||
|   // so we must scan all configured pads to find which ones were touched | ||||
| @@ -201,19 +210,12 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { | ||||
|       value = touch_ll_read_raw_data(pad); | ||||
|     } | ||||
|  | ||||
|     // Skip pads with 0 value - they haven't been measured in this cycle | ||||
|     // This is important: not all pads are measured every interrupt cycle, | ||||
|     // only those that the hardware has updated | ||||
|     if (value == 0) { | ||||
|     // Skip pads that aren’t in the trigger mask | ||||
|     bool is_touched = (mask >> pad) & 1; | ||||
|     if (!is_touched) { | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! | ||||
|     // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE | ||||
|     // Therefore: touched = (value < threshold) | ||||
|     // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) | ||||
|     bool is_touched = value < child->get_threshold(); | ||||
|  | ||||
|     // Always send the current state - the main loop will filter for changes | ||||
|     // We send both touched and untouched states because the ISR doesn't | ||||
|     // track previous state (to keep ISR fast and simple) | ||||
|   | ||||
| @@ -15,6 +15,7 @@ from esphome.const import ( | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PLATFORM_ESP8266, | ||||
|     CoreModel, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.helpers import copy_file_if_changed | ||||
| @@ -180,12 +181,14 @@ async def to_code(config): | ||||
|     cg.add(esp8266_ns.setup_preferences()) | ||||
|  | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|     cg.add_platformio_option("lib_compat_mode", "strict") | ||||
|  | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     cg.add_build_flag("-DUSE_ESP8266") | ||||
|     cg.set_cpp_standard("gnu++20") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_define("ESPHOME_VARIANT", "ESP8266") | ||||
|     cg.add_define(CoreModel.SINGLE) | ||||
|  | ||||
|     cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,10 @@ void Mutex::unlock() {} | ||||
| IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } | ||||
| IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } | ||||
|  | ||||
| // ESP8266 doesn't support lwIP core locking, so this is a no-op | ||||
| LwIPLock::LwIPLock() {} | ||||
| LwIPLock::~LwIPLock() {} | ||||
|  | ||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||
|   wifi_get_macaddr(STATION_IF, mac); | ||||
| } | ||||
|   | ||||
| @@ -20,14 +20,16 @@ adjusted_ids = set() | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.ensure_list( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(EspLdo), | ||||
|             cv.Required(CONF_VOLTAGE): cv.All( | ||||
|                 cv.voltage, cv.float_range(min=0.5, max=2.7) | ||||
|             ), | ||||
|             cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), | ||||
|             cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, | ||||
|         } | ||||
|         cv.COMPONENT_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.GenerateID(): cv.declare_id(EspLdo), | ||||
|                 cv.Required(CONF_VOLTAGE): cv.All( | ||||
|                     cv.voltage, cv.float_range(min=0.5, max=2.7) | ||||
|                 ), | ||||
|                 cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), | ||||
|                 cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, | ||||
|             } | ||||
|         ) | ||||
|     ), | ||||
|     cv.only_with_esp_idf, | ||||
|     only_on_variant(supported=[VARIANT_ESP32P4]), | ||||
|   | ||||
| @@ -17,6 +17,9 @@ class EspLdo : public Component { | ||||
|   void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } | ||||
|   void set_voltage(float voltage) { this->voltage_ = voltage; } | ||||
|   void adjust_voltage(float voltage); | ||||
|   float get_setup_priority() const override { | ||||
|     return setup_priority::BUS;  // LDO setup should be done early | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   int channel_; | ||||
|   | ||||
| @@ -342,5 +342,11 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add_define("USE_ETHERNET") | ||||
|  | ||||
|     # Disable WiFi when using Ethernet to save memory | ||||
|     if CORE.using_esp_idf: | ||||
|         add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) | ||||
|         # Also disable WiFi/BT coexistence since WiFi is disabled | ||||
|         add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) | ||||
|  | ||||
|     if CORE.using_arduino: | ||||
|         cg.add_library("WiFi", None) | ||||
|   | ||||
| @@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() { | ||||
| } | ||||
|  | ||||
| network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { | ||||
|   LwIPLock lock; | ||||
|   const ip_addr_t *dns_ip = dns_getserver(num); | ||||
|   return dns_ip; | ||||
| } | ||||
| @@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() { | ||||
|   ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); | ||||
|  | ||||
|   if (this->manual_ip_.has_value()) { | ||||
|     LwIPLock lock; | ||||
|     if (this->manual_ip_->dns1.is_set()) { | ||||
|       ip_addr_t d; | ||||
|       d = this->manual_ip_->dns1; | ||||
| @@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen | ||||
| void EthernetComponent::dump_connect_params_() { | ||||
|   esp_netif_ip_info_t ip; | ||||
|   esp_netif_get_ip_info(this->eth_netif_, &ip); | ||||
|   const ip_addr_t *dns_ip1 = dns_getserver(0); | ||||
|   const ip_addr_t *dns_ip2 = dns_getserver(1); | ||||
|   const ip_addr_t *dns_ip1; | ||||
|   const ip_addr_t *dns_ip2; | ||||
|   { | ||||
|     LwIPLock lock; | ||||
|     dns_ip1 = dns_getserver(0); | ||||
|     dns_ip2 = dns_getserver(1); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  IP Address: %s\n" | ||||
|   | ||||
| @@ -29,7 +29,6 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS | ||||
|   } | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::ETHERNET; } | ||||
|   std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; } | ||||
|   void dump_config() override; | ||||
|   void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } | ||||
|  | ||||
| @@ -52,7 +51,6 @@ class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::Text | ||||
|     } | ||||
|   } | ||||
|   float get_setup_priority() const override { return setup_priority::ETHERNET; } | ||||
|   std::string unique_id() override { return get_mac_address() + "-ethernetinfo-dns"; } | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
| @@ -63,7 +61,6 @@ class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor | ||||
|  public: | ||||
|   void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); } | ||||
|   float get_setup_priority() const override { return setup_priority::ETHERNET; } | ||||
|   std::string unique_id() override { return get_mac_address() + "-ethernetinfo-mac"; } | ||||
|   void dump_config() override; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() { | ||||
|   return {}; | ||||
| } | ||||
| void Fan::save_state_() { | ||||
|   if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   FanRestoreState state{}; | ||||
|   state.state = this->state; | ||||
|   state.oscillating = this->oscillating; | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/gl_r01_i2c/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/gl_r01_i2c/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										68
									
								
								esphome/components/gl_r01_i2c/gl_r01_i2c.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								esphome/components/gl_r01_i2c/gl_r01_i2c.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "gl_r01_i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gl_r01_i2c { | ||||
|  | ||||
| static const char *const TAG = "gl_r01_i2c"; | ||||
|  | ||||
| // Register definitions from datasheet | ||||
| static const uint8_t REG_VERSION = 0x00; | ||||
| static const uint8_t REG_DISTANCE = 0x02; | ||||
| static const uint8_t REG_TRIGGER = 0x10; | ||||
| static const uint8_t CMD_TRIGGER = 0xB0; | ||||
| static const uint8_t RESTART_CMD1 = 0x5A; | ||||
| static const uint8_t RESTART_CMD2 = 0xA5; | ||||
| static const uint8_t READ_DELAY = 40;  // minimum milliseconds from datasheet to safely read measurement result | ||||
|  | ||||
| void GLR01I2CComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C..."); | ||||
|   // Verify sensor presence | ||||
|   if (!this->read_byte_16(REG_VERSION, &this->version_)) { | ||||
|     ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_); | ||||
| } | ||||
|  | ||||
| void GLR01I2CComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "GL-R01 I2C:"); | ||||
|   ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_SENSOR(" ", "Distance", this); | ||||
| } | ||||
|  | ||||
| void GLR01I2CComponent::update() { | ||||
|   // Trigger a new measurement | ||||
|   if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) { | ||||
|     ESP_LOGE(TAG, "Failed to trigger measurement!"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Schedule reading the result after the read delay | ||||
|   this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); }); | ||||
| } | ||||
|  | ||||
| void GLR01I2CComponent::read_distance_() { | ||||
|   uint16_t distance = 0; | ||||
|   if (!this->read_byte_16(REG_DISTANCE, &distance)) { | ||||
|     ESP_LOGE(TAG, "Failed to read distance value!"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (distance == 0xFFFF) { | ||||
|     ESP_LOGW(TAG, "Invalid measurement received!"); | ||||
|     this->status_set_warning(); | ||||
|   } else { | ||||
|     ESP_LOGV(TAG, "Distance: %umm", distance); | ||||
|     this->publish_state(distance); | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace gl_r01_i2c | ||||
| }  // namespace esphome | ||||
							
								
								
									
										22
									
								
								esphome/components/gl_r01_i2c/gl_r01_i2c.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								esphome/components/gl_r01_i2c/gl_r01_i2c.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gl_r01_i2c { | ||||
|  | ||||
| class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|  | ||||
|  protected: | ||||
|   void read_distance_(); | ||||
|   uint16_t version_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace gl_r01_i2c | ||||
| }  // namespace esphome | ||||
							
								
								
									
										36
									
								
								esphome/components/gl_r01_i2c/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/gl_r01_i2c/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_MILLIMETER, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@pkejval"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c") | ||||
| GLR01I2CComponent = gl_r01_i2c_ns.class_( | ||||
|     "GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         GLR01I2CComponent, | ||||
|         unit_of_measurement=UNIT_MILLIMETER, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_DISTANCE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x74)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await sensor.register_sensor(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
| @@ -1,11 +1,22 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import binary_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_PIN | ||||
| from esphome.const import ( | ||||
|     CONF_ALLOW_OTHER_USES, | ||||
|     CONF_ID, | ||||
|     CONF_NAME, | ||||
|     CONF_NUMBER, | ||||
|     CONF_PIN, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
| from .. import gpio_ns | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| GPIOBinarySensor = gpio_ns.class_( | ||||
|     "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||
| ) | ||||
| @@ -24,7 +35,21 @@ CONFIG_SCHEMA = ( | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_PIN): pins.gpio_input_pin_schema, | ||||
|             cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, | ||||
|             # Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms | ||||
|             # due to hardware limitations or lack of reliable interrupt support. This ensures | ||||
|             # stable operation on these platforms. Future maintainers should verify platform | ||||
|             # capabilities before changing this default behavior. | ||||
|             cv.SplitDefault( | ||||
|                 CONF_USE_INTERRUPT, | ||||
|                 bk72xx=False, | ||||
|                 esp32=True, | ||||
|                 esp8266=True, | ||||
|                 host=True, | ||||
|                 ln882x=False, | ||||
|                 nrf52=True, | ||||
|                 rp2040=True, | ||||
|                 rtl87xx=False, | ||||
|             ): cv.boolean, | ||||
|             cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( | ||||
|                 INTERRUPT_TYPES, upper=True | ||||
|             ), | ||||
| @@ -41,6 +66,34 @@ async def to_code(config): | ||||
|     pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|     cg.add(var.set_pin(pin)) | ||||
|  | ||||
|     cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) | ||||
|     if config[CONF_USE_INTERRUPT]: | ||||
|     # Check for ESP8266 GPIO16 interrupt limitation | ||||
|     # GPIO16 on ESP8266 is a special pin that doesn't support interrupts through | ||||
|     # the Arduino attachInterrupt() function. This is the only known GPIO pin | ||||
|     # across all supported platforms that has this limitation, so we handle it | ||||
|     # here instead of in the platform-specific code. | ||||
|     use_interrupt = config[CONF_USE_INTERRUPT] | ||||
|     if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16: | ||||
|         _LOGGER.warning( | ||||
|             "GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. " | ||||
|             "Falling back to polling mode (same as in ESPHome <2025.7). " | ||||
|             "The sensor will work exactly as before, but other pins have better " | ||||
|             "performance with interrupts.", | ||||
|             config.get(CONF_NAME, config[CONF_ID]), | ||||
|         ) | ||||
|         use_interrupt = False | ||||
|  | ||||
|     # Check if pin is shared with other components (allow_other_uses) | ||||
|     # When a pin is shared, interrupts can interfere with other components | ||||
|     # (e.g., duty_cycle sensor) that need to monitor the pin's state changes | ||||
|     if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): | ||||
|         _LOGGER.info( | ||||
|             "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. " | ||||
|             "The sensor will use polling mode for compatibility with other pin uses.", | ||||
|             config.get(CONF_NAME, config[CONF_ID]), | ||||
|             config[CONF_PIN][CONF_NUMBER], | ||||
|         ) | ||||
|         use_interrupt = False | ||||
|  | ||||
|     cg.add(var.set_use_interrupt(use_interrupt)) | ||||
|     if use_interrupt: | ||||
|         cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) | ||||
|   | ||||
| @@ -7,6 +7,7 @@ from esphome.const import ( | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PLATFORM_HOST, | ||||
|     CoreModel, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
| @@ -43,5 +44,7 @@ async def to_code(config): | ||||
|     cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) | ||||
|     cg.add_build_flag("-std=gnu++20") | ||||
|     cg.add_define("ESPHOME_BOARD", "host") | ||||
|     cg.add_define(CoreModel.MULTI_ATOMICS) | ||||
|     cg.add_platformio_option("platform", "platformio/native") | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|     cg.add_platformio_option("lib_compat_mode", "strict") | ||||
|   | ||||
| @@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) { | ||||
|     container.reset();  // Release ownership of the container's shared_ptr | ||||
|  | ||||
|     valid = json::parse_json(response, [this_update](JsonObject root) -> bool { | ||||
|       if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { | ||||
|       if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) { | ||||
|         ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||
|         return false; | ||||
|       } | ||||
| @@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) { | ||||
|       this_update->update_info_.latest_version = root["version"].as<std::string>(); | ||||
|  | ||||
|       for (auto build : root["builds"].as<JsonArray>()) { | ||||
|         if (!build.containsKey("chipFamily")) { | ||||
|         if (!build["chipFamily"].is<const char *>()) { | ||||
|           ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||
|           return false; | ||||
|         } | ||||
|         if (build["chipFamily"] == ESPHOME_VARIANT) { | ||||
|           if (!build.containsKey("ota")) { | ||||
|           if (!build["ota"].is<JsonObject>()) { | ||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||
|             return false; | ||||
|           } | ||||
|           auto ota = build["ota"]; | ||||
|           if (!ota.containsKey("path") || !ota.containsKey("md5")) { | ||||
|           JsonObject ota = build["ota"].as<JsonObject>(); | ||||
|           if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) { | ||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||
|             return false; | ||||
|           } | ||||
|           this_update->update_info_.firmware_url = ota["path"].as<std::string>(); | ||||
|           this_update->update_info_.md5 = ota["md5"].as<std::string>(); | ||||
|  | ||||
|           if (ota.containsKey("summary")) | ||||
|           if (ota["summary"].is<const char *>()) | ||||
|             this_update->update_info_.summary = ota["summary"].as<std::string>(); | ||||
|           if (ota.containsKey("release_url")) | ||||
|           if (ota["release_url"].is<const char *>()) | ||||
|             this_update->update_info_.release_url = ota["release_url"].as<std::string>(); | ||||
|  | ||||
|           return true; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <driver/gpio.h> | ||||
|  | ||||
| #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
| #define SOC_HP_I2C_NUM SOC_I2C_NUM | ||||
| @@ -20,21 +21,72 @@ static const char *const TAG = "i2c.idf"; | ||||
| void IDFI2CBus::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   static i2c_port_t next_port = I2C_NUM_0; | ||||
|   port_ = next_port; | ||||
|   this->port_ = next_port; | ||||
|   if (this->port_ == I2C_NUM_MAX) { | ||||
|     ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->timeout_ > 13000) { | ||||
|     ESP_LOGW(TAG, "Using max allowed timeout: 13 ms"); | ||||
|     this->timeout_ = 13000; | ||||
|   } | ||||
|  | ||||
|   this->recover_(); | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) | ||||
|   next_port = (i2c_port_t) (next_port + 1); | ||||
|  | ||||
|   i2c_master_bus_config_t bus_conf{}; | ||||
|   memset(&bus_conf, 0, sizeof(bus_conf)); | ||||
|   bus_conf.sda_io_num = gpio_num_t(sda_pin_); | ||||
|   bus_conf.scl_io_num = gpio_num_t(scl_pin_); | ||||
|   bus_conf.i2c_port = this->port_; | ||||
|   bus_conf.glitch_ignore_cnt = 7; | ||||
| #if SOC_LP_I2C_SUPPORTED | ||||
|   if (this->port_ < SOC_HP_I2C_NUM) { | ||||
|     bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; | ||||
|   } else { | ||||
|     bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT; | ||||
|   } | ||||
| #else | ||||
|   bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; | ||||
| #endif | ||||
|   bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_; | ||||
|   esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   i2c_device_config_t dev_conf{}; | ||||
|   memset(&dev_conf, 0, sizeof(dev_conf)); | ||||
|   dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7; | ||||
|   dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED; | ||||
|   dev_conf.scl_speed_hz = this->frequency_; | ||||
|   dev_conf.scl_wait_us = this->timeout_; | ||||
|   err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->initialized_ = true; | ||||
|  | ||||
|   if (this->scan_) { | ||||
|     ESP_LOGV(TAG, "Scanning for devices"); | ||||
|     this->i2c_scan_(); | ||||
|   } | ||||
| #else | ||||
| #if SOC_HP_I2C_NUM > 1 | ||||
|   next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; | ||||
| #else | ||||
|   next_port = I2C_NUM_MAX; | ||||
| #endif | ||||
|  | ||||
|   if (port_ == I2C_NUM_MAX) { | ||||
|     ESP_LOGE(TAG, "No more than %u buses supported", SOC_HP_I2C_NUM); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   recover_(); | ||||
|  | ||||
|   i2c_config_t conf{}; | ||||
|   memset(&conf, 0, sizeof(conf)); | ||||
|   conf.mode = I2C_MODE_MASTER; | ||||
| @@ -53,11 +105,7 @@ void IDFI2CBus::setup() { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (timeout_ > 0) {  // if timeout specified in yaml: | ||||
|     if (timeout_ > 13000) { | ||||
|       ESP_LOGW(TAG, "i2c timeout of %" PRIu32 "us greater than max of 13ms on esp-idf, setting to max", timeout_); | ||||
|       timeout_ = 13000; | ||||
|     } | ||||
|   if (timeout_ > 0) { | ||||
|     err = i2c_set_timeout(port_, timeout_ * 80);  // unit: APB 80MHz clock cycle | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err)); | ||||
| @@ -73,12 +121,15 @@ void IDFI2CBus::setup() { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   initialized_ = true; | ||||
|   if (this->scan_) { | ||||
|     ESP_LOGV(TAG, "Scanning bus for active devices"); | ||||
|     this->i2c_scan_(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void IDFI2CBus::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "I2C Bus:"); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
| @@ -123,6 +174,74 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { | ||||
|     ESP_LOGVV(TAG, "i2c bus not initialized!"); | ||||
|     return ERROR_NOT_INITIALIZED; | ||||
|   } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) | ||||
|   i2c_operation_job_t jobs[cnt + 4]; | ||||
|   uint8_t read = (address << 1) | I2C_MASTER_READ; | ||||
|   size_t last = 0, num = 0; | ||||
|  | ||||
|   jobs[num].command = I2C_MASTER_CMD_START; | ||||
|   num++; | ||||
|  | ||||
|   jobs[num].command = I2C_MASTER_CMD_WRITE; | ||||
|   jobs[num].write.ack_check = true; | ||||
|   jobs[num].write.data = &read; | ||||
|   jobs[num].write.total_bytes = 1; | ||||
|   num++; | ||||
|  | ||||
|   // find the last valid index | ||||
|   for (size_t i = 0; i < cnt; i++) { | ||||
|     const auto &buf = buffers[i]; | ||||
|     if (buf.len == 0) { | ||||
|       continue; | ||||
|     } | ||||
|     last = i; | ||||
|   } | ||||
|  | ||||
|   for (size_t i = 0; i < cnt; i++) { | ||||
|     const auto &buf = buffers[i]; | ||||
|     if (buf.len == 0) { | ||||
|       continue; | ||||
|     } | ||||
|     if (i == last) { | ||||
|       // the last byte read before stop should always be a nack, | ||||
|       // split the last read if len is larger than 1 | ||||
|       if (buf.len > 1) { | ||||
|         jobs[num].command = I2C_MASTER_CMD_READ; | ||||
|         jobs[num].read.ack_value = I2C_ACK_VAL; | ||||
|         jobs[num].read.data = (uint8_t *) buf.data; | ||||
|         jobs[num].read.total_bytes = buf.len - 1; | ||||
|         num++; | ||||
|       } | ||||
|       jobs[num].command = I2C_MASTER_CMD_READ; | ||||
|       jobs[num].read.ack_value = I2C_NACK_VAL; | ||||
|       jobs[num].read.data = (uint8_t *) buf.data + buf.len - 1; | ||||
|       jobs[num].read.total_bytes = 1; | ||||
|       num++; | ||||
|     } else { | ||||
|       jobs[num].command = I2C_MASTER_CMD_READ; | ||||
|       jobs[num].read.ack_value = I2C_ACK_VAL; | ||||
|       jobs[num].read.data = (uint8_t *) buf.data; | ||||
|       jobs[num].read.total_bytes = buf.len; | ||||
|       num++; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   jobs[num].command = I2C_MASTER_CMD_STOP; | ||||
|   num++; | ||||
|  | ||||
|   esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); | ||||
|   if (err == ESP_ERR_INVALID_STATE) { | ||||
|     ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); | ||||
|     return ERROR_NOT_ACKNOWLEDGED; | ||||
|   } else if (err == ESP_ERR_TIMEOUT) { | ||||
|     ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); | ||||
|     return ERROR_TIMEOUT; | ||||
|   } else if (err != ESP_OK) { | ||||
|     ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); | ||||
|     return ERROR_UNKNOWN; | ||||
|   } | ||||
| #else | ||||
|   i2c_cmd_handle_t cmd = i2c_cmd_link_create(); | ||||
|   esp_err_t err = i2c_master_start(cmd); | ||||
|   if (err != ESP_OK) { | ||||
| @@ -168,6 +287,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { | ||||
|     ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); | ||||
|     return ERROR_UNKNOWN; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE | ||||
|   char debug_buf[4]; | ||||
| @@ -185,6 +305,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { | ||||
|  | ||||
|   return ERROR_OK; | ||||
| } | ||||
|  | ||||
| ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { | ||||
|   // logging is only enabled with vv level, if warnings are shown the caller | ||||
|   // should log them | ||||
| @@ -207,6 +328,49 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b | ||||
|   ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); | ||||
| #endif | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) | ||||
|   i2c_operation_job_t jobs[cnt + 3]; | ||||
|   uint8_t write = (address << 1) | I2C_MASTER_WRITE; | ||||
|   size_t num = 0; | ||||
|  | ||||
|   jobs[num].command = I2C_MASTER_CMD_START; | ||||
|   num++; | ||||
|  | ||||
|   jobs[num].command = I2C_MASTER_CMD_WRITE; | ||||
|   jobs[num].write.ack_check = true; | ||||
|   jobs[num].write.data = &write; | ||||
|   jobs[num].write.total_bytes = 1; | ||||
|   num++; | ||||
|  | ||||
|   for (size_t i = 0; i < cnt; i++) { | ||||
|     const auto &buf = buffers[i]; | ||||
|     if (buf.len == 0) { | ||||
|       continue; | ||||
|     } | ||||
|     jobs[num].command = I2C_MASTER_CMD_WRITE; | ||||
|     jobs[num].write.ack_check = true; | ||||
|     jobs[num].write.data = (uint8_t *) buf.data; | ||||
|     jobs[num].write.total_bytes = buf.len; | ||||
|     num++; | ||||
|   } | ||||
|  | ||||
|   if (stop) { | ||||
|     jobs[num].command = I2C_MASTER_CMD_STOP; | ||||
|     num++; | ||||
|   } | ||||
|  | ||||
|   esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); | ||||
|   if (err == ESP_ERR_INVALID_STATE) { | ||||
|     ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); | ||||
|     return ERROR_NOT_ACKNOWLEDGED; | ||||
|   } else if (err == ESP_ERR_TIMEOUT) { | ||||
|     ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); | ||||
|     return ERROR_TIMEOUT; | ||||
|   } else if (err != ESP_OK) { | ||||
|     ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); | ||||
|     return ERROR_UNKNOWN; | ||||
|   } | ||||
| #else | ||||
|   i2c_cmd_handle_t cmd = i2c_cmd_link_create(); | ||||
|   esp_err_t err = i2c_master_start(cmd); | ||||
|   if (err != ESP_OK) { | ||||
| @@ -252,6 +416,7 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b | ||||
|     ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); | ||||
|     return ERROR_UNKNOWN; | ||||
|   } | ||||
| #endif | ||||
|   return ERROR_OK; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,9 +2,14 @@ | ||||
|  | ||||
| #ifdef USE_ESP_IDF | ||||
|  | ||||
| #include <driver/i2c.h> | ||||
| #include "esphome/core/component.h" | ||||
| #include "i2c_bus.h" | ||||
| #include "esp_idf_version.h" | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) | ||||
| #include <driver/i2c_master.h> | ||||
| #else | ||||
| #include <driver/i2c.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace i2c { | ||||
| @@ -38,6 +43,10 @@ class IDFI2CBus : public InternalI2CBus, public Component { | ||||
|   RecoveryCode recovery_result_; | ||||
|  | ||||
|  protected: | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) | ||||
|   i2c_master_dev_handle_t dev_; | ||||
|   i2c_master_bus_handle_t bus_; | ||||
| #endif | ||||
|   i2c_port_t port_; | ||||
|   uint8_t sda_pin_; | ||||
|   bool sda_pullup_enabled_; | ||||
|   | ||||
| @@ -36,8 +36,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | ||||
|  | ||||
| #ifdef USE_I2S_LEGACY | ||||
| #if SOC_I2S_SUPPORTS_ADC | ||||
|   void set_adc_channel(adc1_channel_t channel) { | ||||
|     this->adc_channel_ = channel; | ||||
|   void set_adc_channel(adc_channel_t channel) { | ||||
|     this->adc_channel_ = (adc1_channel_t) channel; | ||||
|     this->adc_ = true; | ||||
|   } | ||||
| #endif | ||||
|   | ||||
| @@ -180,7 +180,7 @@ async def to_code(config): | ||||
|     await speaker.register_speaker(var, config) | ||||
|  | ||||
|     if config[CONF_DAC_TYPE] == "internal": | ||||
|         cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) | ||||
|         cg.add(var.set_internal_dac_mode(config[CONF_MODE])) | ||||
|     else: | ||||
|         cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) | ||||
|         if use_legacy(): | ||||
|   | ||||
| @@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
| @coroutine_with_priority(1.0) | ||||
| async def to_code(config): | ||||
|     cg.add_library("bblanchon/ArduinoJson", "6.18.5") | ||||
|     cg.add_library("bblanchon/ArduinoJson", "7.4.2") | ||||
|     cg.add_define("USE_JSON") | ||||
|     cg.add_global(json_ns.using) | ||||
|   | ||||
| @@ -1,83 +1,76 @@ | ||||
| #include "json_util.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| // ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h | ||||
|  | ||||
| namespace esphome { | ||||
| namespace json { | ||||
|  | ||||
| static const char *const TAG = "json"; | ||||
|  | ||||
| static std::vector<char> global_json_build_buffer;  // NOLINT | ||||
| static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL); | ||||
| // Build an allocator for the JSON Library using the RAMAllocator class | ||||
| struct SpiRamAllocator : ArduinoJson::Allocator { | ||||
|   void *allocate(size_t size) override { return this->allocator_.allocate(size); } | ||||
|  | ||||
|   void deallocate(void *pointer) override { | ||||
|     // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. | ||||
|     // RAMAllocator::deallocate() requires the size, which we don't have access to here. | ||||
|     // RAMAllocator::deallocate implementation just calls free() regardless of whether | ||||
|     // the memory was allocated with heap_caps_malloc or malloc. | ||||
|     // This is safe because ESP-IDF's heap implementation internally tracks the memory region | ||||
|     // and routes free() to the appropriate heap. | ||||
|     free(pointer);  // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) | ||||
|   } | ||||
|  | ||||
|   void *reallocate(void *ptr, size_t new_size) override { | ||||
|     return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)}; | ||||
| }; | ||||
|  | ||||
| std::string build_json(const json_build_t &f) { | ||||
|   // Here we are allocating up to 5kb of memory, | ||||
|   // with the heap size minus 2kb to be safe if less than 5kb | ||||
|   // as we can not have a true dynamic sized document. | ||||
|   // The excess memory is freed below with `shrinkToFit()` | ||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); | ||||
|   size_t request_size = std::min(free_heap, (size_t) 512); | ||||
|   while (true) { | ||||
|     ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); | ||||
|     DynamicJsonDocument json_document(request_size); | ||||
|     if (json_document.capacity() == 0) { | ||||
|       ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes", | ||||
|                request_size, free_heap); | ||||
|       return "{}"; | ||||
|     } | ||||
|     JsonObject root = json_document.to<JsonObject>(); | ||||
|     f(root); | ||||
|     if (json_document.overflowed()) { | ||||
|       if (request_size == free_heap) { | ||||
|         ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes", | ||||
|                  free_heap); | ||||
|         return "{}"; | ||||
|       } | ||||
|       request_size = std::min(request_size * 2, free_heap); | ||||
|       continue; | ||||
|     } | ||||
|     json_document.shrinkToFit(); | ||||
|     ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); | ||||
|     std::string output; | ||||
|     serializeJson(json_document, output); | ||||
|     return output; | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   auto doc_allocator = SpiRamAllocator(); | ||||
|   JsonDocument json_document(&doc_allocator); | ||||
|   if (json_document.overflowed()) { | ||||
|     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||
|     return "{}"; | ||||
|   } | ||||
|   JsonObject root = json_document.to<JsonObject>(); | ||||
|   f(root); | ||||
|   if (json_document.overflowed()) { | ||||
|     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||
|     return "{}"; | ||||
|   } | ||||
|   std::string output; | ||||
|   serializeJson(json_document, output); | ||||
|   return output; | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
|  | ||||
| bool parse_json(const std::string &data, const json_parse_t &f) { | ||||
|   // Here we are allocating 1.5 times the data size, | ||||
|   // with the heap size minus 2kb to be safe if less than that | ||||
|   // as we can not have a true dynamic sized document. | ||||
|   // The excess memory is freed below with `shrinkToFit()` | ||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); | ||||
|   size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); | ||||
|   while (true) { | ||||
|     DynamicJsonDocument json_document(request_size); | ||||
|     if (json_document.capacity() == 0) { | ||||
|       ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size, | ||||
|                free_heap); | ||||
|       return false; | ||||
|     } | ||||
|     DeserializationError err = deserializeJson(json_document, data); | ||||
|     json_document.shrinkToFit(); | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   auto doc_allocator = SpiRamAllocator(); | ||||
|   JsonDocument json_document(&doc_allocator); | ||||
|   if (json_document.overflowed()) { | ||||
|     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||
|     return false; | ||||
|   } | ||||
|   DeserializationError err = deserializeJson(json_document, data); | ||||
|  | ||||
|     JsonObject root = json_document.as<JsonObject>(); | ||||
|   JsonObject root = json_document.as<JsonObject>(); | ||||
|  | ||||
|     if (err == DeserializationError::Ok) { | ||||
|       return f(root); | ||||
|     } else if (err == DeserializationError::NoMemory) { | ||||
|       if (request_size * 2 >= free_heap) { | ||||
|         ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); | ||||
|         return false; | ||||
|       } | ||||
|       ESP_LOGV(TAG, "Increasing memory allocation."); | ||||
|       request_size *= 2; | ||||
|       continue; | ||||
|     } else { | ||||
|       ESP_LOGE(TAG, "Parse error: %s", err.c_str()); | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
|   if (err == DeserializationError::Ok) { | ||||
|     return f(root); | ||||
|   } else if (err == DeserializationError::NoMemory) { | ||||
|     ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGE(TAG, "Parse error: %s", err.c_str()); | ||||
|   return false; | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
|  | ||||
| }  // namespace json | ||||
|   | ||||
| @@ -178,13 +178,8 @@ static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; | ||||
|  | ||||
| static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } | ||||
|  | ||||
| static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { | ||||
|   for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { | ||||
|     if (header_footer[i] != buffer[i]) { | ||||
|       return false;  // Mismatch in header/footer | ||||
|     } | ||||
|   } | ||||
|   return true;  // Valid header/footer | ||||
| static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { | ||||
|   return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; | ||||
| } | ||||
|  | ||||
| void LD2410Component::dump_config() { | ||||
| @@ -300,14 +295,12 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu | ||||
|   if (command_value != nullptr) { | ||||
|     len += command_value_len; | ||||
|   } | ||||
|   uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; | ||||
|   // 2 length bytes (low, high) + 2 command bytes (low, high) | ||||
|   uint8_t len_cmd[] = {len, 0x00, command, 0x00}; | ||||
|   this->write_array(len_cmd, sizeof(len_cmd)); | ||||
|  | ||||
|   // command value bytes | ||||
|   if (command_value != nullptr) { | ||||
|     for (uint8_t i = 0; i < command_value_len; i++) { | ||||
|       this->write_byte(command_value[i]); | ||||
|     } | ||||
|     this->write_array(command_value, command_value_len); | ||||
|   } | ||||
|   // frame footer bytes | ||||
|   this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); | ||||
| @@ -401,7 +394,7 @@ void LD2410Component::handle_periodic_data_() { | ||||
|     /* | ||||
|       Moving distance range: 18th byte | ||||
|       Still distance range: 19th byte | ||||
|       Moving enery: 20~28th bytes | ||||
|       Moving energy: 20~28th bytes | ||||
|     */ | ||||
|     for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { | ||||
|       sensor::Sensor *s = this->gate_move_sensors_[i]; | ||||
| @@ -480,7 +473,7 @@ bool LD2410Component::handle_ack_data_() { | ||||
|     ESP_LOGE(TAG, "Invalid status"); | ||||
|     return true; | ||||
|   } | ||||
|   if (ld2410::two_byte_to_int(this->buffer_data_[8], this->buffer_data_[9]) != 0x00) { | ||||
|   if (this->buffer_data_[8] || this->buffer_data_[9]) { | ||||
|     ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); | ||||
|     return true; | ||||
|   } | ||||
| @@ -534,8 +527,8 @@ bool LD2410Component::handle_ack_data_() { | ||||
|       const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); | ||||
|       const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); | ||||
|       ESP_LOGV(TAG, | ||||
|                "Light function is: %s\n" | ||||
|                "Light threshold is: %u\n" | ||||
|                "Light function: %s\n" | ||||
|                "Light threshold: %u\n" | ||||
|                "Out pin level: %s", | ||||
|                light_function_str, this->light_threshold_, out_pin_level_str); | ||||
| #ifdef USE_SELECT | ||||
| @@ -600,7 +593,7 @@ bool LD2410Component::handle_ack_data_() { | ||||
|       break; | ||||
|  | ||||
|     case CMD_QUERY: {  // Query parameters response | ||||
|       if (this->buffer_data_[10] != 0xAA) | ||||
|       if (this->buffer_data_[10] != HEADER) | ||||
|         return true;  // value head=0xAA | ||||
| #ifdef USE_NUMBER | ||||
|       /* | ||||
| @@ -656,17 +649,11 @@ void LD2410Component::readline_(int readch) { | ||||
|   if (this->buffer_pos_ < 4) { | ||||
|     return;  // Not enough data to process yet | ||||
|   } | ||||
|   if (this->buffer_data_[this->buffer_pos_ - 4] == DATA_FRAME_FOOTER[0] && | ||||
|       this->buffer_data_[this->buffer_pos_ - 3] == DATA_FRAME_FOOTER[1] && | ||||
|       this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[2] && | ||||
|       this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[3]) { | ||||
|   if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { | ||||
|     ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); | ||||
|     this->handle_periodic_data_(); | ||||
|     this->buffer_pos_ = 0;  // Reset position index for next message | ||||
|   } else if (this->buffer_data_[this->buffer_pos_ - 4] == CMD_FRAME_FOOTER[0] && | ||||
|              this->buffer_data_[this->buffer_pos_ - 3] == CMD_FRAME_FOOTER[1] && | ||||
|              this->buffer_data_[this->buffer_pos_ - 2] == CMD_FRAME_FOOTER[2] && | ||||
|              this->buffer_data_[this->buffer_pos_ - 1] == CMD_FRAME_FOOTER[3]) { | ||||
|   } else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { | ||||
|     ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); | ||||
|     if (this->handle_ack_data_()) { | ||||
|       this->buffer_pos_ = 0;  // Reset position index for next message | ||||
| @@ -772,7 +759,6 @@ void LD2410Component::set_max_distances_timeout() { | ||||
|                        0x00}; | ||||
|   this->set_config_mode_(true); | ||||
|   this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value)); | ||||
|   delay(50);  // NOLINT | ||||
|   this->query_parameters_(); | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
|   this->set_config_mode_(false); | ||||
| @@ -802,7 +788,6 @@ void LD2410Component::set_gate_threshold(uint8_t gate) { | ||||
|                        0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, | ||||
|                        0x02, 0x00, lowbyte(still),  highbyte(still),  0x00, 0x00}; | ||||
|   this->send_command_(CMD_GATE_SENS, value, sizeof(value)); | ||||
|   delay(50);  // NOLINT | ||||
|   this->query_parameters_(); | ||||
|   this->set_config_mode_(false); | ||||
| } | ||||
| @@ -833,7 +818,6 @@ void LD2410Component::set_light_out_control() { | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t value[4] = {this->light_function_, this->light_threshold_, this->out_pin_level_, 0x00}; | ||||
|   this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); | ||||
|   delay(50);  // NOLINT | ||||
|   this->query_light_control_(); | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
|   this->set_config_mode_(false); | ||||
|   | ||||
| @@ -5,10 +5,10 @@ | ||||
| namespace esphome { | ||||
| namespace ld2420 { | ||||
|  | ||||
| static const char *const TAG = "LD2420.binary_sensor"; | ||||
| static const char *const TAG = "ld2420.binary_sensor"; | ||||
|  | ||||
| void LD2420BinarySensor::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:"); | ||||
|   ESP_LOGCONFIG(TAG, "Binary Sensor:"); | ||||
|   LOG_BINARY_SENSOR("  ", "Presence", this->presence_bsensor_); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| static const char *const TAG = "LD2420.button"; | ||||
| static const char *const TAG = "ld2420.button"; | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2420 { | ||||
|   | ||||
| @@ -137,7 +137,7 @@ static const std::string OP_SIMPLE_MODE_STRING = "Simple"; | ||||
| // Memory-efficient lookup tables | ||||
| struct StringToUint8 { | ||||
|   const char *str; | ||||
|   uint8_t value; | ||||
|   const uint8_t value; | ||||
| }; | ||||
|  | ||||
| static constexpr StringToUint8 OP_MODE_BY_STR[] = { | ||||
| @@ -155,8 +155,9 @@ static constexpr const char *ERR_MESSAGE[] = { | ||||
| // Helper function for lookups | ||||
| template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { | ||||
|   for (const auto &entry : arr) { | ||||
|     if (str == entry.str) | ||||
|     if (str == entry.str) { | ||||
|       return entry.value; | ||||
|     } | ||||
|   } | ||||
|   return 0xFF;  // Not found | ||||
| } | ||||
| @@ -326,15 +327,8 @@ void LD2420Component::revert_config_action() { | ||||
|  | ||||
| void LD2420Component::loop() { | ||||
|   // If there is a active send command do not process it here, the send command call will handle it. | ||||
|   if (!this->get_cmd_active_()) { | ||||
|     if (!this->available()) | ||||
|       return; | ||||
|     static uint8_t buffer[2048]; | ||||
|     static uint8_t rx_data; | ||||
|     while (this->available()) { | ||||
|       rx_data = this->read(); | ||||
|       this->readline_(rx_data, buffer, sizeof(buffer)); | ||||
|     } | ||||
|   while (!this->cmd_active_ && this->available()) { | ||||
|     this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -365,8 +359,9 @@ void LD2420Component::auto_calibrate_sensitivity() { | ||||
|  | ||||
|     // Store average and peak values | ||||
|     this->gate_avg[gate] = sum / CALIBRATE_SAMPLES; | ||||
|     if (this->gate_peak[gate] < peak) | ||||
|     if (this->gate_peak[gate] < peak) { | ||||
|       this->gate_peak[gate] = peak; | ||||
|     } | ||||
|  | ||||
|     uint32_t calculated_value = | ||||
|         (static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate]))); | ||||
| @@ -403,8 +398,9 @@ void LD2420Component::set_operating_mode(const std::string &state) { | ||||
|       } | ||||
|     } else { | ||||
|       // Set the current data back so we don't have new data that can be applied in error. | ||||
|       if (this->get_calibration_()) | ||||
|       if (this->get_calibration_()) { | ||||
|         memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); | ||||
|       } | ||||
|       this->set_calibration_(false); | ||||
|     } | ||||
|   } else { | ||||
| @@ -414,30 +410,32 @@ void LD2420Component::set_operating_mode(const std::string &state) { | ||||
| } | ||||
|  | ||||
| void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { | ||||
|   static int pos = 0; | ||||
|  | ||||
|   if (rx_data >= 0) { | ||||
|     if (pos < len - 1) { | ||||
|       buffer[pos++] = rx_data; | ||||
|       buffer[pos] = 0; | ||||
|     } else { | ||||
|       pos = 0; | ||||
|     } | ||||
|     if (pos >= 4) { | ||||
|       if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { | ||||
|         this->set_cmd_active_(false);  // Set command state to inactive after responce. | ||||
|         this->handle_ack_data_(buffer, pos); | ||||
|         pos = 0; | ||||
|       } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && | ||||
|                  (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { | ||||
|         this->handle_simple_mode_(buffer, pos); | ||||
|         pos = 0; | ||||
|       } else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && | ||||
|                  (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { | ||||
|         this->handle_energy_mode_(buffer, pos); | ||||
|         pos = 0; | ||||
|       } | ||||
|     } | ||||
|   if (rx_data < 0) { | ||||
|     return;  // No data available | ||||
|   } | ||||
|   if (this->buffer_pos_ < len - 1) { | ||||
|     buffer[this->buffer_pos_++] = rx_data; | ||||
|     buffer[this->buffer_pos_] = 0; | ||||
|   } else { | ||||
|     // We should never get here, but just in case... | ||||
|     ESP_LOGW(TAG, "Max command length exceeded; ignoring"); | ||||
|     this->buffer_pos_ = 0; | ||||
|   } | ||||
|   if (this->buffer_pos_ < 4) { | ||||
|     return;  // Not enough data to process yet | ||||
|   } | ||||
|   if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { | ||||
|     this->cmd_active_ = false;  // Set command state to inactive after response | ||||
|     this->handle_ack_data_(buffer, this->buffer_pos_); | ||||
|     this->buffer_pos_ = 0; | ||||
|   } else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) && | ||||
|              (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { | ||||
|     this->handle_simple_mode_(buffer, this->buffer_pos_); | ||||
|     this->buffer_pos_ = 0; | ||||
|   } else if ((memcmp(&buffer[this->buffer_pos_ - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && | ||||
|              (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { | ||||
|     this->handle_energy_mode_(buffer, this->buffer_pos_); | ||||
|     this->buffer_pos_ = 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -462,8 +460,9 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { | ||||
|  | ||||
|   // Resonable refresh rate for home assistant database size health | ||||
|   const int32_t current_millis = App.get_loop_component_start_time(); | ||||
|   if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) | ||||
|   if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) { | ||||
|     return; | ||||
|   } | ||||
|   this->last_periodic_millis = current_millis; | ||||
|   for (auto &listener : this->listeners_) { | ||||
|     listener->on_distance(this->get_distance_()); | ||||
| @@ -506,14 +505,16 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { | ||||
|     } | ||||
|   } | ||||
|   outbuf[index] = '\0'; | ||||
|   if (index > 1) | ||||
|   if (index > 1) { | ||||
|     this->set_distance_(strtol(outbuf, &endptr, 10)); | ||||
|   } | ||||
|  | ||||
|   if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { | ||||
|     // Resonable refresh rate for home assistant database size health | ||||
|     const int32_t current_millis = App.get_loop_component_start_time(); | ||||
|     if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) | ||||
|     if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) { | ||||
|       return; | ||||
|     } | ||||
|     this->last_normal_periodic_millis = current_millis; | ||||
|     for (auto &listener : this->listeners_) | ||||
|       listener->on_distance(this->get_distance_()); | ||||
| @@ -593,11 +594,12 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
| int LD2420Component::send_cmd_from_array(CmdFrameT frame) { | ||||
|   uint32_t start_millis = millis(); | ||||
|   uint8_t error = 0; | ||||
|   uint8_t ack_buffer[64]; | ||||
|   uint8_t cmd_buffer[64]; | ||||
|   uint8_t ack_buffer[MAX_LINE_LENGTH]; | ||||
|   uint8_t cmd_buffer[MAX_LINE_LENGTH]; | ||||
|   this->cmd_reply_.ack = false; | ||||
|   if (frame.command != CMD_RESTART) | ||||
|     this->set_cmd_active_(true);  // Restart does not reply, thus no ack state required. | ||||
|   if (frame.command != CMD_RESTART) { | ||||
|     this->cmd_active_ = true; | ||||
|   }  // Restart does not reply, thus no ack state required | ||||
|   uint8_t retry = 3; | ||||
|   while (retry) { | ||||
|     frame.length = 0; | ||||
| @@ -619,9 +621,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { | ||||
|  | ||||
|     memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer)); | ||||
|     frame.length += sizeof(frame.footer); | ||||
|     for (uint16_t index = 0; index < frame.length; index++) { | ||||
|       this->write_byte(cmd_buffer[index]); | ||||
|     } | ||||
|     this->write_array(cmd_buffer, frame.length); | ||||
|  | ||||
|     error = 0; | ||||
|     if (frame.command == CMD_RESTART) { | ||||
| @@ -630,7 +630,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { | ||||
|  | ||||
|     while (!this->cmd_reply_.ack) { | ||||
|       while (this->available()) { | ||||
|         this->readline_(read(), ack_buffer, sizeof(ack_buffer)); | ||||
|         this->readline_(this->read(), ack_buffer, sizeof(ack_buffer)); | ||||
|       } | ||||
|       delay_microseconds_safe(1450); | ||||
|       // Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT. | ||||
| @@ -641,10 +641,12 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     if (this->cmd_reply_.ack) | ||||
|     if (this->cmd_reply_.ack) { | ||||
|       retry = 0; | ||||
|     if (this->cmd_reply_.error > 0) | ||||
|     } | ||||
|     if (this->cmd_reply_.error > 0) { | ||||
|       this->handle_cmd_error(error); | ||||
|     } | ||||
|   } | ||||
|   return error; | ||||
| } | ||||
| @@ -764,8 +766,9 @@ void LD2420Component::set_system_mode(uint16_t mode) { | ||||
|   cmd_frame.data_length += sizeof(unknown_parm); | ||||
|   cmd_frame.footer = CMD_FRAME_FOOTER; | ||||
|   ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); | ||||
|   if (this->send_cmd_from_array(cmd_frame) == 0) | ||||
|   if (this->send_cmd_from_array(cmd_frame) == 0) { | ||||
|     this->set_mode_(mode); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LD2420Component::get_firmware_version_() { | ||||
| @@ -840,18 +843,24 @@ void LD2420Component::set_gate_threshold(uint8_t gate) { | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
| void LD2420Component::init_gate_config_numbers() { | ||||
|   if (this->gate_timeout_number_ != nullptr) | ||||
|   if (this->gate_timeout_number_ != nullptr) { | ||||
|     this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout)); | ||||
|   if (this->gate_select_number_ != nullptr) | ||||
|   } | ||||
|   if (this->gate_select_number_ != nullptr) { | ||||
|     this->gate_select_number_->publish_state(0); | ||||
|   if (this->min_gate_distance_number_ != nullptr) | ||||
|   } | ||||
|   if (this->min_gate_distance_number_ != nullptr) { | ||||
|     this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate)); | ||||
|   if (this->max_gate_distance_number_ != nullptr) | ||||
|   } | ||||
|   if (this->max_gate_distance_number_ != nullptr) { | ||||
|     this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate)); | ||||
|   if (this->gate_move_sensitivity_factor_number_ != nullptr) | ||||
|   } | ||||
|   if (this->gate_move_sensitivity_factor_number_ != nullptr) { | ||||
|     this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); | ||||
|   if (this->gate_still_sensitivity_factor_number_ != nullptr) | ||||
|   } | ||||
|   if (this->gate_still_sensitivity_factor_number_ != nullptr) { | ||||
|     this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); | ||||
|   } | ||||
|   for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { | ||||
|     if (this->gate_still_threshold_numbers_[gate] != nullptr) { | ||||
|       this->gate_still_threshold_numbers_[gate]->publish_state( | ||||
|   | ||||
| @@ -20,8 +20,9 @@ | ||||
| namespace esphome { | ||||
| namespace ld2420 { | ||||
|  | ||||
| static const uint8_t TOTAL_GATES = 16; | ||||
| static const uint8_t CALIBRATE_SAMPLES = 64; | ||||
| static const uint8_t MAX_LINE_LENGTH = 46;  // Max characters for serial buffer | ||||
| static const uint8_t TOTAL_GATES = 16; | ||||
|  | ||||
| enum OpMode : uint8_t { | ||||
|   OP_NORMAL_MODE = 1, | ||||
| @@ -118,10 +119,10 @@ class LD2420Component : public Component, public uart::UARTDevice { | ||||
|  | ||||
|   float gate_move_sensitivity_factor{0.5}; | ||||
|   float gate_still_sensitivity_factor{0.5}; | ||||
|   int32_t last_periodic_millis = millis(); | ||||
|   int32_t report_periodic_millis = millis(); | ||||
|   int32_t monitor_periodic_millis = millis(); | ||||
|   int32_t last_normal_periodic_millis = millis(); | ||||
|   int32_t last_periodic_millis{0}; | ||||
|   int32_t report_periodic_millis{0}; | ||||
|   int32_t monitor_periodic_millis{0}; | ||||
|   int32_t last_normal_periodic_millis{0}; | ||||
|   uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]; | ||||
|   uint16_t gate_avg[TOTAL_GATES]; | ||||
|   uint16_t gate_peak[TOTAL_GATES]; | ||||
| @@ -161,8 +162,6 @@ class LD2420Component : public Component, public uart::UARTDevice { | ||||
|   void set_presence_(bool presence) { this->presence_ = presence; }; | ||||
|   uint16_t get_distance_() { return this->distance_; }; | ||||
|   void set_distance_(uint16_t distance) { this->distance_ = distance; }; | ||||
|   bool get_cmd_active_() { return this->cmd_active_; }; | ||||
|   void set_cmd_active_(bool active) { this->cmd_active_ = active; }; | ||||
|   void handle_simple_mode_(const uint8_t *inbuf, int len); | ||||
|   void handle_energy_mode_(uint8_t *buffer, int len); | ||||
|   void handle_ack_data_(uint8_t *buffer, int len); | ||||
| @@ -181,12 +180,11 @@ class LD2420Component : public Component, public uart::UARTDevice { | ||||
|   std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16); | ||||
| #endif | ||||
|  | ||||
|   uint32_t max_distance_gate_; | ||||
|   uint32_t min_distance_gate_; | ||||
|   uint16_t distance_{0}; | ||||
|   uint16_t system_mode_; | ||||
|   uint16_t gate_energy_[TOTAL_GATES]; | ||||
|   uint16_t distance_{0}; | ||||
|   uint8_t config_checksum_{0}; | ||||
|   uint8_t buffer_pos_{0};  // where to resume processing/populating buffer | ||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||
|   char firmware_ver_[8]{"v0.0.0"}; | ||||
|   bool cmd_active_{false}; | ||||
|   bool presence_{false}; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| static const char *const TAG = "LD2420.number"; | ||||
| static const char *const TAG = "ld2420.number"; | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2420 { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| namespace esphome { | ||||
| namespace ld2420 { | ||||
|  | ||||
| static const char *const TAG = "LD2420.select"; | ||||
| static const char *const TAG = "ld2420.select"; | ||||
|  | ||||
| void LD2420Select::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   | ||||
| @@ -5,10 +5,10 @@ | ||||
| namespace esphome { | ||||
| namespace ld2420 { | ||||
|  | ||||
| static const char *const TAG = "LD2420.sensor"; | ||||
| static const char *const TAG = "ld2420.sensor"; | ||||
|  | ||||
| void LD2420Sensor::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "LD2420 Sensor:"); | ||||
|   ESP_LOGCONFIG(TAG, "Sensor:"); | ||||
|   LOG_SENSOR("  ", "Distance", this->distance_sensor_); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,10 +5,10 @@ | ||||
| namespace esphome { | ||||
| namespace ld2420 { | ||||
|  | ||||
| static const char *const TAG = "LD2420.text_sensor"; | ||||
| static const char *const TAG = "ld2420.text_sensor"; | ||||
|  | ||||
| void LD2420TextSensor::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "LD2420 TextSensor:"); | ||||
|   ESP_LOGCONFIG(TAG, "Text Sensor:"); | ||||
|   LOG_TEXT_SENSOR("  ", "Firmware", this->fw_version_text_sensor_); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,6 +20,7 @@ from esphome.const import ( | ||||
|     KEY_FRAMEWORK_VERSION, | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     CoreModel, | ||||
|     __version__, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| @@ -260,6 +261,7 @@ async def component_to_code(config): | ||||
|     cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) | ||||
|     cg.add_define(CoreModel.MULTI_NO_ATOMICS) | ||||
|  | ||||
|     # force using arduino framework | ||||
|     cg.add_platformio_option("framework", "arduino") | ||||
| @@ -268,6 +270,7 @@ async def component_to_code(config): | ||||
|  | ||||
|     # disable library compatibility checks | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|     cg.add_platformio_option("lib_compat_mode", "soft") | ||||
|     # include <Arduino.h> in every file | ||||
|     cg.add_platformio_option("build_src_flags", "-include Arduino.h") | ||||
|     # dummy version code | ||||
|   | ||||
| @@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } | ||||
| IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } | ||||
| IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } | ||||
|  | ||||
| // LibreTiny doesn't support lwIP core locking, so this is a no-op | ||||
| LwIPLock::LwIPLock() {} | ||||
| LwIPLock::~LwIPLock() {} | ||||
|  | ||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||
|   WiFi.macAddress(mac); | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ namespace light { | ||||
| // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema | ||||
|  | ||||
| void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | ||||
|   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   if (state.supports_effects()) | ||||
|     root["effect"] = state.get_effect_name(); | ||||
|  | ||||
| @@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | ||||
|   if (values.get_color_mode() & ColorCapability::BRIGHTNESS) | ||||
|     root["brightness"] = uint8_t(values.get_brightness() * 255); | ||||
|  | ||||
|   JsonObject color = root.createNestedObject("color"); | ||||
|   JsonObject color = root["color"].to<JsonObject>(); | ||||
|   if (values.get_color_mode() & ColorCapability::RGB) { | ||||
|     color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); | ||||
|     color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); | ||||
| @@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | ||||
| } | ||||
|  | ||||
| void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { | ||||
|   if (root.containsKey("state")) { | ||||
|   if (root["state"].is<const char *>()) { | ||||
|     auto val = parse_on_off(root["state"]); | ||||
|     switch (val) { | ||||
|       case PARSE_ON: | ||||
| @@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("brightness")) { | ||||
|   if (root["brightness"].is<uint8_t>()) { | ||||
|     call.set_brightness(float(root["brightness"]) / 255.0f); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("color")) { | ||||
|   if (root["color"].is<JsonObject>()) { | ||||
|     JsonObject color = root["color"]; | ||||
|     // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. | ||||
|     float max_rgb = 0.0f; | ||||
|     if (color.containsKey("r")) { | ||||
|     if (color["r"].is<uint8_t>()) { | ||||
|       float r = float(color["r"]) / 255.0f; | ||||
|       max_rgb = fmaxf(max_rgb, r); | ||||
|       call.set_red(r); | ||||
|     } | ||||
|     if (color.containsKey("g")) { | ||||
|     if (color["g"].is<uint8_t>()) { | ||||
|       float g = float(color["g"]) / 255.0f; | ||||
|       max_rgb = fmaxf(max_rgb, g); | ||||
|       call.set_green(g); | ||||
|     } | ||||
|     if (color.containsKey("b")) { | ||||
|     if (color["b"].is<uint8_t>()) { | ||||
|       float b = float(color["b"]) / 255.0f; | ||||
|       max_rgb = fmaxf(max_rgb, b); | ||||
|       call.set_blue(b); | ||||
|     } | ||||
|     if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { | ||||
|     if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) { | ||||
|       call.set_color_brightness(max_rgb); | ||||
|     } | ||||
|  | ||||
|     if (color.containsKey("c")) { | ||||
|     if (color["c"].is<uint8_t>()) { | ||||
|       call.set_cold_white(float(color["c"]) / 255.0f); | ||||
|     } | ||||
|     if (color.containsKey("w")) { | ||||
|     if (color["w"].is<uint8_t>()) { | ||||
|       // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm | ||||
|       // white channel in RGBWW. | ||||
|       if (color.containsKey("c")) { | ||||
|       if (color["c"].is<uint8_t>()) { | ||||
|         call.set_warm_white(float(color["w"]) / 255.0f); | ||||
|       } else { | ||||
|         call.set_white(float(color["w"]) / 255.0f); | ||||
| @@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("white_value")) {  // legacy API | ||||
|   if (root["white_value"].is<uint8_t>()) {  // legacy API | ||||
|     call.set_white(float(root["white_value"]) / 255.0f); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("color_temp")) { | ||||
|   if (root["color_temp"].is<uint16_t>()) { | ||||
|     call.set_color_temperature(float(root["color_temp"])); | ||||
|   } | ||||
| } | ||||
| @@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | ||||
| void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { | ||||
|   LightJSONSchema::parse_color_json(state, call, root); | ||||
|  | ||||
|   if (root.containsKey("flash")) { | ||||
|   if (root["flash"].is<uint32_t>()) { | ||||
|     auto length = uint32_t(float(root["flash"]) * 1000); | ||||
|     call.set_flash_length(length); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("transition")) { | ||||
|   if (root["transition"].is<uint16_t>()) { | ||||
|     auto length = uint32_t(float(root["transition"]) * 1000); | ||||
|     call.set_transition_length(length); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("effect")) { | ||||
|   if (root["effect"].is<const char *>()) { | ||||
|     const char *effect = root["effect"]; | ||||
|     call.set_effect(effect); | ||||
|   } | ||||
|   | ||||
| @@ -21,6 +21,11 @@ from esphome.components.libretiny.const import ( | ||||
|     COMPONENT_LN882X, | ||||
|     COMPONENT_RTL87XX, | ||||
| ) | ||||
| from esphome.components.zephyr import ( | ||||
|     zephyr_add_cdc_acm, | ||||
|     zephyr_add_overlay, | ||||
|     zephyr_add_prj_conf, | ||||
| ) | ||||
| from esphome.config_helpers import filter_source_files_from_platform | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -41,6 +46,7 @@ from esphome.const import ( | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_LN882X, | ||||
|     PLATFORM_NRF52, | ||||
|     PLATFORM_RP2040, | ||||
|     PLATFORM_RTL87XX, | ||||
|     PlatformFramework, | ||||
| @@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] | ||||
|  | ||||
| UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] | ||||
|  | ||||
| UART_SELECTION_NRF52 = [USB_CDC, UART0] | ||||
|  | ||||
| HARDWARE_UART_TO_UART_SELECTION = { | ||||
|     UART0: logger_ns.UART_SELECTION_UART0, | ||||
|     UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, | ||||
| @@ -167,6 +175,8 @@ def uart_selection(value): | ||||
|             return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) | ||||
|     if CORE.is_host: | ||||
|         raise cv.Invalid("Uart selection not valid for host platform") | ||||
|     if CORE.is_nrf52: | ||||
|         return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value) | ||||
|     raise NotImplementedError | ||||
|  | ||||
|  | ||||
| @@ -183,9 +193,10 @@ def validate_local_no_higher_than_global(value): | ||||
| Logger = logger_ns.class_("Logger", cg.Component) | ||||
| LoggerMessageTrigger = logger_ns.class_( | ||||
|     "LoggerMessageTrigger", | ||||
|     automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), | ||||
|     automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr), | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
| @@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 bk72xx=DEFAULT, | ||||
|                 ln882x=DEFAULT, | ||||
|                 rtl87xx=DEFAULT, | ||||
|                 nrf52=USB_CDC, | ||||
|             ): cv.All( | ||||
|                 cv.only_on( | ||||
|                     [ | ||||
| @@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                         PLATFORM_BK72XX, | ||||
|                         PLATFORM_LN882X, | ||||
|                         PLATFORM_RTL87XX, | ||||
|                         PLATFORM_NRF52, | ||||
|                     ] | ||||
|                 ), | ||||
|                 uart_selection, | ||||
| @@ -358,6 +371,15 @@ async def to_code(config): | ||||
|     except cv.Invalid: | ||||
|         pass | ||||
|  | ||||
|     if CORE.using_zephyr: | ||||
|         if config[CONF_HARDWARE_UART] == UART0: | ||||
|             zephyr_add_overlay("""&uart0 { status = "okay";};""") | ||||
|         if config[CONF_HARDWARE_UART] == UART1: | ||||
|             zephyr_add_overlay("""&uart1 { status = "okay";};""") | ||||
|         if config[CONF_HARDWARE_UART] == USB_CDC: | ||||
|             zephyr_add_prj_conf("UART_LINE_CTRL", True) | ||||
|             zephyr_add_cdc_acm(config, 0) | ||||
|  | ||||
|     # Register at end for safe mode | ||||
|     await cg.register_component(log, config) | ||||
|  | ||||
| @@ -368,7 +390,7 @@ async def to_code(config): | ||||
|         await automation.build_automation( | ||||
|             trigger, | ||||
|             [ | ||||
|                 (cg.int_, "level"), | ||||
|                 (cg.uint8, "level"), | ||||
|                 (cg.const_char_ptr, "tag"), | ||||
|                 (cg.const_char_ptr, "message"), | ||||
|             ], | ||||
| @@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||
|             PlatformFramework.RTL87XX_ARDUINO, | ||||
|             PlatformFramework.LN882X_ARDUINO, | ||||
|         }, | ||||
|         "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, | ||||
|         "task_log_buffer.cpp": { | ||||
|             PlatformFramework.ESP32_ARDUINO, | ||||
|             PlatformFramework.ESP32_IDF, | ||||
|   | ||||
| @@ -4,9 +4,9 @@ | ||||
| #include <memory>  // For unique_ptr | ||||
| #endif | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace logger { | ||||
| @@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate | ||||
|   this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];  // NOLINT | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
|   this->main_task_ = xTaskGetCurrentTaskHandle(); | ||||
| #elif defined(USE_ZEPHYR) | ||||
|   this->main_task_ = k_current_get(); | ||||
| #endif | ||||
| } | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
| @@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifndef USE_ZEPHYR | ||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | ||||
| void Logger::loop() { | ||||
| #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | ||||
| @@ -185,8 +188,13 @@ void Logger::loop() { | ||||
|     } | ||||
|     opened = !opened; | ||||
|   } | ||||
| #endif | ||||
|   this->process_messages_(); | ||||
| } | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| void Logger::process_messages_() { | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   // Process any buffered messages when available | ||||
|   if (this->log_buffer_->has_messages()) { | ||||
| @@ -227,12 +235,11 @@ void Logger::loop() { | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } | ||||
| void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
| UARTSelection Logger::get_uart() const { return this->uart_; } | ||||
| #endif | ||||
|  | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user