Compare commits

...

10 Commits

Author SHA1 Message Date
J. Nick Koston
6de59fa246 Merge branch 'dev' into esp32_ard_compile_time 2026-02-03 03:12:32 +01:00
J. Nick Koston
ccf5c1f7e9 [esp32] Exclude additional unused IDF components (driver, dac, mcpwm, twai, openthread, ulp) (#13664) 2026-02-03 03:12:12 +01:00
dependabot[bot]
efecea9450 Bump github/codeql-action from 4.32.0 to 4.32.1 (#13726)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 02:27:34 +01:00
J. Nick Koston
26e4cda610 [logger] Use vsnprintf_P directly for ESP8266 flash format strings (#13716) 2026-02-03 02:25:54 +01:00
J. Nick Koston
dfcf611a67 Merge upstream/dev into esp32_ard_compile_time
Resolved conflict in ethernet/__init__.py:
- Kept on_connect/on_disconnect automation code from upstream
- Removed Arduino WiFi library addition (handled by selective compilation now)
2026-02-02 17:01:42 +01:00
J. Nick Koston
f4be547d41 Merge branch 'dev' into esp32_ard_compile_time 2026-01-29 17:31:13 -10:00
J. Nick Koston
6e2f7a196f Merge branch 'secondary_jtag' into esp32_ard_compile_time 2026-01-29 14:48:31 -10:00
J. Nick Koston
e2182b6227 Update esphome/components/esp32/__init__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 18:48:17 -06:00
J. Nick Koston
77fa58541f [esp32] Reduce Arduino build size by 44% and build time by 36% 2026-01-29 18:38:54 -06:00
J. Nick Koston
49840ed4fa [logger] Fix USB Serial JTAG VFS linker errors when using UART on IDF 2026-01-29 17:18:42 -06:00
20 changed files with 440 additions and 80 deletions

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
with:
category: "/language:${{matrix.language}}"

View File

@@ -89,8 +89,9 @@ async def to_code(config):
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
)
# Although this component does not use SPI, the BSEC library requires the SPI library
# Although this component does not use SPI/Wire directly, the BSEC library requires them
cg.add_library("SPI", None)
cg.add_library("Wire", None)
cg.add_define("USE_BSEC")
cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480")

View File

@@ -5,6 +5,7 @@ import logging
import os
from pathlib import Path
import re
import shutil
from esphome import yaml_util
import esphome.codegen as cg
@@ -50,6 +51,7 @@ from esphome.writer import clean_cmake_cache
from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa
KEY_ARDUINO_LIBRARIES,
KEY_BOARD,
KEY_COMPONENTS,
KEY_ESP32,
@@ -124,10 +126,14 @@ COMPILER_OPTIMIZATIONS = {
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp_adc", # ADC driver - only needed by adc component
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
"esp_eth", # Ethernet driver - only needed by ethernet component
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component
@@ -138,14 +144,178 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"openthread", # Thread protocol - only needed by openthread component
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
"protocomm", # Protocol communication for provisioning - unused by ESPHome
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
"ulp", # ULP coprocessor - not currently used by any ESPHome component
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
)
# Additional IDF managed components to exclude for Arduino framework builds
# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome
# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because
# that's how managed components are registered in the IDF build system
# List includes direct dependencies from arduino-esp32/idf_component.yml
# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need)
ARDUINO_EXCLUDED_IDF_COMPONENTS = (
"chmorgan__esp-libhelix-mp3", # MP3 decoder - not used
"espressif__cbor", # CBOR library - only used by RainMaker/Insights
"espressif__esp-dsp", # DSP library - not used
"espressif__esp-modbus", # Modbus - ESPHome has its own
"espressif__esp-sr", # Speech recognition - not used
"espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used
"espressif__esp-zigbee-lib", # Zigbee library - not used
"espressif__esp_diag_data_store", # Diagnostics - not used
"espressif__esp_diagnostics", # Diagnostics - not used
"espressif__esp_hosted", # ESP hosted - only for ESP32-P4
"espressif__esp_insights", # ESP Insights - not used
"espressif__esp_modem", # Modem library - not used
"espressif__esp_rainmaker", # RainMaker - not used
"espressif__esp_rcp_update", # RCP update - RainMaker transitive dep
"espressif__esp_schedule", # Schedule - RainMaker transitive dep
"espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep
"espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4
"espressif__json_generator", # JSON generator - RainMaker transitive dep
"espressif__json_parser", # JSON parser - RainMaker transitive dep
"espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly
"espressif__libsodium", # Crypto - ESPHome uses its own noise-c library
"espressif__network_provisioning", # Network provisioning - not used
"espressif__qrcode", # QR code - not used
"espressif__rmaker_common", # RainMaker common - not used
"joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem
)
# Mapping of Arduino libraries to IDF managed components they require
# When an Arduino library is enabled via cg.add_library(), these components
# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS.
#
# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have
# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle
# their managed component dependencies. Our mapping is primarily needed for libraries
# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.)
# and to ensure the stubs are removed from our idf_component.yml overrides.
ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = {
"BLE": ("esp_driver_gptimer",),
"BluetoothSerial": ("esp_driver_gptimer",),
"ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"),
"ESP_SR": ("espressif__esp-sr",),
"Ethernet": ("espressif__lan867x",),
"FFat": ("fatfs",),
"Insights": (
"espressif__cbor",
"espressif__esp_insights",
"espressif__esp_diagnostics",
"espressif__esp_diag_data_store",
"espressif__rmaker_common", # Transitive dep from esp_insights
),
"LittleFS": ("joltwallet__littlefs",),
"Matter": ("espressif__esp_matter",),
"PPP": ("espressif__esp_modem",),
"RainMaker": (
# Direct deps from idf_component.yml
"espressif__cbor",
"espressif__esp_rainmaker",
"espressif__esp_insights",
"espressif__esp_diagnostics",
"espressif__esp_diag_data_store",
"espressif__rmaker_common",
"espressif__qrcode",
# Transitive deps from esp_rainmaker
"espressif__esp_rcp_update",
"espressif__esp_schedule",
"espressif__esp_secure_cert_mgr",
"espressif__json_generator",
"espressif__json_parser",
"espressif__network_provisioning",
),
"SD": ("fatfs",),
"SD_MMC": ("fatfs",),
"SPIFFS": ("spiffs",),
"WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"),
"Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"),
}
# Arduino library to Arduino library dependencies
# When enabling one library, also enable its dependencies
# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION
ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = {
"Ethernet": ("Network",),
"WiFi": ("Network",),
}
def _idf_component_stub_name(component: str) -> str:
"""Get stub directory name from IDF component name.
Component names are typically namespace__name (e.g., espressif__cbor).
Returns just the name part (e.g., cbor). If no namespace is present,
returns the original component name.
"""
_prefix, sep, suffix = component.partition("__")
return suffix if sep else component
def _idf_component_dep_name(component: str) -> str:
"""Convert IDF component name to dependency format.
Converts espressif__cbor to espressif/cbor.
"""
return component.replace("__", "/")
# Arduino libraries to disable by default when using Arduino framework
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
# Components use cg.add_library() which auto-enables any they need
# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt
ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset(
{
"ArduinoOTA",
"AsyncUDP",
"BLE",
"BluetoothSerial",
"DNSServer",
"EEPROM",
"ESP_HostedOTA",
"ESP_I2S",
"ESP_NOW",
"ESP_SR",
"ESPmDNS",
"Ethernet",
"FFat",
"FS",
"Hash",
"HTTPClient",
"HTTPUpdate",
"Insights",
"LittleFS",
"Matter",
"NetBIOS",
"Network",
"NetworkClientSecure",
"OpenThread",
"PPP",
"Preferences",
"RainMaker",
"SD",
"SD_MMC",
"SimpleBLE",
"SPI",
"SPIFFS",
"Ticker",
"Update",
"USB",
"WebServer",
"WiFi",
"WiFiProv",
"Wire",
"Zigbee",
}
)
# ESP32 (original) chip revision options
# Setting minimum revision to 3.0 or higher:
# - Reduces flash size by excluding workaround code for older chip bugs
@@ -237,7 +407,13 @@ def set_core_data(config):
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
# Initialize with default exclusions - components can call include_builtin_idf_component()
# to re-enable any they need
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
# Add Arduino-specific managed component exclusions when using Arduino framework
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS)
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
@@ -385,6 +561,26 @@ def include_builtin_idf_component(name: str) -> None:
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
def _enable_arduino_library(name: str) -> None:
"""Enable an Arduino library that is disabled by default.
This is called automatically by CORE.add_library() when a component adds
an Arduino library via cg.add_library(). Components should not call this
directly - just use cg.add_library("LibName", None).
Args:
name: The library name (e.g., "Wire", "SPI", "WiFi")
"""
enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES]
enabled_libs.add(name)
# Also enable any required Arduino library dependencies
for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()):
enabled_libs.add(dep_lib)
# Also enable any required IDF components
for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()):
include_builtin_idf_component(idf_component)
def add_extra_script(stage: str, filename: str, path: Path):
"""Add an extra script to the project."""
key = f"{stage}:{filename}"
@@ -1126,6 +1322,27 @@ async def _write_exclude_components() -> None:
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _write_arduino_libraries_sdkconfig() -> None:
"""Write Arduino selective compilation sdkconfig after all components have added libraries.
This must run at FINAL priority so that all components have had a chance to call
cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library().
"""
if KEY_ESP32 not in CORE.data:
return
# Enable Arduino selective compilation to disable unused Arduino libraries
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
# cg.add_library() auto-enables needed libraries; users can also add
# libraries via esphome: libraries: config which calls cg.add_library()
add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True)
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
for lib in ARDUINO_DISABLED_LIBRARIES:
# Enable if explicitly requested, disable otherwise
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_yaml_idf_components(components: list[ConfigType]):
"""Add IDF components from YAML config with final priority to override code-added components."""
@@ -1544,6 +1761,11 @@ async def to_code(config):
# Default exclusions are added in set_core_data() during config validation.
CORE.add_job(_write_exclude_components)
# Write Arduino selective compilation sdkconfig at FINAL priority after all
# components have had a chance to call cg.add_library() to enable libraries they need.
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.add_job(_write_arduino_libraries_sdkconfig)
APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB
@@ -1624,11 +1846,49 @@ def _write_sdkconfig():
def _write_idf_component_yml():
yml_path = CORE.relative_build_path("src/idf_component.yml")
dependencies: dict[str, dict] = {}
# For Arduino builds, override unused managed components from the Arduino framework
# by pointing them to empty stub directories using override_path
# This prevents the IDF component manager from downloading the real components
if CORE.using_arduino:
# Determine which IDF components are needed by enabled Arduino libraries
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
required_idf_components = {
comp
for lib in enabled_libs
for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ())
}
# Only stub components that are not required by any enabled Arduino library
components_to_stub = (
set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components
)
stubs_dir = CORE.relative_build_path("component_stubs")
stubs_dir.mkdir(exist_ok=True)
for component_name in components_to_stub:
# Create stub directory with minimal CMakeLists.txt
stub_path = stubs_dir / _idf_component_stub_name(component_name)
stub_path.mkdir(exist_ok=True)
stub_cmake = stub_path / "CMakeLists.txt"
if not stub_cmake.exists():
stub_cmake.write_text("idf_component_register()\n")
dependencies[_idf_component_dep_name(component_name)] = {
"version": "*",
"override_path": str(stub_path),
}
# Remove stubs for components that are now required by enabled libraries
for component_name in required_idf_components:
stub_path = stubs_dir / _idf_component_stub_name(component_name)
if stub_path.exists():
shutil.rmtree(stub_path)
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
dependencies = {}
for name, component in components.items():
dependency = {}
dependency: dict[str, str] = {}
if component[KEY_REF]:
dependency["version"] = component[KEY_REF]
if component[KEY_REPO]:
@@ -1636,9 +1896,8 @@ def _write_idf_component_yml():
if component[KEY_PATH]:
dependency["path"] = component[KEY_PATH]
dependencies[name] = dependency
contents = yaml_util.dump({"dependencies": dependencies})
else:
contents = ""
contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else ""
if write_file_if_changed(yml_path, contents):
dependencies_lock = CORE.relative_build_path("dependencies.lock")
if dependencies_lock.is_file():

