diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 134b00ce66c..b0a45e19b8e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -199,11 +199,8 @@ PSRAM_XIP_VARIANTS = { # neither, so the engine is already in the image and the wrap saves nothing. ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6} -# Variants whose ROM exports sscanf under the newlib format ESPHome links -# against: the ESP32-C6 normal-format ROM (esp32c6.rom.newlib-normal.ld) and -# the ESP32-C2 nano-format ROM, nano being the IDF default on the C2. There -# the sscanf wrap saves nothing; see sscanf_stubs.cpp. The classic ESP32 ROM -# only exports it under nano format, which ESPHome does not use. +# Variants whose ROM exports sscanf with the newlib format in use (C6 normal +# format, C2 nano format by IDF default), so the sscanf wrap saves nothing. ROM_SSCANF_VARIANTS = {VARIANT_ESP32C2, VARIANT_ESP32C6} # NVS encryption (HMAC peripheral scheme) is only available on variants that @@ -2327,12 +2324,10 @@ async def _set_libc_picolibc_newlib_compat() -> None: def _newlib_wraps_apply() -> bool: - """Whether the printf, vasprintf and sscanf linker wraps belong in this build. + """Whether the printf, vasprintf and sscanf wraps belong in this build. - They need an ESP-IDF framework build (their stubs compile out on Arduino) - on newlib: IDF 5.x defaults to newlib on every variant, IDF 6.0+ switches - to picolibc on every variant, and the wraps are only measured against - newlib. + ESP-IDF framework (the stubs compile out on Arduino) on newlib, which + IDF 6.0+ replaces with picolibc. """ return not CORE.using_arduino and idf_version() < cv.Version(6, 0, 0) @@ -2340,9 +2335,8 @@ def _newlib_wraps_apply() -> bool: def _add_wrap_stub(symbol: str, define: str) -> None: """Emit a linker wrap for ``symbol`` served by a stub compiled under ``define``. - The --undefined flag is needed because libsrc.a is scanned before the IDF - libraries that reference the symbol, so the stub would otherwise never be - pulled from the archive. + --undefined is needed because libsrc.a is scanned before the IDF library + that references the symbol. """ cg.add_define(define) cg.add_build_flag(f"-Wl,--wrap={symbol}") @@ -2353,11 +2347,8 @@ def _add_wrap_stub(symbol: str, define: str) -> None: async def _add_sscanf_stub(enable_full_scanf: bool | None) -> None: """Wrap sscanf when bluedroid is the only reason newlib's scanf engine links. - Runs at FINAL priority so every request_bluetooth() call has happened; - see sscanf_stubs.cpp for what the stub covers. A lambda that scans a float - keeps the libc sscanf unless enable_full_scanf is set explicitly, since the - stub has no floating-point conversions and the wrap applies to the whole - image. + FINAL priority so every request_bluetooth() call has happened. A lambda + that scans a float keeps the libc sscanf unless enable_full_scanf is set. """ if ( enable_full_scanf diff --git a/esphome/components/esp32/sscanf_stubs.cpp b/esphome/components/esp32/sscanf_stubs.cpp index a22b254944e..da2c12b23d2 100644 --- a/esphome/components/esp32/sscanf_stubs.cpp +++ b/esphome/components/esp32/sscanf_stubs.cpp @@ -1,23 +1,13 @@ /* - * Linker wrap stub for sscanf on ESP-IDF, newlib only. + * Linker wrap for sscanf on ESP-IDF with newlib. * - * Nothing in ESPHome calls sscanf, but bluedroid does in two places: - * btc_config_get_bin decodes stored bonding keys with "%02x" and - * string_to_bdaddr parses "%02x:%02x:%02x:%02x:%02x:%02x". Those two - * calls are the only reference to newlib's scanf engine (__ssvfscanf_r, - * _strtod_l and their helpers, ~13 KB) in a Bluetooth build on variants - * whose ROM does not export sscanf. - * - * This stub routes sscanf through the scanner in core/sscanf_no_float.h, - * so any other caller in the image (user lambdas included) keeps working. - * Floating-point conversions are the one gap: supporting them would pull - * _strtod_l straight back, so a format using %f and friends aborts with a - * message pointing at enable_full_scanf: true. Codegen already keeps the - * libc sscanf when a lambda scans a float. - * - * Only compiled in when codegen defines USE_ESP32_SSCANF_STUB, which is - * gated on a Bluetooth component being present, on the variant's ROM and - * on the same newlib condition as printf_stubs.cpp. + * bluedroid's two "%02x" parses (btc_config_get_bin, string_to_bdaddr) are + * the only sscanf callers in an ESPHome image and link newlib's whole scanf + * engine (~13 KB with _strtod_l). Routing them through core/sscanf_no_float.h + * drops it; floats would pull _strtod_l back, so those abort with a message + * naming enable_full_scanf. Codegen skips the wrap when a lambda scans a float + * and only defines USE_ESP32_SSCANF_STUB for Bluetooth builds on variants + * whose ROM lacks sscanf. */ #include "esphome/core/defines.h" diff --git a/esphome/core/sscanf_no_float.h b/esphome/core/sscanf_no_float.h index 044aaf8f6f1..210372f1eab 100644 --- a/esphome/core/sscanf_no_float.h +++ b/esphome/core/sscanf_no_float.h @@ -1,18 +1,9 @@ #pragma once -/* - * A small sscanf without floating-point conversions. - * - * Backs the ESP-IDF linker wrap in components/esp32/sscanf_stubs.cpp, where - * bluedroid's "%02x" parses would otherwise link newlib's whole scanf engine - * (~13 KB with _strtod_l). Integer, character, string and scanset conversions - * behave like libc; a conversion this scanner does not implement (floats, - * %p, wide characters) returns SSCANF_UNSUPPORTED so the caller can fail - * loudly instead of misparsing. - * - * Header only so the host unit tests can exercise it without compiling a - * target platform's sources. - */ +// sscanf without floating-point conversions, backing the linker wrap in +// components/esp32/sscanf_stubs.cpp. Integer, character, string and scanset +// conversions behave like libc; anything else returns SSCANF_UNSUPPORTED. +// Header only so the host unit tests can cover it. #include #include @@ -40,9 +31,7 @@ enum class SscanfLength : uint8_t { inline bool is_space(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); } -// The bit pattern is what the signed conversions want as well. All object -// pointers share one representation, so the caller fetches the argument -// once as void * rather than once per pointee type. +// The unsigned bit pattern is what the signed conversions want as well inline void store_int(void *dest, SscanfLength length, uint64_t value) { switch (length) { case SscanfLength::SSCANF_LENGTH_HH: @@ -80,8 +69,7 @@ struct Scanset { } }; -// Parses a "[set]" directive starting just after the '[' and returns the -// position after the closing ']'. +// Parses "[set]" from just after the '[' and returns the position after ']' inline const char *parse_scanset(const char *f, Scanset &set) { if (*f == '^') { set.negate = true; @@ -103,8 +91,7 @@ inline const char *parse_scanset(const char *f, Scanset &set) { return *f == ']' ? f + 1 : f; } -// Radix of an integer conversion: 0 means "detect from the prefix" (%i), -// 0xFF marks a conversion this scanner does not implement. +// Radix of an integer conversion: 0 detects from the prefix (%i), 0xFF is unsupported inline unsigned int_base(char conv) { switch (conv) { case 'd': @@ -261,8 +248,7 @@ inline int vsscanf_no_float(const char *str, const char *fmt, va_list ap) { // Scanset set; if (conv == '[') f = parse_scanset(f + 1, set) - 1; // the loop's f++ steps past the ']' - // %c reads exactly width characters (default 1); like newlib, a short - // read still assigns whatever was available. + // %c reads width characters (default 1); a short read still assigns, like newlib const size_t limit = conv == 'c' && width == SIZE_MAX ? 1 : width; size_t n = 0; while (n < limit && in[n] != '\0' && (conv == 'c' || (conv == '[' ? set.contains(in[n]) : !is_space(in[n])))) {