From 8aa4157574e6c7dcbbe797e1f51e3f29018f9ed0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:01:29 -0400 Subject: [PATCH] [fastled_base] Use FastLED IDF component on ESP32 (#16804) --- .clang-tidy.hash | 2 +- esphome/components/fastled_base/__init__.py | 12 ++++-- .../components/fastled_base/fastled_light.h | 2 - esphome/espidf/clang_tidy.py | 6 +++ esphome/idf_component.yml | 5 +++ platformio.ini | 3 +- script/clang-tidy | 12 +++++- tests/unit_tests/test_espidf_clang_tidy.py | 39 +++++++++++++++++++ 8 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 tests/unit_tests/test_espidf_clang_tidy.py diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 0782b065f3..3bcf356f86 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -0b8325f52fca9224efb80dacca51ccbc8b3499bde7bb4aaa6f28a848c2e0a6a8 +d583091c0f465aed86a825138e309af6d9db6834106ab424f36712424a6c2223 diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index c944e8a930..d99dffdc08 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -41,10 +41,16 @@ async def new_fastled_light(config): if CONF_MAX_REFRESH_RATE in config: cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) - cg.add_library("fastled/FastLED", "3.9.16") if CORE.is_esp32: - from esphome.components.esp32 import include_builtin_idf_component + from esphome.components.esp32 import add_idf_component - include_builtin_idf_component("esp_lcd") + add_idf_component( + name="fastled/FastLED", + repo="https://github.com/FastLED/FastLED.git", + ref="d44c800a9e876a8394caefc2ce4915dd96dac77b", + ) + cg.add_library("SPI", None) + else: + cg.add_library("fastled/FastLED", "3.9.16") await light.register_light(var, config) return var diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 8e87f67e6d..f8535eb628 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -143,7 +143,6 @@ class FastLEDLightOutput : public light::AddressableLight { } } -#ifdef FASTLED_HAS_CLOCKLESS template class CHIPSET, uint8_t DATA_PIN, EOrder RGB_ORDER> CLEDController &add_leds(int num_leds) { static CHIPSET controller; @@ -160,7 +159,6 @@ class FastLEDLightOutput : public light::AddressableLight { static CHIPSET controller; return add_leds(&controller, num_leds); } -#endif template class CHIPSET, EOrder RGB_ORDER> CLEDController &add_leds(int num_leds) { static CHIPSET controller; diff --git a/esphome/espidf/clang_tidy.py b/esphome/espidf/clang_tidy.py index 2cfbe67a70..7647db63f5 100644 --- a/esphome/espidf/clang_tidy.py +++ b/esphome/espidf/clang_tidy.py @@ -160,6 +160,12 @@ def _setup_core(work_dir: Path, settings: _Settings) -> None: CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = "esp32" CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = settings.target_framework + # Gates arduino-only components in esphome/idf_component.yml (IDF reads it at + # reconfigure time). Set here -- before the manifest is written/reconfigured. + os.environ["ESPHOME_ARDUINO"] = ( + "1" if settings.target_framework == "arduino" else "0" + ) + # Special IDF "components" that are tools/subprojects, not requirable by an app # (they provide no public includes and break requirement resolution), plus our diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 3a5b050072..4a4bc18579 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -103,3 +103,8 @@ dependencies: version: 0.6.1 lvgl/lvgl: version: 9.5.0 + fastled/FastLED: + git: https://github.com/FastLED/FastLED.git + version: d44c800a9e876a8394caefc2ce4915dd96dac77b + rules: + - if: "$ESPHOME_ARDUINO == 1" diff --git a/platformio.ini b/platformio.ini index 07e9b8aad3..d7bcc49758 100644 --- a/platformio.ini +++ b/platformio.ini @@ -79,7 +79,6 @@ lib_deps = SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) heman/AsyncMqttClient-esphome@1.0.0 ; mqtt - fastled/FastLED@3.9.16 ; fastled_base freekode/TM1651@1.0.1 ; tm1651 dudanov/MideaUART@1.1.9 ; midea tonia/HeatpumpIR@1.0.41 ; heatpumpir @@ -108,6 +107,7 @@ platform_packages = framework = arduino lib_deps = ${common:arduino.lib_deps} + fastled/FastLED@3.9.16 ; fastled_base bblanchon/ArduinoJson@7.4.2 ; json ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) @@ -198,6 +198,7 @@ platform_packages = framework = arduino lib_deps = ${common:arduino.lib_deps} + fastled/FastLED@3.9.16 ; fastled_base ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base diff --git a/script/clang-tidy b/script/clang-tidy index 633b8d4b7d..f19bdb9b56 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -35,9 +35,19 @@ def clang_options(idedata): # extract target architecture from triplet in g++ filename triplet = Path(idedata["cxx_path"]).name[:-4] if triplet.startswith("xtensa-"): - # clang doesn't support Xtensa (yet?), so compile in 32-bit mode and pretend we're the Xtensa compiler + # clang has an Xtensa frontend, but only a generic core -- the esp32 IDF + # toolchain headers (xtruntime, xtensa/config) need the GCC core config + # (XCHAL_*) it doesn't ship, so we still compile in 32-bit x86 mode and + # just pretend to be Xtensa. Undefine the host x86 arch macros -m32 sets, + # so libraries with x86 SIMD paths (FastLED's fl/math/simd, simd_x86.hpp) + # fall back to their scalar implementation instead of an incomplete + # host-x86 one, and define the xtensa endianness macro newlib's + # machine/ieeefp.h then needs in their place. cmd.append("-m32") + cmd.append("-U__i386__") + cmd.append("-U__x86_64__") cmd.append("-D__XTENSA__") + cmd.append("-D__XTENSA_EL__") cmd.append("-D_LIBC") else: # RISC-V (and other non-Xtensa targets) have a real clang backend, so diff --git a/tests/unit_tests/test_espidf_clang_tidy.py b/tests/unit_tests/test_espidf_clang_tidy.py new file mode 100644 index 0000000000..7a71dc26f4 --- /dev/null +++ b/tests/unit_tests/test_espidf_clang_tidy.py @@ -0,0 +1,39 @@ +"""Tests for esphome.espidf.clang_tidy tidy-project setup.""" + +import os +from pathlib import Path + +import pytest + +from esphome.espidf.clang_tidy import _Settings, _setup_core + + +def _settings(target_framework: str) -> _Settings: + return _Settings( + idf_target="esp32", + variant="ESP32", + idf_version="5.5.4", + target_framework=target_framework, + platform_defines=("USE_ESP32",), + framework_deps={}, + ) + + +@pytest.mark.parametrize( + ("target_framework", "expected"), + [("arduino", "1"), ("espidf", "0")], +) +def test_setup_core_sets_arduino_env( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + target_framework: str, + expected: str, +) -> None: + """_setup_core sets ESPHOME_ARDUINO, which gates arduino-only manifest deps.""" + # monkeypatch snapshots os.environ, so the env var _setup_core writes is + # restored after the test instead of leaking into later tests. + monkeypatch.delenv("ESPHOME_ARDUINO", raising=False) + + _setup_core(tmp_path / "proj", _settings(target_framework)) + + assert os.environ["ESPHOME_ARDUINO"] == expected