View File

@@ -7,6 +7,7 @@ KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components"
KEY_EXCLUDE_COMPONENTS = "exclude_components"
KEY_ARDUINO_LIBRARIES = "arduino_libraries"
KEY_REPO = "repo"
KEY_REF = "ref"
KEY_REFRESH = "refresh"

View File

@@ -15,6 +15,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate):
async def to_code(config):
# Legacy driver component provides driver/twai.h header
include_builtin_idf_component("driver")
# Also enable esp_driver_twai for future migration to new API
include_builtin_idf_component("esp_driver_twai")
var = cg.new_Pvariable(config[CONF_ID])
await canbus.register_canbus(var, config)

View File

@@ -1,7 +1,12 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import output
from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32S2,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
@@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config):
include_builtin_idf_component("esp_driver_dac")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await output.register_output(var, config)

View File

@@ -269,6 +269,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_touch_sens")
# Legacy driver component provides driver/touch_sensor.h header
include_builtin_idf_component("driver")
touch = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(touch, config)

View File

@@ -13,7 +13,7 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_WIFI,
)
from esphome.core import CORE, HexInt
from esphome.core import HexInt
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
@@ -124,9 +124,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CORE.using_arduino:
cg.add_library("WiFi", None)
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
# This enables low-latency event processing instead of waiting for select() timeout
socket.require_wake_loop_threadsafe()

View File

@@ -431,9 +431,6 @@ async def to_code(config):
# Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0")
if CORE.using_arduino:
cg.add_library("WiFi", None)
if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
await automation.build_automation(

View File

@@ -114,6 +114,7 @@ async def to_code(config):
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb"))
cg.add_library("WiFi", None)
cg.add_library("NetworkClientSecure", None)
cg.add_library("HTTPClient", None)
cg.add_library("esphome/ESP32-audioI2S", "2.3.0")

View File

@@ -128,22 +128,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
//
// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
// The buffer is used in a special way to avoid allocating extra memory:
//
// Memory layout during execution:
// Step 1: Copy format string from flash to buffer
// tx_buffer_: [format_string][null][.....................]
// tx_buffer_at_: ------------------^
// msg_start: saved here -----------^
//
// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
// and writes formatted output starting at msg_start position
// tx_buffer_: [format_string][null][formatted_message][null]
// tx_buffer_at_: -------------------------------------^
//
// Step 3: Output the formatted message (starting at msg_start)
// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
// which points to: [formatted_message][null]
// Uses vsnprintf_P to read the format string directly from flash without copying to RAM.
//
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT
@@ -153,35 +138,25 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
RecursionGuard guard(global_recursion_guard_);
this->tx_buffer_at_ = 0;
// Copy format string from progmem
auto *format_pgm_p = reinterpret_cast<const uint8_t *>(format);
char ch = '.';
while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') {
this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++);
}
// Write header, format body directly from flash, and write footer
this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_,
this->tx_buffer_size_);
this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_,
reinterpret_cast<PGM_P>(format), args);
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
// Buffer full from copying format - RAII guard handles cleanup on return
if (this->tx_buffer_at_ >= this->tx_buffer_size_) {
return;
}
// Save the offset before calling format_log_to_buffer_with_terminator_
// since it will increment tx_buffer_at_ to the end of the formatted string
uint16_t msg_start = this->tx_buffer_at_;
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
&this->tx_buffer_at_, this->tx_buffer_size_);
uint16_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
// Ensure null termination
uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_;
this->tx_buffer_[null_pos] = '\0';
// Listeners get message first (before console write)
#ifdef USE_LOG_LISTENERS
for (auto *listener : this->log_listeners_)
listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length);
listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_);
#endif
// Write to console starting at the msg_start
this->write_tx_buffer_to_console_(msg_start, &msg_length);
// Write to console
this->write_tx_buffer_to_console_();
}
#endif // USE_STORE_LOG_STR_IN_FLASH

View File

@@ -597,31 +597,40 @@ class Logger : public Component {
*buffer_at = pos;
}
// Helper to process vsnprintf return value and strip trailing newlines.
// Updates buffer_at with the formatted length, handling truncation:
// - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
__attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at,
uint16_t remaining, int ret) {
if (ret < 0)
return; // Encoding error, do not increment buffer_at
*buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast<uint16_t>(ret);
// Remove all trailing newlines right after formatting
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n')
(*buffer_at)--;
}
inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format,
va_list args) {
// Get remaining capacity in the buffer
// Check remaining capacity in the buffer
if (*buffer_at >= buffer_size)
return;
const uint16_t remaining = buffer_size - *buffer_at;
const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args);
if (ret < 0) {
return; // Encoding error, do not increment buffer_at
}
// Update buffer_at with the formatted length (handle truncation)
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
*buffer_at += formatted_len;
// Remove all trailing newlines right after formatting
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') {
(*buffer_at)--;
}
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
// ESP8266 variant that reads format string directly from flash using vsnprintf_P
inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format,
va_list args) {
if (*buffer_at >= buffer_size)
return;
const uint16_t remaining = buffer_size - *buffer_at;
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args));
}
#endif
inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);

View File

@@ -30,7 +30,7 @@ from esphome.const import (
UNIT_PERCENT,
UNIT_WATT,
)
from esphome.core import coroutine
from esphome.core import CORE, coroutine
CODEOWNERS = ["@dudanov"]
DEPENDENCIES = ["climate", "uart"]
@@ -290,4 +290,7 @@ async def to_code(config):
if CONF_HUMIDITY_SETPOINT in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))
# MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping)
if CORE.is_esp32:
cg.add_library("WiFi", None)
cg.add_library("dudanov/MideaUART", "1.1.9")

View File

@@ -137,8 +137,7 @@ CONFIG_SCHEMA = cv.Schema(
@coroutine_with_priority(CoroPriority.NETWORK)
async def to_code(config):
cg.add_define("USE_NETWORK")
if CORE.using_arduino and CORE.is_esp32:
cg.add_library("Networking", None)
# ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed
# Apply high performance networking settings
# Config can explicitly enable/disable, or default to component-driven behavior

View File

@@ -4,6 +4,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C6,
VARIANT_ESP32H2,
add_idf_sdkconfig_option,
include_builtin_idf_component,
only_on_variant,
require_vfs_select,
)
@@ -172,6 +173,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
# Re-enable openthread IDF component (excluded by default)
include_builtin_idf_component("openthread")
cg.add_define("USE_OPENTHREAD")
# OpenThread SRP needs access to mDNS services after setup

View File

@@ -38,11 +38,8 @@ async def to_code(config):
cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
return
# ESP32 uses IDF web server (early return above), so this is for other Arduino platforms
if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("WiFi", None)
cg.add_library("FS", None)
cg.add_library("Update", None)
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
if CORE.is_libretiny:

View File

@@ -3,6 +3,7 @@ from esphome.components.light.effects import register_addressable_effect
from esphome.components.light.types import AddressableLightEffect
import esphome.config_validation as cv
from esphome.const import CONF_NAME, CONF_PORT
from esphome.core import CORE
wled_ns = cg.esphome_ns.namespace("wled")
WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect)
@@ -27,4 +28,6 @@ async def wled_light_effect_to_code(config, effect_id):
cg.add(effect.set_port(config[CONF_PORT]))
cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK]))
cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START]))
if CORE.is_esp32:
cg.add_library("WiFi", None)
return effect

View File

@@ -892,6 +892,16 @@ class EsphomeCore:
library.name if "/" not in library.name else library.name.split("/")[-1]
)
# Auto-enable Arduino libraries on ESP32 Arduino builds
if self.is_esp32 and self.using_arduino:
from esphome.components.esp32 import (
ARDUINO_DISABLED_LIBRARIES,
_enable_arduino_library,
)
if short_name in ARDUINO_DISABLED_LIBRARIES:
_enable_arduino_library(short_name)
if short_name not in self.platformio_libraries:
_LOGGER.debug("Adding library: %s", library)
self.platformio_libraries[short_name] = library

View File

@@ -0,0 +1,16 @@
substitutions:
i2s_bclk_pin: GPIO15
i2s_lrclk_pin: GPIO4
i2s_mclk_pin: GPIO5
<<: !include common.yaml
wifi:
ssid: test
password: test1234
media_player:
- platform: i2s_audio
name: Test Media Player
dac_type: internal
mode: stereo

View File

@@ -780,3 +780,78 @@ class TestEsphomeCore:
target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}}
assert target.has_networking is False
def test_add_library__esp32_arduino_enables_disabled_library(self, target):
"""Test add_library auto-enables Arduino libraries on ESP32 Arduino builds."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("WiFi", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_called_once_with("WiFi")
assert "WiFi" in target.platformio_libraries
def test_add_library__esp32_arduino_ignores_non_arduino_library(self, target):
"""Test add_library doesn't enable libraries not in ARDUINO_DISABLED_LIBRARIES."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("SomeOtherLib", "1.0.0")
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_not_called()
assert "SomeOtherLib" in target.platformio_libraries
def test_add_library__esp32_idf_does_not_enable_arduino_library(self, target):
"""Test add_library doesn't auto-enable Arduino libraries on ESP32 IDF builds."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "esp-idf",
}
library = core.Library("WiFi", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_not_called()
assert "WiFi" in target.platformio_libraries
def test_add_library__esp8266_does_not_enable_arduino_library(self, target):
"""Test add_library doesn't auto-enable Arduino libraries on ESP8266."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp8266",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("WiFi", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_not_called()
assert "WiFi" in target.platformio_libraries
def test_add_library__extracts_short_name_from_path(self, target):
"""Test add_library extracts short name from library paths like owner/lib."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("arduino/Wire", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_called_once_with("Wire")
assert "Wire" in target.platformio_libraries