diff --git a/README.md b/README.md index eba38ec..a6cb180 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,6 @@ servo 文件夹是用于写入舵机初始化程序 2025-11-23 v1.3增加一个简单的越障模式,和斜坡自平衡功能. -2025-12-21 v2.0增加建议云台人脸跟踪效果. \ No newline at end of file +2025-12-21 v1.4增加建议云台人脸跟踪效果. + +2025-12-23 v1.5增加一个舵机控制前档,实现踢球效果。 \ No newline at end of file diff --git a/v2.0/maixcam/main.py b/v1.4/maixcam/main.py similarity index 100% rename from v2.0/maixcam/main.py rename to v1.4/maixcam/main.py diff --git a/v2.0/wrobot/.gitignore b/v1.4/wrobot/.gitignore similarity index 100% rename from v2.0/wrobot/.gitignore rename to v1.4/wrobot/.gitignore diff --git a/v2.0/wrobot/README.md b/v1.4/wrobot/README.md similarity index 100% rename from v2.0/wrobot/README.md rename to v1.4/wrobot/README.md diff --git a/v2.0/wrobot/lib/ESP32Servo/AddDoxygenToGHPages.sh b/v1.4/wrobot/lib/ESP32Servo/AddDoxygenToGHPages.sh similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/AddDoxygenToGHPages.sh rename to v1.4/wrobot/lib/ESP32Servo/AddDoxygenToGHPages.sh diff --git a/v2.0/wrobot/lib/ESP32Servo/README.md b/v1.4/wrobot/lib/ESP32Servo/README.md similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/README.md rename to v1.4/wrobot/lib/ESP32Servo/README.md diff --git a/v2.0/wrobot/lib/ESP32Servo/doxy.doxyfile b/v1.4/wrobot/lib/ESP32Servo/doxy.doxyfile similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/doxy.doxyfile rename to v1.4/wrobot/lib/ESP32Servo/doxy.doxyfile diff --git a/v2.0/wrobot/lib/ESP32Servo/examples/Knob/Knob.ino b/v1.4/wrobot/lib/ESP32Servo/examples/Knob/Knob.ino similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/examples/Knob/Knob.ino rename to v1.4/wrobot/lib/ESP32Servo/examples/Knob/Knob.ino diff --git a/v2.0/wrobot/lib/ESP32Servo/examples/Multiple-Servo-Example-Arduino/Multiple-Servo-Example-Arduino.ino b/v1.4/wrobot/lib/ESP32Servo/examples/Multiple-Servo-Example-Arduino/Multiple-Servo-Example-Arduino.ino similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/examples/Multiple-Servo-Example-Arduino/Multiple-Servo-Example-Arduino.ino rename to v1.4/wrobot/lib/ESP32Servo/examples/Multiple-Servo-Example-Arduino/Multiple-Servo-Example-Arduino.ino diff --git a/v2.0/wrobot/lib/ESP32Servo/examples/PWMExample/PWMExample.ino b/v1.4/wrobot/lib/ESP32Servo/examples/PWMExample/PWMExample.ino similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/examples/PWMExample/PWMExample.ino rename to v1.4/wrobot/lib/ESP32Servo/examples/PWMExample/PWMExample.ino diff --git a/v2.0/wrobot/lib/ESP32Servo/examples/Sweep/Sweep.ino b/v1.4/wrobot/lib/ESP32Servo/examples/Sweep/Sweep.ino similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/examples/Sweep/Sweep.ino rename to v1.4/wrobot/lib/ESP32Servo/examples/Sweep/Sweep.ino diff --git a/v2.0/wrobot/lib/ESP32Servo/examples/ToneExample/ToneExample.ino b/v1.4/wrobot/lib/ESP32Servo/examples/ToneExample/ToneExample.ino similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/examples/ToneExample/ToneExample.ino rename to v1.4/wrobot/lib/ESP32Servo/examples/ToneExample/ToneExample.ino diff --git a/v2.0/wrobot/lib/ESP32Servo/examples/analogWriteExample/analogWriteExample.ino b/v1.4/wrobot/lib/ESP32Servo/examples/analogWriteExample/analogWriteExample.ino similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/examples/analogWriteExample/analogWriteExample.ino rename to v1.4/wrobot/lib/ESP32Servo/examples/analogWriteExample/analogWriteExample.ino diff --git a/v2.0/wrobot/lib/ESP32Servo/keywords.txt b/v1.4/wrobot/lib/ESP32Servo/keywords.txt similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/keywords.txt rename to v1.4/wrobot/lib/ESP32Servo/keywords.txt diff --git a/v2.0/wrobot/lib/ESP32Servo/library.properties b/v1.4/wrobot/lib/ESP32Servo/library.properties similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/library.properties rename to v1.4/wrobot/lib/ESP32Servo/library.properties diff --git a/v2.0/wrobot/lib/ESP32Servo/src/ESP32PWM.cpp b/v1.4/wrobot/lib/ESP32Servo/src/ESP32PWM.cpp similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/src/ESP32PWM.cpp rename to v1.4/wrobot/lib/ESP32Servo/src/ESP32PWM.cpp diff --git a/v2.0/wrobot/lib/ESP32Servo/src/ESP32PWM.h b/v1.4/wrobot/lib/ESP32Servo/src/ESP32PWM.h similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/src/ESP32PWM.h rename to v1.4/wrobot/lib/ESP32Servo/src/ESP32PWM.h diff --git a/v2.0/wrobot/lib/ESP32Servo/src/ESP32Servo.cpp b/v1.4/wrobot/lib/ESP32Servo/src/ESP32Servo.cpp similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/src/ESP32Servo.cpp rename to v1.4/wrobot/lib/ESP32Servo/src/ESP32Servo.cpp diff --git a/v2.0/wrobot/lib/ESP32Servo/src/ESP32Servo.h b/v1.4/wrobot/lib/ESP32Servo/src/ESP32Servo.h similarity index 100% rename from v2.0/wrobot/lib/ESP32Servo/src/ESP32Servo.h rename to v1.4/wrobot/lib/ESP32Servo/src/ESP32Servo.h diff --git a/v2.0/wrobot/lib/FastLED/ADVANCED_DEVELOPMENT.md b/v1.4/wrobot/lib/FastLED/ADVANCED_DEVELOPMENT.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ADVANCED_DEVELOPMENT.md rename to v1.4/wrobot/lib/FastLED/ADVANCED_DEVELOPMENT.md diff --git a/v2.0/wrobot/lib/FastLED/APA102.md b/v1.4/wrobot/lib/FastLED/APA102.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/APA102.md rename to v1.4/wrobot/lib/FastLED/APA102.md diff --git a/v2.0/wrobot/lib/FastLED/BACKGROUND_AGENT_TESTING_REQUIREMENTS.md b/v1.4/wrobot/lib/FastLED/BACKGROUND_AGENT_TESTING_REQUIREMENTS.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/BACKGROUND_AGENT_TESTING_REQUIREMENTS.md rename to v1.4/wrobot/lib/FastLED/BACKGROUND_AGENT_TESTING_REQUIREMENTS.md diff --git a/v2.0/wrobot/lib/FastLED/CLAUDE.md b/v1.4/wrobot/lib/FastLED/CLAUDE.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/CLAUDE.md rename to v1.4/wrobot/lib/FastLED/CLAUDE.md diff --git a/v2.0/wrobot/lib/FastLED/CMakeLists.txt b/v1.4/wrobot/lib/FastLED/CMakeLists.txt similarity index 100% rename from v2.0/wrobot/lib/FastLED/CMakeLists.txt rename to v1.4/wrobot/lib/FastLED/CMakeLists.txt diff --git a/v2.0/wrobot/lib/FastLED/CONTRIBUTING.md b/v1.4/wrobot/lib/FastLED/CONTRIBUTING.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/CONTRIBUTING.md rename to v1.4/wrobot/lib/FastLED/CONTRIBUTING.md diff --git a/v2.0/wrobot/lib/FastLED/CORKSCREW.md b/v1.4/wrobot/lib/FastLED/CORKSCREW.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/CORKSCREW.md rename to v1.4/wrobot/lib/FastLED/CORKSCREW.md diff --git a/v2.0/wrobot/lib/FastLED/FEATURE.md b/v1.4/wrobot/lib/FastLED/FEATURE.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/FEATURE.md rename to v1.4/wrobot/lib/FastLED/FEATURE.md diff --git a/v2.0/wrobot/lib/FastLED/FEATURE_PACKAGE_JSON.md b/v1.4/wrobot/lib/FastLED/FEATURE_PACKAGE_JSON.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/FEATURE_PACKAGE_JSON.md rename to v1.4/wrobot/lib/FastLED/FEATURE_PACKAGE_JSON.md diff --git a/v2.0/wrobot/lib/FastLED/FEATURE_RMT5_POOL.md b/v1.4/wrobot/lib/FastLED/FEATURE_RMT5_POOL.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/FEATURE_RMT5_POOL.md rename to v1.4/wrobot/lib/FastLED/FEATURE_RMT5_POOL.md diff --git a/v2.0/wrobot/lib/FastLED/GEMINI.md b/v1.4/wrobot/lib/FastLED/GEMINI.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/GEMINI.md rename to v1.4/wrobot/lib/FastLED/GEMINI.md diff --git a/v2.0/wrobot/lib/FastLED/LAYOUT.md b/v1.4/wrobot/lib/FastLED/LAYOUT.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/LAYOUT.md rename to v1.4/wrobot/lib/FastLED/LAYOUT.md diff --git a/v2.0/wrobot/lib/FastLED/LICENSE b/v1.4/wrobot/lib/FastLED/LICENSE similarity index 100% rename from v2.0/wrobot/lib/FastLED/LICENSE rename to v1.4/wrobot/lib/FastLED/LICENSE diff --git a/v2.0/wrobot/lib/FastLED/PORTING.md b/v1.4/wrobot/lib/FastLED/PORTING.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/PORTING.md rename to v1.4/wrobot/lib/FastLED/PORTING.md diff --git a/v2.0/wrobot/lib/FastLED/QWEN.md b/v1.4/wrobot/lib/FastLED/QWEN.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/QWEN.md rename to v1.4/wrobot/lib/FastLED/QWEN.md diff --git a/v2.0/wrobot/lib/FastLED/README.md b/v1.4/wrobot/lib/FastLED/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/README.md rename to v1.4/wrobot/lib/FastLED/README.md diff --git a/v2.0/wrobot/lib/FastLED/RELEASE.md b/v1.4/wrobot/lib/FastLED/RELEASE.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/RELEASE.md rename to v1.4/wrobot/lib/FastLED/RELEASE.md diff --git a/v2.0/wrobot/lib/FastLED/RMT.md b/v1.4/wrobot/lib/FastLED/RMT.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/RMT.md rename to v1.4/wrobot/lib/FastLED/RMT.md diff --git a/v2.0/wrobot/lib/FastLED/TESTCASE_INSTRUCTIONS.md b/v1.4/wrobot/lib/FastLED/TESTCASE_INSTRUCTIONS.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/TESTCASE_INSTRUCTIONS.md rename to v1.4/wrobot/lib/FastLED/TESTCASE_INSTRUCTIONS.md diff --git a/v2.0/wrobot/lib/FastLED/TODO.md b/v1.4/wrobot/lib/FastLED/TODO.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/TODO.md rename to v1.4/wrobot/lib/FastLED/TODO.md diff --git a/v2.0/wrobot/lib/FastLED/VIDEO.md b/v1.4/wrobot/lib/FastLED/VIDEO.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/VIDEO.md rename to v1.4/wrobot/lib/FastLED/VIDEO.md diff --git a/v2.0/wrobot/lib/FastLED/apa102fix.md b/v1.4/wrobot/lib/FastLED/apa102fix.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/apa102fix.md rename to v1.4/wrobot/lib/FastLED/apa102fix.md diff --git a/v2.0/wrobot/lib/FastLED/ci/AGENTS.md b/v1.4/wrobot/lib/FastLED/ci/AGENTS.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/AGENTS.md rename to v1.4/wrobot/lib/FastLED/ci/AGENTS.md diff --git a/v2.0/wrobot/lib/FastLED/ci/BUILD_SYSTEM_MIGRATION.md b/v1.4/wrobot/lib/FastLED/ci/BUILD_SYSTEM_MIGRATION.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/BUILD_SYSTEM_MIGRATION.md rename to v1.4/wrobot/lib/FastLED/ci/BUILD_SYSTEM_MIGRATION.md diff --git a/v2.0/wrobot/lib/FastLED/ci/README.md b/v1.4/wrobot/lib/FastLED/ci/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/README.md rename to v1.4/wrobot/lib/FastLED/ci/README.md diff --git a/v2.0/wrobot/lib/FastLED/ci/SYMBOL_ANALYSIS.md b/v1.4/wrobot/lib/FastLED/ci/SYMBOL_ANALYSIS.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/SYMBOL_ANALYSIS.md rename to v1.4/wrobot/lib/FastLED/ci/SYMBOL_ANALYSIS.md diff --git a/v2.0/wrobot/lib/FastLED/ci/__init__.py b/v1.4/wrobot/lib/FastLED/ci/__init__.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/__init__.py rename to v1.4/wrobot/lib/FastLED/ci/__init__.py diff --git a/v2.0/wrobot/lib/FastLED/ci/adafruit_bridge/adafruit_bridge.ino.cpp b/v1.4/wrobot/lib/FastLED/ci/adafruit_bridge/adafruit_bridge.ino.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/adafruit_bridge/adafruit_bridge.ino.cpp rename to v1.4/wrobot/lib/FastLED/ci/adafruit_bridge/adafruit_bridge.ino.cpp diff --git a/v2.0/wrobot/lib/FastLED/ci/adafruit_bridge/platformio.ini b/v1.4/wrobot/lib/FastLED/ci/adafruit_bridge/platformio.ini similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/adafruit_bridge/platformio.ini rename to v1.4/wrobot/lib/FastLED/ci/adafruit_bridge/platformio.ini diff --git a/v2.0/wrobot/lib/FastLED/ci/boards.py b/v1.4/wrobot/lib/FastLED/ci/boards.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards.py rename to v1.4/wrobot/lib/FastLED/ci/boards.py diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/bluepill.json b/v1.4/wrobot/lib/FastLED/ci/boards/bluepill.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/bluepill.json rename to v1.4/wrobot/lib/FastLED/ci/boards/bluepill.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/esp32-c2-devkitm-1.json b/v1.4/wrobot/lib/FastLED/ci/boards/esp32-c2-devkitm-1.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/esp32-c2-devkitm-1.json rename to v1.4/wrobot/lib/FastLED/ci/boards/esp32-c2-devkitm-1.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/esp32-c6-devkitc-1.json b/v1.4/wrobot/lib/FastLED/ci/boards/esp32-c6-devkitc-1.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/esp32-c6-devkitc-1.json rename to v1.4/wrobot/lib/FastLED/ci/boards/esp32-c6-devkitc-1.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/esp32-h2-devkitm-1.json b/v1.4/wrobot/lib/FastLED/ci/boards/esp32-h2-devkitm-1.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/esp32-h2-devkitm-1.json rename to v1.4/wrobot/lib/FastLED/ci/boards/esp32-h2-devkitm-1.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/giga.json b/v1.4/wrobot/lib/FastLED/ci/boards/giga.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/giga.json rename to v1.4/wrobot/lib/FastLED/ci/boards/giga.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/nrf52840_dk.json b/v1.4/wrobot/lib/FastLED/ci/boards/nrf52840_dk.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/nrf52840_dk.json rename to v1.4/wrobot/lib/FastLED/ci/boards/nrf52840_dk.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/rpipico.json b/v1.4/wrobot/lib/FastLED/ci/boards/rpipico.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/rpipico.json rename to v1.4/wrobot/lib/FastLED/ci/boards/rpipico.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/rpipico2.json b/v1.4/wrobot/lib/FastLED/ci/boards/rpipico2.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/rpipico2.json rename to v1.4/wrobot/lib/FastLED/ci/boards/rpipico2.json diff --git a/v2.0/wrobot/lib/FastLED/ci/boards/rpipicow.json b/v1.4/wrobot/lib/FastLED/ci/boards/rpipicow.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/boards/rpipicow.json rename to v1.4/wrobot/lib/FastLED/ci/boards/rpipicow.json diff --git a/v2.0/wrobot/lib/FastLED/ci/build_api.py b/v1.4/wrobot/lib/FastLED/ci/build_api.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/build_api.py rename to v1.4/wrobot/lib/FastLED/ci/build_api.py diff --git a/v2.0/wrobot/lib/FastLED/ci/build_commands.toml b/v1.4/wrobot/lib/FastLED/ci/build_commands.toml similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/build_commands.toml rename to v1.4/wrobot/lib/FastLED/ci/build_commands.toml diff --git a/v2.0/wrobot/lib/FastLED/ci/build_example.toml b/v1.4/wrobot/lib/FastLED/ci/build_example.toml similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/build_example.toml rename to v1.4/wrobot/lib/FastLED/ci/build_example.toml diff --git a/v2.0/wrobot/lib/FastLED/ci/build_example_usage.py b/v1.4/wrobot/lib/FastLED/ci/build_example_usage.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/build_example_usage.py rename to v1.4/wrobot/lib/FastLED/ci/build_example_usage.py diff --git a/v2.0/wrobot/lib/FastLED/ci/build_unit.toml b/v1.4/wrobot/lib/FastLED/ci/build_unit.toml similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/build_unit.toml rename to v1.4/wrobot/lib/FastLED/ci/build_unit.toml diff --git a/v2.0/wrobot/lib/FastLED/ci/check-js-types.py b/v1.4/wrobot/lib/FastLED/ci/check-js-types.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/check-js-types.py rename to v1.4/wrobot/lib/FastLED/ci/check-js-types.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-check-compiled-size.py b/v1.4/wrobot/lib/FastLED/ci/ci-check-compiled-size.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-check-compiled-size.py rename to v1.4/wrobot/lib/FastLED/ci/ci-check-compiled-size.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-compile b/v1.4/wrobot/lib/FastLED/ci/ci-compile similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-compile rename to v1.4/wrobot/lib/FastLED/ci/ci-compile diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-compile-concurrent-backup.py b/v1.4/wrobot/lib/FastLED/ci/ci-compile-concurrent-backup.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-compile-concurrent-backup.py rename to v1.4/wrobot/lib/FastLED/ci/ci-compile-concurrent-backup.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-compile-native.py b/v1.4/wrobot/lib/FastLED/ci/ci-compile-native.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-compile-native.py rename to v1.4/wrobot/lib/FastLED/ci/ci-compile-native.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-compile-pio-ci-working.py b/v1.4/wrobot/lib/FastLED/ci/ci-compile-pio-ci-working.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-compile-pio-ci-working.py rename to v1.4/wrobot/lib/FastLED/ci/ci-compile-pio-ci-working.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-compile.py b/v1.4/wrobot/lib/FastLED/ci/ci-compile.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-compile.py rename to v1.4/wrobot/lib/FastLED/ci/ci-compile.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-compile.save.py b/v1.4/wrobot/lib/FastLED/ci/ci-compile.save.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-compile.save.py rename to v1.4/wrobot/lib/FastLED/ci/ci-compile.save.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-cppcheck.py b/v1.4/wrobot/lib/FastLED/ci/ci-cppcheck.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-cppcheck.py rename to v1.4/wrobot/lib/FastLED/ci/ci-cppcheck.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-flags.py b/v1.4/wrobot/lib/FastLED/ci/ci-flags.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-flags.py rename to v1.4/wrobot/lib/FastLED/ci/ci-flags.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci-iwyu.py b/v1.4/wrobot/lib/FastLED/ci/ci-iwyu.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci-iwyu.py rename to v1.4/wrobot/lib/FastLED/ci/ci-iwyu.py diff --git a/v2.0/wrobot/lib/FastLED/ci/ci/fingerprint_cache.py b/v1.4/wrobot/lib/FastLED/ci/ci/fingerprint_cache.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/ci/fingerprint_cache.py rename to v1.4/wrobot/lib/FastLED/ci/ci/fingerprint_cache.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compile-commands.py b/v1.4/wrobot/lib/FastLED/ci/compile-commands.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compile-commands.py rename to v1.4/wrobot/lib/FastLED/ci/compile-commands.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiled_size.py b/v1.4/wrobot/lib/FastLED/ci/compiled_size.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiled_size.py rename to v1.4/wrobot/lib/FastLED/ci/compiled_size.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiled_size_history.py b/v1.4/wrobot/lib/FastLED/ci/compiled_size_history.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiled_size_history.py rename to v1.4/wrobot/lib/FastLED/ci/compiled_size_history.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/__init__.py b/v1.4/wrobot/lib/FastLED/ci/compiler/__init__.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/__init__.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/__init__.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/build_dynamic_lib.py b/v1.4/wrobot/lib/FastLED/ci/compiler/build_dynamic_lib.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/build_dynamic_lib.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/build_dynamic_lib.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/cache_enhanced_compilation.py b/v1.4/wrobot/lib/FastLED/ci/compiler/cache_enhanced_compilation.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/cache_enhanced_compilation.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/cache_enhanced_compilation.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/cache_setup.scons b/v1.4/wrobot/lib/FastLED/ci/compiler/cache_setup.scons similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/cache_setup.scons rename to v1.4/wrobot/lib/FastLED/ci/compiler/cache_setup.scons diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/clang_compiler.py b/v1.4/wrobot/lib/FastLED/ci/compiler/clang_compiler.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/clang_compiler.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/clang_compiler.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/compile_for_board.py b/v1.4/wrobot/lib/FastLED/ci/compiler/compile_for_board.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/compile_for_board.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/compile_for_board.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/compiler.py b/v1.4/wrobot/lib/FastLED/ci/compiler/compiler.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/compiler.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/compiler.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/cpp_test_compile.py b/v1.4/wrobot/lib/FastLED/ci/compiler/cpp_test_compile.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/cpp_test_compile.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/cpp_test_compile.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/cpp_test_run.py b/v1.4/wrobot/lib/FastLED/ci/compiler/cpp_test_run.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/cpp_test_run.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/cpp_test_run.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/packages.py b/v1.4/wrobot/lib/FastLED/ci/compiler/packages.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/packages.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/packages.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/pio.py b/v1.4/wrobot/lib/FastLED/ci/compiler/pio.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/pio.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/pio.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/platformio_cache.py b/v1.4/wrobot/lib/FastLED/ci/compiler/platformio_cache.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/platformio_cache.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/platformio_cache.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/platformio_ini.py b/v1.4/wrobot/lib/FastLED/ci/compiler/platformio_ini.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/platformio_ini.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/platformio_ini.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/test_compiler.py b/v1.4/wrobot/lib/FastLED/ci/compiler/test_compiler.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/test_compiler.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/test_compiler.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/test_example_compilation.py b/v1.4/wrobot/lib/FastLED/ci/compiler/test_example_compilation.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/test_example_compilation.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/test_example_compilation.py diff --git a/v2.0/wrobot/lib/FastLED/ci/compiler/unity_archive.py b/v1.4/wrobot/lib/FastLED/ci/compiler/unity_archive.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/compiler/unity_archive.py rename to v1.4/wrobot/lib/FastLED/ci/compiler/unity_archive.py diff --git a/v2.0/wrobot/lib/FastLED/ci/demo_symbol_analysis.py b/v1.4/wrobot/lib/FastLED/ci/demo_symbol_analysis.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/demo_symbol_analysis.py rename to v1.4/wrobot/lib/FastLED/ci/demo_symbol_analysis.py diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/DockerManager.py b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/DockerManager.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/DockerManager.py rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/DockerManager.py diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32 b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32 similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32 rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32 diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32-lite b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32-lite similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32-lite rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32-lite diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/README.md b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/README.md rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/README.md diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/__init__.py b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/__init__.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/__init__.py rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/__init__.py diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/docker-compose.yml b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/docker-compose.yml similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/docker-compose.yml rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/docker-compose.yml diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/qemu_esp32_docker.py b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/qemu_esp32_docker.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/qemu_esp32_docker.py rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/qemu_esp32_docker.py diff --git a/v2.0/wrobot/lib/FastLED/ci/dockerfiles/qemu_test_integration.py b/v1.4/wrobot/lib/FastLED/ci/dockerfiles/qemu_test_integration.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/dockerfiles/qemu_test_integration.py rename to v1.4/wrobot/lib/FastLED/ci/dockerfiles/qemu_test_integration.py diff --git a/v2.0/wrobot/lib/FastLED/ci/docs.py b/v1.4/wrobot/lib/FastLED/ci/docs.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/docs.py rename to v1.4/wrobot/lib/FastLED/ci/docs.py diff --git a/v2.0/wrobot/lib/FastLED/ci/esp32_symbol_analysis_runner.py b/v1.4/wrobot/lib/FastLED/ci/esp32_symbol_analysis_runner.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/esp32_symbol_analysis_runner.py rename to v1.4/wrobot/lib/FastLED/ci/esp32_symbol_analysis_runner.py diff --git a/v2.0/wrobot/lib/FastLED/ci/examples/fingerprint_cache_demo.py b/v1.4/wrobot/lib/FastLED/ci/examples/fingerprint_cache_demo.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/examples/fingerprint_cache_demo.py rename to v1.4/wrobot/lib/FastLED/ci/examples/fingerprint_cache_demo.py diff --git a/v2.0/wrobot/lib/FastLED/ci/inspect_binary.py b/v1.4/wrobot/lib/FastLED/ci/inspect_binary.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/inspect_binary.py rename to v1.4/wrobot/lib/FastLED/ci/inspect_binary.py diff --git a/v2.0/wrobot/lib/FastLED/ci/inspect_elf.py b/v1.4/wrobot/lib/FastLED/ci/inspect_elf.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/inspect_elf.py rename to v1.4/wrobot/lib/FastLED/ci/inspect_elf.py diff --git a/v2.0/wrobot/lib/FastLED/ci/inspect_obj.py b/v1.4/wrobot/lib/FastLED/ci/inspect_obj.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/inspect_obj.py rename to v1.4/wrobot/lib/FastLED/ci/inspect_obj.py diff --git a/v2.0/wrobot/lib/FastLED/ci/install-qemu.py b/v1.4/wrobot/lib/FastLED/ci/install-qemu.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/install-qemu.py rename to v1.4/wrobot/lib/FastLED/ci/install-qemu.py diff --git a/v2.0/wrobot/lib/FastLED/ci/install_qemu_esp32.py b/v1.4/wrobot/lib/FastLED/ci/install_qemu_esp32.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/install_qemu_esp32.py rename to v1.4/wrobot/lib/FastLED/ci/install_qemu_esp32.py diff --git a/v2.0/wrobot/lib/FastLED/ci/iwyu/fastled.imp b/v1.4/wrobot/lib/FastLED/ci/iwyu/fastled.imp similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/iwyu/fastled.imp rename to v1.4/wrobot/lib/FastLED/ci/iwyu/fastled.imp diff --git a/v2.0/wrobot/lib/FastLED/ci/iwyu/stdlib.imp b/v1.4/wrobot/lib/FastLED/ci/iwyu/stdlib.imp similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/iwyu/stdlib.imp rename to v1.4/wrobot/lib/FastLED/ci/iwyu/stdlib.imp diff --git a/v2.0/wrobot/lib/FastLED/ci/js/README.md b/v1.4/wrobot/lib/FastLED/ci/js/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/js/README.md rename to v1.4/wrobot/lib/FastLED/ci/js/README.md diff --git a/v2.0/wrobot/lib/FastLED/ci/kitchensink/kitchensink.ino.cpp b/v1.4/wrobot/lib/FastLED/ci/kitchensink/kitchensink.ino.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/kitchensink/kitchensink.ino.cpp rename to v1.4/wrobot/lib/FastLED/ci/kitchensink/kitchensink.ino.cpp diff --git a/v2.0/wrobot/lib/FastLED/ci/kitchensink/platformio.ini b/v1.4/wrobot/lib/FastLED/ci/kitchensink/platformio.ini similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/kitchensink/platformio.ini rename to v1.4/wrobot/lib/FastLED/ci/kitchensink/platformio.ini diff --git a/v2.0/wrobot/lib/FastLED/ci/lint-js-fast b/v1.4/wrobot/lib/FastLED/ci/lint-js-fast similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/lint-js-fast rename to v1.4/wrobot/lib/FastLED/ci/lint-js-fast diff --git a/v2.0/wrobot/lib/FastLED/ci/lint-js-fast.backup b/v1.4/wrobot/lib/FastLED/ci/lint-js-fast.backup similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/lint-js-fast.backup rename to v1.4/wrobot/lib/FastLED/ci/lint-js-fast.backup diff --git a/v2.0/wrobot/lib/FastLED/ci/migration_example.py b/v1.4/wrobot/lib/FastLED/ci/migration_example.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/migration_example.py rename to v1.4/wrobot/lib/FastLED/ci/migration_example.py diff --git a/v2.0/wrobot/lib/FastLED/ci/native/platformio.ini b/v1.4/wrobot/lib/FastLED/ci/native/platformio.ini similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/native/platformio.ini rename to v1.4/wrobot/lib/FastLED/ci/native/platformio.ini diff --git a/v2.0/wrobot/lib/FastLED/ci/optimization_report.py b/v1.4/wrobot/lib/FastLED/ci/optimization_report.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/optimization_report.py rename to v1.4/wrobot/lib/FastLED/ci/optimization_report.py diff --git a/v2.0/wrobot/lib/FastLED/ci/pyright-cached.py b/v1.4/wrobot/lib/FastLED/ci/pyright-cached.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/pyright-cached.py rename to v1.4/wrobot/lib/FastLED/ci/pyright-cached.py diff --git a/v2.0/wrobot/lib/FastLED/ci/qemu-esp32.py b/v1.4/wrobot/lib/FastLED/ci/qemu-esp32.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/qemu-esp32.py rename to v1.4/wrobot/lib/FastLED/ci/qemu-esp32.py diff --git a/v2.0/wrobot/lib/FastLED/ci/run-clang-format.py b/v1.4/wrobot/lib/FastLED/ci/run-clang-format.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/run-clang-format.py rename to v1.4/wrobot/lib/FastLED/ci/run-clang-format.py diff --git a/v2.0/wrobot/lib/FastLED/ci/run_test_wasm_local_compile.py b/v1.4/wrobot/lib/FastLED/ci/run_test_wasm_local_compile.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/run_test_wasm_local_compile.py rename to v1.4/wrobot/lib/FastLED/ci/run_test_wasm_local_compile.py diff --git a/v2.0/wrobot/lib/FastLED/ci/run_tests.py b/v1.4/wrobot/lib/FastLED/ci/run_tests.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/run_tests.py rename to v1.4/wrobot/lib/FastLED/ci/run_tests.py diff --git a/v2.0/wrobot/lib/FastLED/ci/setup-js-linting-fast.py b/v1.4/wrobot/lib/FastLED/ci/setup-js-linting-fast.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/setup-js-linting-fast.py rename to v1.4/wrobot/lib/FastLED/ci/setup-js-linting-fast.py diff --git a/v2.0/wrobot/lib/FastLED/ci/setup-llvm.py b/v1.4/wrobot/lib/FastLED/ci/setup-llvm.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/setup-llvm.py rename to v1.4/wrobot/lib/FastLED/ci/setup-llvm.py diff --git a/v2.0/wrobot/lib/FastLED/ci/symbol_analysis_runner.py b/v1.4/wrobot/lib/FastLED/ci/symbol_analysis_runner.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/symbol_analysis_runner.py rename to v1.4/wrobot/lib/FastLED/ci/symbol_analysis_runner.py diff --git a/v2.0/wrobot/lib/FastLED/ci/test_integration/test_archive_creation.py b/v1.4/wrobot/lib/FastLED/ci/test_integration/test_archive_creation.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/test_integration/test_archive_creation.py rename to v1.4/wrobot/lib/FastLED/ci/test_integration/test_archive_creation.py diff --git a/v2.0/wrobot/lib/FastLED/ci/test_integration/test_xcache.py b/v1.4/wrobot/lib/FastLED/ci/test_integration/test_xcache.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/test_integration/test_xcache.py rename to v1.4/wrobot/lib/FastLED/ci/test_integration/test_xcache.py diff --git a/v2.0/wrobot/lib/FastLED/ci/test_integration/test_xcache_standalone.py b/v1.4/wrobot/lib/FastLED/ci/test_integration/test_xcache_standalone.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/test_integration/test_xcache_standalone.py rename to v1.4/wrobot/lib/FastLED/ci/test_integration/test_xcache_standalone.py diff --git a/v2.0/wrobot/lib/FastLED/ci/test_unified_build.py b/v1.4/wrobot/lib/FastLED/ci/test_unified_build.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/test_unified_build.py rename to v1.4/wrobot/lib/FastLED/ci/test_unified_build.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/check_namespace_includes.py b/v1.4/wrobot/lib/FastLED/ci/tests/check_namespace_includes.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/check_namespace_includes.py rename to v1.4/wrobot/lib/FastLED/ci/tests/check_namespace_includes.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/no_using_namespace_fl_in_headers.py b/v1.4/wrobot/lib/FastLED/ci/tests/no_using_namespace_fl_in_headers.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/no_using_namespace_fl_in_headers.py rename to v1.4/wrobot/lib/FastLED/ci/tests/no_using_namespace_fl_in_headers.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_bin_2_elf.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_bin_2_elf.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_bin_2_elf.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_bin_2_elf.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_board_to_platformio_ini.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_board_to_platformio_ini.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_board_to_platformio_ini.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_board_to_platformio_ini.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_build_flags_toml.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_build_flags_toml.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_build_flags_toml.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_build_flags_toml.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_buildflags.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_buildflags.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_buildflags.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_buildflags.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_elf.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_elf.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_elf.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_elf.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_fingerprint_cache.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_fingerprint_cache.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_fingerprint_cache.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_fingerprint_cache.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_github_actions_security.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_github_actions_security.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_github_actions_security.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_github_actions_security.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_map_parser.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_map_parser.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_map_parser.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_map_parser.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_missing_pragma_once.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_missing_pragma_once.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_missing_pragma_once.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_missing_pragma_once.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_no_banned_headers.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_no_banned_headers.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_no_banned_headers.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_no_banned_headers.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_no_include_after_namespace.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_no_include_after_namespace.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_no_include_after_namespace.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_no_include_after_namespace.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_package.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_package.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_package.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_package.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_platformio_ini_class.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_platformio_ini_class.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_platformio_ini_class.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_platformio_ini_class.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_platformio_url_resolution.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_platformio_url_resolution.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_platformio_url_resolution.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_platformio_url_resolution.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_real_platformio_cache.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_real_platformio_cache.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_real_platformio_cache.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_real_platformio_cache.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_running_process.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_running_process.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_running_process.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_running_process.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_symbol_analysis.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_symbol_analysis.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_symbol_analysis.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_symbol_analysis.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_unity_generation.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_unity_generation.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_unity_generation.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_unity_generation.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_wasm_clang_format.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_wasm_clang_format.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_wasm_clang_format.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_wasm_clang_format.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/test_wrong_defines.py b/v1.4/wrobot/lib/FastLED/ci/tests/test_wrong_defines.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/test_wrong_defines.py rename to v1.4/wrobot/lib/FastLED/ci/tests/test_wrong_defines.py diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/uno/firmware.elf b/v1.4/wrobot/lib/FastLED/ci/tests/uno/firmware.elf similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/uno/firmware.elf rename to v1.4/wrobot/lib/FastLED/ci/tests/uno/firmware.elf diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/uno/firmware.hex b/v1.4/wrobot/lib/FastLED/ci/tests/uno/firmware.hex similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/uno/firmware.hex rename to v1.4/wrobot/lib/FastLED/ci/tests/uno/firmware.hex diff --git a/v2.0/wrobot/lib/FastLED/ci/tests/uno/firmware.map b/v1.4/wrobot/lib/FastLED/ci/tests/uno/firmware.map similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/tests/uno/firmware.map rename to v1.4/wrobot/lib/FastLED/ci/tests/uno/firmware.map diff --git a/v2.0/wrobot/lib/FastLED/ci/util/__init__.py b/v1.4/wrobot/lib/FastLED/ci/util/__init__.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/__init__.py rename to v1.4/wrobot/lib/FastLED/ci/util/__init__.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/bin_2_elf.py b/v1.4/wrobot/lib/FastLED/ci/util/bin_2_elf.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/bin_2_elf.py rename to v1.4/wrobot/lib/FastLED/ci/util/bin_2_elf.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/build_info_analyzer.py b/v1.4/wrobot/lib/FastLED/ci/util/build_info_analyzer.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/build_info_analyzer.py rename to v1.4/wrobot/lib/FastLED/ci/util/build_info_analyzer.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/cached_compiler.py b/v1.4/wrobot/lib/FastLED/ci/util/cached_compiler.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/cached_compiler.py rename to v1.4/wrobot/lib/FastLED/ci/util/cached_compiler.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/ccache_config.py b/v1.4/wrobot/lib/FastLED/ci/util/ccache_config.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/ccache_config.py rename to v1.4/wrobot/lib/FastLED/ci/util/ccache_config.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/check_files.py b/v1.4/wrobot/lib/FastLED/ci/util/check_files.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/check_files.py rename to v1.4/wrobot/lib/FastLED/ci/util/check_files.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/check_implementation_files.py.disabled b/v1.4/wrobot/lib/FastLED/ci/util/check_implementation_files.py.disabled similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/check_implementation_files.py.disabled rename to v1.4/wrobot/lib/FastLED/ci/util/check_implementation_files.py.disabled diff --git a/v2.0/wrobot/lib/FastLED/ci/util/concurrent_run.py b/v1.4/wrobot/lib/FastLED/ci/util/concurrent_run.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/concurrent_run.py rename to v1.4/wrobot/lib/FastLED/ci/util/concurrent_run.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/console_utf8.py b/v1.4/wrobot/lib/FastLED/ci/util/console_utf8.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/console_utf8.py rename to v1.4/wrobot/lib/FastLED/ci/util/console_utf8.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/cpu_count.py b/v1.4/wrobot/lib/FastLED/ci/util/cpu_count.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/cpu_count.py rename to v1.4/wrobot/lib/FastLED/ci/util/cpu_count.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/create_build_dir.py b/v1.4/wrobot/lib/FastLED/ci/util/create_build_dir.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/create_build_dir.py rename to v1.4/wrobot/lib/FastLED/ci/util/create_build_dir.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/elf.py b/v1.4/wrobot/lib/FastLED/ci/util/elf.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/elf.py rename to v1.4/wrobot/lib/FastLED/ci/util/elf.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/esp32_symbol_analysis.py b/v1.4/wrobot/lib/FastLED/ci/util/esp32_symbol_analysis.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/esp32_symbol_analysis.py rename to v1.4/wrobot/lib/FastLED/ci/util/esp32_symbol_analysis.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/locked_print.py b/v1.4/wrobot/lib/FastLED/ci/util/locked_print.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/locked_print.py rename to v1.4/wrobot/lib/FastLED/ci/util/locked_print.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/map_dump.py b/v1.4/wrobot/lib/FastLED/ci/util/map_dump.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/map_dump.py rename to v1.4/wrobot/lib/FastLED/ci/util/map_dump.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/output_formatter.py b/v1.4/wrobot/lib/FastLED/ci/util/output_formatter.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/output_formatter.py rename to v1.4/wrobot/lib/FastLED/ci/util/output_formatter.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/paths.py b/v1.4/wrobot/lib/FastLED/ci/util/paths.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/paths.py rename to v1.4/wrobot/lib/FastLED/ci/util/paths.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/process_status_display.py b/v1.4/wrobot/lib/FastLED/ci/util/process_status_display.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/process_status_display.py rename to v1.4/wrobot/lib/FastLED/ci/util/process_status_display.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/resumable_downloader.py b/v1.4/wrobot/lib/FastLED/ci/util/resumable_downloader.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/resumable_downloader.py rename to v1.4/wrobot/lib/FastLED/ci/util/resumable_downloader.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/running_process.py b/v1.4/wrobot/lib/FastLED/ci/util/running_process.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/running_process.py rename to v1.4/wrobot/lib/FastLED/ci/util/running_process.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/running_process_group.py b/v1.4/wrobot/lib/FastLED/ci/util/running_process_group.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/running_process_group.py rename to v1.4/wrobot/lib/FastLED/ci/util/running_process_group.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/running_process_manager.py b/v1.4/wrobot/lib/FastLED/ci/util/running_process_manager.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/running_process_manager.py rename to v1.4/wrobot/lib/FastLED/ci/util/running_process_manager.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/sccache_config.py b/v1.4/wrobot/lib/FastLED/ci/util/sccache_config.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/sccache_config.py rename to v1.4/wrobot/lib/FastLED/ci/util/sccache_config.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/scrapers/README.md b/v1.4/wrobot/lib/FastLED/ci/util/scrapers/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/scrapers/README.md rename to v1.4/wrobot/lib/FastLED/ci/util/scrapers/README.md diff --git a/v2.0/wrobot/lib/FastLED/ci/util/scrapers/fastled_python_investigation.md b/v1.4/wrobot/lib/FastLED/ci/util/scrapers/fastled_python_investigation.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/scrapers/fastled_python_investigation.md rename to v1.4/wrobot/lib/FastLED/ci/util/scrapers/fastled_python_investigation.md diff --git a/v2.0/wrobot/lib/FastLED/ci/util/scrapers/run_fastled_scraper.py b/v1.4/wrobot/lib/FastLED/ci/util/scrapers/run_fastled_scraper.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/scrapers/run_fastled_scraper.py rename to v1.4/wrobot/lib/FastLED/ci/util/scrapers/run_fastled_scraper.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/scrapers/scrape_festival_stick.py b/v1.4/wrobot/lib/FastLED/ci/util/scrapers/scrape_festival_stick.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/scrapers/scrape_festival_stick.py rename to v1.4/wrobot/lib/FastLED/ci/util/scrapers/scrape_festival_stick.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/symbol_analysis.py b/v1.4/wrobot/lib/FastLED/ci/util/symbol_analysis.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/symbol_analysis.py rename to v1.4/wrobot/lib/FastLED/ci/util/symbol_analysis.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/test_args.py b/v1.4/wrobot/lib/FastLED/ci/util/test_args.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/test_args.py rename to v1.4/wrobot/lib/FastLED/ci/util/test_args.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/test_commands.py b/v1.4/wrobot/lib/FastLED/ci/util/test_commands.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/test_commands.py rename to v1.4/wrobot/lib/FastLED/ci/util/test_commands.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/test_env.py b/v1.4/wrobot/lib/FastLED/ci/util/test_env.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/test_env.py rename to v1.4/wrobot/lib/FastLED/ci/util/test_env.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/test_exceptions.py b/v1.4/wrobot/lib/FastLED/ci/util/test_exceptions.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/test_exceptions.py rename to v1.4/wrobot/lib/FastLED/ci/util/test_exceptions.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/test_runner.py b/v1.4/wrobot/lib/FastLED/ci/util/test_runner.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/test_runner.py rename to v1.4/wrobot/lib/FastLED/ci/util/test_runner.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/test_types.py b/v1.4/wrobot/lib/FastLED/ci/util/test_types.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/test_types.py rename to v1.4/wrobot/lib/FastLED/ci/util/test_types.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/tools.py b/v1.4/wrobot/lib/FastLED/ci/util/tools.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/tools.py rename to v1.4/wrobot/lib/FastLED/ci/util/tools.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/url_utils.py b/v1.4/wrobot/lib/FastLED/ci/util/url_utils.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/url_utils.py rename to v1.4/wrobot/lib/FastLED/ci/util/url_utils.py diff --git a/v2.0/wrobot/lib/FastLED/ci/util/xcache.py b/v1.4/wrobot/lib/FastLED/ci/util/xcache.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/util/xcache.py rename to v1.4/wrobot/lib/FastLED/ci/util/xcache.py diff --git a/v2.0/wrobot/lib/FastLED/ci/wasm_compile.py b/v1.4/wrobot/lib/FastLED/ci/wasm_compile.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/wasm_compile.py rename to v1.4/wrobot/lib/FastLED/ci/wasm_compile.py diff --git a/v2.0/wrobot/lib/FastLED/ci/wasm_test.py b/v1.4/wrobot/lib/FastLED/ci/wasm_test.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/ci/wasm_test.py rename to v1.4/wrobot/lib/FastLED/ci/wasm_test.py diff --git a/v2.0/wrobot/lib/FastLED/clean b/v1.4/wrobot/lib/FastLED/clean similarity index 100% rename from v2.0/wrobot/lib/FastLED/clean rename to v1.4/wrobot/lib/FastLED/clean diff --git a/v2.0/wrobot/lib/FastLED/code_of_conduct.md b/v1.4/wrobot/lib/FastLED/code_of_conduct.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/code_of_conduct.md rename to v1.4/wrobot/lib/FastLED/code_of_conduct.md diff --git a/v2.0/wrobot/lib/FastLED/compile b/v1.4/wrobot/lib/FastLED/compile similarity index 100% rename from v2.0/wrobot/lib/FastLED/compile rename to v1.4/wrobot/lib/FastLED/compile diff --git a/v2.0/wrobot/lib/FastLED/compile.bat b/v1.4/wrobot/lib/FastLED/compile.bat similarity index 100% rename from v2.0/wrobot/lib/FastLED/compile.bat rename to v1.4/wrobot/lib/FastLED/compile.bat diff --git a/v2.0/wrobot/lib/FastLED/component.mk b/v1.4/wrobot/lib/FastLED/component.mk similarity index 100% rename from v2.0/wrobot/lib/FastLED/component.mk rename to v1.4/wrobot/lib/FastLED/component.mk diff --git a/v2.0/wrobot/lib/FastLED/cool_projects.md b/v1.4/wrobot/lib/FastLED/cool_projects.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/cool_projects.md rename to v1.4/wrobot/lib/FastLED/cool_projects.md diff --git a/v2.0/wrobot/lib/FastLED/debug_test.html b/v1.4/wrobot/lib/FastLED/debug_test.html similarity index 100% rename from v2.0/wrobot/lib/FastLED/debug_test.html rename to v1.4/wrobot/lib/FastLED/debug_test.html diff --git a/v2.0/wrobot/lib/FastLED/dev.sh b/v1.4/wrobot/lib/FastLED/dev.sh similarity index 100% rename from v2.0/wrobot/lib/FastLED/dev.sh rename to v1.4/wrobot/lib/FastLED/dev.sh diff --git a/v2.0/wrobot/lib/FastLED/dev/dev.ino b/v1.4/wrobot/lib/FastLED/dev/dev.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/dev/dev.ino rename to v1.4/wrobot/lib/FastLED/dev/dev.ino diff --git a/v2.0/wrobot/lib/FastLED/docs/Doxyfile b/v1.4/wrobot/lib/FastLED/docs/Doxyfile similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/Doxyfile rename to v1.4/wrobot/lib/FastLED/docs/Doxyfile diff --git a/v2.0/wrobot/lib/FastLED/docs/DoxygenLayout.xml b/v1.4/wrobot/lib/FastLED/docs/DoxygenLayout.xml similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/DoxygenLayout.xml rename to v1.4/wrobot/lib/FastLED/docs/DoxygenLayout.xml diff --git a/v2.0/wrobot/lib/FastLED/docs/include/fastled-docs.css b/v1.4/wrobot/lib/FastLED/docs/include/fastled-docs.css similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/include/fastled-docs.css rename to v1.4/wrobot/lib/FastLED/docs/include/fastled-docs.css diff --git a/v2.0/wrobot/lib/FastLED/docs/include/fastled_logo.png b/v1.4/wrobot/lib/FastLED/docs/include/fastled_logo.png similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/include/fastled_logo.png rename to v1.4/wrobot/lib/FastLED/docs/include/fastled_logo.png diff --git a/v2.0/wrobot/lib/FastLED/docs/include/footer.html b/v1.4/wrobot/lib/FastLED/docs/include/footer.html similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/include/footer.html rename to v1.4/wrobot/lib/FastLED/docs/include/footer.html diff --git a/v2.0/wrobot/lib/FastLED/docs/include/header.html b/v1.4/wrobot/lib/FastLED/docs/include/header.html similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/include/header.html rename to v1.4/wrobot/lib/FastLED/docs/include/header.html diff --git a/v2.0/wrobot/lib/FastLED/docs/javascript-linting.md b/v1.4/wrobot/lib/FastLED/docs/javascript-linting.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/javascript-linting.md rename to v1.4/wrobot/lib/FastLED/docs/javascript-linting.md diff --git a/v2.0/wrobot/lib/FastLED/docs/style.css b/v1.4/wrobot/lib/FastLED/docs/style.css similarity index 100% rename from v2.0/wrobot/lib/FastLED/docs/style.css rename to v1.4/wrobot/lib/FastLED/docs/style.css diff --git a/v2.0/wrobot/lib/FastLED/examples/AGENTS.md b/v1.4/wrobot/lib/FastLED/examples/AGENTS.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/AGENTS.md rename to v1.4/wrobot/lib/FastLED/examples/AGENTS.md diff --git a/v2.0/wrobot/lib/FastLED/examples/AdafruitBridge/AdafruitBridge.ino b/v1.4/wrobot/lib/FastLED/examples/AdafruitBridge/AdafruitBridge.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/AdafruitBridge/AdafruitBridge.ino rename to v1.4/wrobot/lib/FastLED/examples/AdafruitBridge/AdafruitBridge.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/AnalogOutput/AnalogOutput.ino b/v1.4/wrobot/lib/FastLED/examples/AnalogOutput/AnalogOutput.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/AnalogOutput/AnalogOutput.ino rename to v1.4/wrobot/lib/FastLED/examples/AnalogOutput/AnalogOutput.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/AnalogOutput/compat.h b/v1.4/wrobot/lib/FastLED/examples/AnalogOutput/compat.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/AnalogOutput/compat.h rename to v1.4/wrobot/lib/FastLED/examples/AnalogOutput/compat.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Animartrix/Animartrix.ino b/v1.4/wrobot/lib/FastLED/examples/Animartrix/Animartrix.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Animartrix/Animartrix.ino rename to v1.4/wrobot/lib/FastLED/examples/Animartrix/Animartrix.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Apa102/Apa102.ino b/v1.4/wrobot/lib/FastLED/examples/Apa102/Apa102.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Apa102/Apa102.ino rename to v1.4/wrobot/lib/FastLED/examples/Apa102/Apa102.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Apa102HD/Apa102HD.ino b/v1.4/wrobot/lib/FastLED/examples/Apa102HD/Apa102HD.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Apa102HD/Apa102HD.ino rename to v1.4/wrobot/lib/FastLED/examples/Apa102HD/Apa102HD.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Apa102HDOverride/Apa102HDOverride.ino b/v1.4/wrobot/lib/FastLED/examples/Apa102HDOverride/Apa102HDOverride.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Apa102HDOverride/Apa102HDOverride.ino rename to v1.4/wrobot/lib/FastLED/examples/Apa102HDOverride/Apa102HDOverride.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Async/Async.h b/v1.4/wrobot/lib/FastLED/examples/Async/Async.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Async/Async.h rename to v1.4/wrobot/lib/FastLED/examples/Async/Async.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Async/Async.ino b/v1.4/wrobot/lib/FastLED/examples/Async/Async.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Async/Async.ino rename to v1.4/wrobot/lib/FastLED/examples/Async/Async.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Audio/Audio.ino b/v1.4/wrobot/lib/FastLED/examples/Audio/Audio.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Audio/Audio.ino rename to v1.4/wrobot/lib/FastLED/examples/Audio/Audio.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Audio/advanced/README.md b/v1.4/wrobot/lib/FastLED/examples/Audio/advanced/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Audio/advanced/README.md rename to v1.4/wrobot/lib/FastLED/examples/Audio/advanced/README.md diff --git a/v2.0/wrobot/lib/FastLED/examples/Audio/advanced/advanced.h b/v1.4/wrobot/lib/FastLED/examples/Audio/advanced/advanced.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Audio/advanced/advanced.h rename to v1.4/wrobot/lib/FastLED/examples/Audio/advanced/advanced.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Audio/simple/README.md b/v1.4/wrobot/lib/FastLED/examples/Audio/simple/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Audio/simple/README.md rename to v1.4/wrobot/lib/FastLED/examples/Audio/simple/README.md diff --git a/v2.0/wrobot/lib/FastLED/examples/Audio/simple/fx_audio.h b/v1.4/wrobot/lib/FastLED/examples/Audio/simple/fx_audio.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Audio/simple/fx_audio.h rename to v1.4/wrobot/lib/FastLED/examples/Audio/simple/fx_audio.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Audio/simple/simple.h b/v1.4/wrobot/lib/FastLED/examples/Audio/simple/simple.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Audio/simple/simple.h rename to v1.4/wrobot/lib/FastLED/examples/Audio/simple/simple.h diff --git a/v2.0/wrobot/lib/FastLED/examples/AudioInput/AudioInput.h b/v1.4/wrobot/lib/FastLED/examples/AudioInput/AudioInput.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/AudioInput/AudioInput.h rename to v1.4/wrobot/lib/FastLED/examples/AudioInput/AudioInput.h diff --git a/v2.0/wrobot/lib/FastLED/examples/AudioInput/AudioInput.ino b/v1.4/wrobot/lib/FastLED/examples/AudioInput/AudioInput.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/AudioInput/AudioInput.ino rename to v1.4/wrobot/lib/FastLED/examples/AudioInput/AudioInput.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Blink/Blink.ino b/v1.4/wrobot/lib/FastLED/examples/Blink/Blink.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Blink/Blink.ino rename to v1.4/wrobot/lib/FastLED/examples/Blink/Blink.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/BlinkParallel/BlinkParallel.ino b/v1.4/wrobot/lib/FastLED/examples/BlinkParallel/BlinkParallel.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/BlinkParallel/BlinkParallel.ino rename to v1.4/wrobot/lib/FastLED/examples/BlinkParallel/BlinkParallel.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Blur/Blur.ino b/v1.4/wrobot/lib/FastLED/examples/Blur/Blur.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Blur/Blur.ino rename to v1.4/wrobot/lib/FastLED/examples/Blur/Blur.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Blur2d/Blur2d.ino b/v1.4/wrobot/lib/FastLED/examples/Blur2d/Blur2d.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Blur2d/Blur2d.ino rename to v1.4/wrobot/lib/FastLED/examples/Blur2d/Blur2d.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/Chromancer.ino b/v1.4/wrobot/lib/FastLED/examples/Chromancer/Chromancer.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/Chromancer.ino rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/Chromancer.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/detail.h b/v1.4/wrobot/lib/FastLED/examples/Chromancer/detail.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/detail.h rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/detail.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/gary_woos_wled_ledmap.h b/v1.4/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/gary_woos_wled_ledmap.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/gary_woos_wled_ledmap.h rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/gary_woos_wled_ledmap.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.json b/v1.4/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.json rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.json diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.min.json b/v1.4/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.min.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.min.json rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/gary_woos_wled_port/presets.min.json diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/gen.py b/v1.4/wrobot/lib/FastLED/examples/Chromancer/gen.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/gen.py rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/gen.py diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/mapping.h b/v1.4/wrobot/lib/FastLED/examples/Chromancer/mapping.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/mapping.h rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/mapping.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/output.json b/v1.4/wrobot/lib/FastLED/examples/Chromancer/output.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/output.json rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/output.json diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/ripple.h b/v1.4/wrobot/lib/FastLED/examples/Chromancer/ripple.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/ripple.h rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/ripple.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Chromancer/screenmap.json.h b/v1.4/wrobot/lib/FastLED/examples/Chromancer/screenmap.json.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Chromancer/screenmap.json.h rename to v1.4/wrobot/lib/FastLED/examples/Chromancer/screenmap.json.h diff --git a/v2.0/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.h b/v1.4/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.h rename to v1.4/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.h diff --git a/v2.0/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.ino b/v1.4/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.ino rename to v1.4/wrobot/lib/FastLED/examples/ColorBoost/ColorBoost.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/ColorPalette/ColorPalette.ino b/v1.4/wrobot/lib/FastLED/examples/ColorPalette/ColorPalette.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/ColorPalette/ColorPalette.ino rename to v1.4/wrobot/lib/FastLED/examples/ColorPalette/ColorPalette.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/ColorTemperature/ColorTemperature.ino b/v1.4/wrobot/lib/FastLED/examples/ColorTemperature/ColorTemperature.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/ColorTemperature/ColorTemperature.ino rename to v1.4/wrobot/lib/FastLED/examples/ColorTemperature/ColorTemperature.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/CompileTest/apollo3_test.h b/v1.4/wrobot/lib/FastLED/examples/CompileTest/apollo3_test.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/CompileTest/apollo3_test.h rename to v1.4/wrobot/lib/FastLED/examples/CompileTest/apollo3_test.h diff --git a/v2.0/wrobot/lib/FastLED/examples/CompileTest/arm_test.h b/v1.4/wrobot/lib/FastLED/examples/CompileTest/arm_test.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/CompileTest/arm_test.h rename to v1.4/wrobot/lib/FastLED/examples/CompileTest/arm_test.h diff --git a/v2.0/wrobot/lib/FastLED/examples/CompileTest/avr_test.h b/v1.4/wrobot/lib/FastLED/examples/CompileTest/avr_test.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/CompileTest/avr_test.h rename to v1.4/wrobot/lib/FastLED/examples/CompileTest/avr_test.h diff --git a/v2.0/wrobot/lib/FastLED/examples/CompileTest/wasm_test.h b/v1.4/wrobot/lib/FastLED/examples/CompileTest/wasm_test.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/CompileTest/wasm_test.h rename to v1.4/wrobot/lib/FastLED/examples/CompileTest/wasm_test.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.h b/v1.4/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.h rename to v1.4/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.ino b/v1.4/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.ino rename to v1.4/wrobot/lib/FastLED/examples/Corkscrew/Corkscrew.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Cylon/Cylon.ino b/v1.4/wrobot/lib/FastLED/examples/Cylon/Cylon.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Cylon/Cylon.ino rename to v1.4/wrobot/lib/FastLED/examples/Cylon/Cylon.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/DemoReel100/DemoReel100.ino b/v1.4/wrobot/lib/FastLED/examples/DemoReel100/DemoReel100.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/DemoReel100/DemoReel100.ino rename to v1.4/wrobot/lib/FastLED/examples/DemoReel100/DemoReel100.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Downscale/Downscale.h b/v1.4/wrobot/lib/FastLED/examples/Downscale/Downscale.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Downscale/Downscale.h rename to v1.4/wrobot/lib/FastLED/examples/Downscale/Downscale.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Downscale/Downscale.ino b/v1.4/wrobot/lib/FastLED/examples/Downscale/Downscale.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Downscale/Downscale.ino rename to v1.4/wrobot/lib/FastLED/examples/Downscale/Downscale.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Downscale/src/wave.cpp b/v1.4/wrobot/lib/FastLED/examples/Downscale/src/wave.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Downscale/src/wave.cpp rename to v1.4/wrobot/lib/FastLED/examples/Downscale/src/wave.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/Downscale/src/wave.h b/v1.4/wrobot/lib/FastLED/examples/Downscale/src/wave.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Downscale/src/wave.h rename to v1.4/wrobot/lib/FastLED/examples/Downscale/src/wave.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Downscale/src/xypaths.cpp b/v1.4/wrobot/lib/FastLED/examples/Downscale/src/xypaths.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Downscale/src/xypaths.cpp rename to v1.4/wrobot/lib/FastLED/examples/Downscale/src/xypaths.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/Downscale/src/xypaths.h b/v1.4/wrobot/lib/FastLED/examples/Downscale/src/xypaths.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Downscale/src/xypaths.h rename to v1.4/wrobot/lib/FastLED/examples/Downscale/src/xypaths.h diff --git a/v2.0/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.h b/v1.4/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.h rename to v1.4/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.h diff --git a/v2.0/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.ino b/v1.4/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.ino rename to v1.4/wrobot/lib/FastLED/examples/EaseInOut/EaseInOut.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.h b/v1.4/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.h rename to v1.4/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.ino b/v1.4/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.ino rename to v1.4/wrobot/lib/FastLED/examples/Esp32S3I2SDemo/Esp32S3I2SDemo.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.h b/v1.4/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.h rename to v1.4/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.h diff --git a/v2.0/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.ino b/v1.4/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.ino rename to v1.4/wrobot/lib/FastLED/examples/EspI2SDemo/EspI2SDemo.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FestivalStick/FestivalStick.ino b/v1.4/wrobot/lib/FastLED/examples/FestivalStick/FestivalStick.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FestivalStick/FestivalStick.ino rename to v1.4/wrobot/lib/FastLED/examples/FestivalStick/FestivalStick.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FestivalStick/curr.h b/v1.4/wrobot/lib/FastLED/examples/FestivalStick/curr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FestivalStick/curr.h rename to v1.4/wrobot/lib/FastLED/examples/FestivalStick/curr.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FestivalStick/old.h b/v1.4/wrobot/lib/FastLED/examples/FestivalStick/old.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FestivalStick/old.h rename to v1.4/wrobot/lib/FastLED/examples/FestivalStick/old.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Fire2012/Fire2012.ino b/v1.4/wrobot/lib/FastLED/examples/Fire2012/Fire2012.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Fire2012/Fire2012.ino rename to v1.4/wrobot/lib/FastLED/examples/Fire2012/Fire2012.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Fire2012WithPalette/Fire2012WithPalette.ino b/v1.4/wrobot/lib/FastLED/examples/Fire2012WithPalette/Fire2012WithPalette.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Fire2012WithPalette/Fire2012WithPalette.ino rename to v1.4/wrobot/lib/FastLED/examples/Fire2012WithPalette/Fire2012WithPalette.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Fire2023/Fire2023.h b/v1.4/wrobot/lib/FastLED/examples/Fire2023/Fire2023.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Fire2023/Fire2023.h rename to v1.4/wrobot/lib/FastLED/examples/Fire2023/Fire2023.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Fire2023/Fire2023.ino b/v1.4/wrobot/lib/FastLED/examples/Fire2023/Fire2023.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Fire2023/Fire2023.ino rename to v1.4/wrobot/lib/FastLED/examples/Fire2023/Fire2023.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.h b/v1.4/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.h rename to v1.4/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.ino b/v1.4/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.ino rename to v1.4/wrobot/lib/FastLED/examples/FireCylinder/FireCylinder.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.h b/v1.4/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.h rename to v1.4/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.ino b/v1.4/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.ino rename to v1.4/wrobot/lib/FastLED/examples/FireMatrix/FireMatrix.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FirstLight/FirstLight.ino b/v1.4/wrobot/lib/FastLED/examples/FirstLight/FirstLight.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FirstLight/FirstLight.ino rename to v1.4/wrobot/lib/FastLED/examples/FirstLight/FirstLight.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxCylon/FxCylon.ino b/v1.4/wrobot/lib/FastLED/examples/FxCylon/FxCylon.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxCylon/FxCylon.ino rename to v1.4/wrobot/lib/FastLED/examples/FxCylon/FxCylon.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxDemoReel100/FxDemoReel100.ino b/v1.4/wrobot/lib/FastLED/examples/FxDemoReel100/FxDemoReel100.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxDemoReel100/FxDemoReel100.ino rename to v1.4/wrobot/lib/FastLED/examples/FxDemoReel100/FxDemoReel100.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxDemoReel100/defs.h b/v1.4/wrobot/lib/FastLED/examples/FxDemoReel100/defs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxDemoReel100/defs.h rename to v1.4/wrobot/lib/FastLED/examples/FxDemoReel100/defs.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FxEngine/FxEngine.ino b/v1.4/wrobot/lib/FastLED/examples/FxEngine/FxEngine.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxEngine/FxEngine.ino rename to v1.4/wrobot/lib/FastLED/examples/FxEngine/FxEngine.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxFire2012/FxFire2012.ino b/v1.4/wrobot/lib/FastLED/examples/FxFire2012/FxFire2012.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxFire2012/FxFire2012.ino rename to v1.4/wrobot/lib/FastLED/examples/FxFire2012/FxFire2012.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxGfx2Video/FxGfx2Video.ino b/v1.4/wrobot/lib/FastLED/examples/FxGfx2Video/FxGfx2Video.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxGfx2Video/FxGfx2Video.ino rename to v1.4/wrobot/lib/FastLED/examples/FxGfx2Video/FxGfx2Video.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxNoisePlusPalette/FxNoisePlusPalette.ino b/v1.4/wrobot/lib/FastLED/examples/FxNoisePlusPalette/FxNoisePlusPalette.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxNoisePlusPalette/FxNoisePlusPalette.ino rename to v1.4/wrobot/lib/FastLED/examples/FxNoisePlusPalette/FxNoisePlusPalette.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.h b/v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.h rename to v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.ino b/v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.ino rename to v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/FxNoiseRing.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/NOISE_RING_IDEAS.md b/v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/NOISE_RING_IDEAS.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/NOISE_RING_IDEAS.md rename to v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/NOISE_RING_IDEAS.md diff --git a/v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/simple_timer.h b/v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/simple_timer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxNoiseRing/simple_timer.h rename to v1.4/wrobot/lib/FastLED/examples/FxNoiseRing/simple_timer.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FxPacifica/FxPacifica.ino b/v1.4/wrobot/lib/FastLED/examples/FxPacifica/FxPacifica.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxPacifica/FxPacifica.ino rename to v1.4/wrobot/lib/FastLED/examples/FxPacifica/FxPacifica.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxPacifica/defs.h b/v1.4/wrobot/lib/FastLED/examples/FxPacifica/defs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxPacifica/defs.h rename to v1.4/wrobot/lib/FastLED/examples/FxPacifica/defs.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FxPride2015/FxPride2015.ino b/v1.4/wrobot/lib/FastLED/examples/FxPride2015/FxPride2015.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxPride2015/FxPride2015.ino rename to v1.4/wrobot/lib/FastLED/examples/FxPride2015/FxPride2015.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxSdCard/FxSdCard.ino b/v1.4/wrobot/lib/FastLED/examples/FxSdCard/FxSdCard.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxSdCard/FxSdCard.ino rename to v1.4/wrobot/lib/FastLED/examples/FxSdCard/FxSdCard.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/color_line_bubbles.rgb b/v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/color_line_bubbles.rgb similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/color_line_bubbles.rgb rename to v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/color_line_bubbles.rgb diff --git a/v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/readme.txt b/v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/readme.txt similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/readme.txt rename to v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/readme.txt diff --git a/v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/screenmap.json b/v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/screenmap.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/screenmap.json rename to v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/screenmap.json diff --git a/v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/video.rgb b/v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/video.rgb similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxSdCard/data/video.rgb rename to v1.4/wrobot/lib/FastLED/examples/FxSdCard/data/video.rgb diff --git a/v2.0/wrobot/lib/FastLED/examples/FxTwinkleFox/FxTwinkleFox.ino b/v1.4/wrobot/lib/FastLED/examples/FxTwinkleFox/FxTwinkleFox.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxTwinkleFox/FxTwinkleFox.ino rename to v1.4/wrobot/lib/FastLED/examples/FxTwinkleFox/FxTwinkleFox.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxWater/FxWater.h b/v1.4/wrobot/lib/FastLED/examples/FxWater/FxWater.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxWater/FxWater.h rename to v1.4/wrobot/lib/FastLED/examples/FxWater/FxWater.h diff --git a/v2.0/wrobot/lib/FastLED/examples/FxWater/FxWater.ino b/v1.4/wrobot/lib/FastLED/examples/FxWater/FxWater.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxWater/FxWater.ino rename to v1.4/wrobot/lib/FastLED/examples/FxWater/FxWater.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxWave2d/FxWave2d.ino b/v1.4/wrobot/lib/FastLED/examples/FxWave2d/FxWave2d.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxWave2d/FxWave2d.ino rename to v1.4/wrobot/lib/FastLED/examples/FxWave2d/FxWave2d.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/FxWave2d/wavefx.cpp b/v1.4/wrobot/lib/FastLED/examples/FxWave2d/wavefx.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxWave2d/wavefx.cpp rename to v1.4/wrobot/lib/FastLED/examples/FxWave2d/wavefx.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/FxWave2d/wavefx.h b/v1.4/wrobot/lib/FastLED/examples/FxWave2d/wavefx.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/FxWave2d/wavefx.h rename to v1.4/wrobot/lib/FastLED/examples/FxWave2d/wavefx.h diff --git a/v2.0/wrobot/lib/FastLED/examples/HD107/HD107.ino b/v1.4/wrobot/lib/FastLED/examples/HD107/HD107.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/HD107/HD107.ino rename to v1.4/wrobot/lib/FastLED/examples/HD107/HD107.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/HSVTest/HSVTest.h b/v1.4/wrobot/lib/FastLED/examples/HSVTest/HSVTest.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/HSVTest/HSVTest.h rename to v1.4/wrobot/lib/FastLED/examples/HSVTest/HSVTest.h diff --git a/v2.0/wrobot/lib/FastLED/examples/HSVTest/HSVTest.ino b/v1.4/wrobot/lib/FastLED/examples/HSVTest/HSVTest.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/HSVTest/HSVTest.ino rename to v1.4/wrobot/lib/FastLED/examples/HSVTest/HSVTest.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Json/Json.ino b/v1.4/wrobot/lib/FastLED/examples/Json/Json.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Json/Json.ino rename to v1.4/wrobot/lib/FastLED/examples/Json/Json.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Json/JsonSketch.h b/v1.4/wrobot/lib/FastLED/examples/Json/JsonSketch.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Json/JsonSketch.h rename to v1.4/wrobot/lib/FastLED/examples/Json/JsonSketch.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/LuminescentGrand.ino b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/LuminescentGrand.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/LuminescentGrand.ino rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/LuminescentGrand.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/LedRopeTCL.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.hpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.hpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/MIDI.hpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/buttons.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/buttons.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/buttons.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/buttons.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Defs.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Defs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Defs.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Defs.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Message.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Message.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Message.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Message.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Namespace.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Namespace.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Namespace.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Namespace.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Platform.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Platform.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Platform.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Platform.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Settings.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Settings.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Settings.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/midi_Settings.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/serialMIDI.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/serialMIDI.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/serialMIDI.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/serialMIDI.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/arduino/ui_state.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/ApproximatingFunction.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/ApproximatingFunction.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/ApproximatingFunction.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/ApproximatingFunction.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Keyboard.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/Painter.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/color_mapper.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/defs.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/defs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/defs.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/defs.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/dprint.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/framebuffer.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_layout_array.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_rope_interface.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_rope_interface.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_rope_interface.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/led_rope_interface.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/settings.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/settings.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/settings.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/settings.h diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.cpp b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.cpp rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.h b/v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.h rename to v1.4/wrobot/lib/FastLED/examples/LuminescentGrand/shared/util.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Luminova/Luminova.h b/v1.4/wrobot/lib/FastLED/examples/Luminova/Luminova.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Luminova/Luminova.h rename to v1.4/wrobot/lib/FastLED/examples/Luminova/Luminova.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Luminova/Luminova.ino b/v1.4/wrobot/lib/FastLED/examples/Luminova/Luminova.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Luminova/Luminova.ino rename to v1.4/wrobot/lib/FastLED/examples/Luminova/Luminova.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/ArrayOfLedArrays/ArrayOfLedArrays.ino b/v1.4/wrobot/lib/FastLED/examples/Multiple/ArrayOfLedArrays/ArrayOfLedArrays.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/ArrayOfLedArrays/ArrayOfLedArrays.ino rename to v1.4/wrobot/lib/FastLED/examples/Multiple/ArrayOfLedArrays/ArrayOfLedArrays.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/MirroringSample/MirroringSample.ino b/v1.4/wrobot/lib/FastLED/examples/Multiple/MirroringSample/MirroringSample.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/MirroringSample/MirroringSample.ino rename to v1.4/wrobot/lib/FastLED/examples/Multiple/MirroringSample/MirroringSample.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/MultiArrays/MultiArrays.ino b/v1.4/wrobot/lib/FastLED/examples/Multiple/MultiArrays/MultiArrays.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/MultiArrays/MultiArrays.ino rename to v1.4/wrobot/lib/FastLED/examples/Multiple/MultiArrays/MultiArrays.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/MultipleStripsInOneArray/MultipleStripsInOneArray.ino b/v1.4/wrobot/lib/FastLED/examples/Multiple/MultipleStripsInOneArray/MultipleStripsInOneArray.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/MultipleStripsInOneArray/MultipleStripsInOneArray.ino rename to v1.4/wrobot/lib/FastLED/examples/Multiple/MultipleStripsInOneArray/MultipleStripsInOneArray.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.h b/v1.4/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.h rename to v1.4/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.ino b/v1.4/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.ino rename to v1.4/wrobot/lib/FastLED/examples/Multiple/OctoWS2811Demo/OctoWS2811Demo.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.h b/v1.4/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.h rename to v1.4/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.ino b/v1.4/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.ino rename to v1.4/wrobot/lib/FastLED/examples/Multiple/ParallelOutputDemo/ParallelOutputDemo.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/NetTest/NetTest.ino b/v1.4/wrobot/lib/FastLED/examples/NetTest/NetTest.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/NetTest/NetTest.ino rename to v1.4/wrobot/lib/FastLED/examples/NetTest/NetTest.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/NetTest/NetTestReal.h b/v1.4/wrobot/lib/FastLED/examples/NetTest/NetTestReal.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/NetTest/NetTestReal.h rename to v1.4/wrobot/lib/FastLED/examples/NetTest/NetTestReal.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Noise/Noise.ino b/v1.4/wrobot/lib/FastLED/examples/Noise/Noise.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Noise/Noise.ino rename to v1.4/wrobot/lib/FastLED/examples/Noise/Noise.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.h b/v1.4/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.h rename to v1.4/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.h diff --git a/v2.0/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.ino b/v1.4/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.ino rename to v1.4/wrobot/lib/FastLED/examples/NoisePlayground/NoisePlayground.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.h b/v1.4/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.h rename to v1.4/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.h diff --git a/v2.0/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.ino b/v1.4/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.ino rename to v1.4/wrobot/lib/FastLED/examples/NoisePlusPalette/NoisePlusPalette.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811.ino b/v1.4/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811.ino rename to v1.4/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811_impl.h b/v1.4/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811_impl.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811_impl.h rename to v1.4/wrobot/lib/FastLED/examples/OctoWS2811/OctoWS2811_impl.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Overclock/Overclock.ino b/v1.4/wrobot/lib/FastLED/examples/Overclock/Overclock.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Overclock/Overclock.ino rename to v1.4/wrobot/lib/FastLED/examples/Overclock/Overclock.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Pacifica/Pacifica.ino b/v1.4/wrobot/lib/FastLED/examples/Pacifica/Pacifica.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Pacifica/Pacifica.ino rename to v1.4/wrobot/lib/FastLED/examples/Pacifica/Pacifica.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/PinMode/PinMode.ino b/v1.4/wrobot/lib/FastLED/examples/PinMode/PinMode.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/PinMode/PinMode.ino rename to v1.4/wrobot/lib/FastLED/examples/PinMode/PinMode.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Pintest/Pintest.h b/v1.4/wrobot/lib/FastLED/examples/Pintest/Pintest.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Pintest/Pintest.h rename to v1.4/wrobot/lib/FastLED/examples/Pintest/Pintest.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Pintest/Pintest.ino b/v1.4/wrobot/lib/FastLED/examples/Pintest/Pintest.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Pintest/Pintest.ino rename to v1.4/wrobot/lib/FastLED/examples/Pintest/Pintest.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.h b/v1.4/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.h rename to v1.4/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.ino b/v1.4/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.ino rename to v1.4/wrobot/lib/FastLED/examples/Ports/PJRCSpectrumAnalyzer/PJRCSpectrumAnalyzer.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Pride2015/Pride2015.ino b/v1.4/wrobot/lib/FastLED/examples/Pride2015/Pride2015.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Pride2015/Pride2015.ino rename to v1.4/wrobot/lib/FastLED/examples/Pride2015/Pride2015.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/README.md b/v1.4/wrobot/lib/FastLED/examples/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/README.md rename to v1.4/wrobot/lib/FastLED/examples/README.md diff --git a/v2.0/wrobot/lib/FastLED/examples/RGBCalibrate/RGBCalibrate.ino b/v1.4/wrobot/lib/FastLED/examples/RGBCalibrate/RGBCalibrate.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/RGBCalibrate/RGBCalibrate.ino rename to v1.4/wrobot/lib/FastLED/examples/RGBCalibrate/RGBCalibrate.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/RGBSetDemo/RGBSetDemo.ino b/v1.4/wrobot/lib/FastLED/examples/RGBSetDemo/RGBSetDemo.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/RGBSetDemo/RGBSetDemo.ino rename to v1.4/wrobot/lib/FastLED/examples/RGBSetDemo/RGBSetDemo.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/RGBW/RGBW.ino b/v1.4/wrobot/lib/FastLED/examples/RGBW/RGBW.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/RGBW/RGBW.ino rename to v1.4/wrobot/lib/FastLED/examples/RGBW/RGBW.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/RGBWEmulated/RGBWEmulated.ino b/v1.4/wrobot/lib/FastLED/examples/RGBWEmulated/RGBWEmulated.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/RGBWEmulated/RGBWEmulated.ino rename to v1.4/wrobot/lib/FastLED/examples/RGBWEmulated/RGBWEmulated.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrix.ino b/v1.4/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrix.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrix.ino rename to v1.4/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrix.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrixSketch.h b/v1.4/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrixSketch.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrixSketch.h rename to v1.4/wrobot/lib/FastLED/examples/SmartMatrix/SmartMatrixSketch.h diff --git a/v2.0/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.h b/v1.4/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.h rename to v1.4/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.h diff --git a/v2.0/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.ino b/v1.4/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.ino rename to v1.4/wrobot/lib/FastLED/examples/TeensyMassiveParallel/TeensyMassiveParallel.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/TeensyParallel/TeensyParallel.ino b/v1.4/wrobot/lib/FastLED/examples/TeensyParallel/TeensyParallel.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/TeensyParallel/TeensyParallel.ino rename to v1.4/wrobot/lib/FastLED/examples/TeensyParallel/TeensyParallel.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/TwinkleFox/TwinkleFox.ino b/v1.4/wrobot/lib/FastLED/examples/TwinkleFox/TwinkleFox.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/TwinkleFox/TwinkleFox.ino rename to v1.4/wrobot/lib/FastLED/examples/TwinkleFox/TwinkleFox.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/UITest/UITest.ino b/v1.4/wrobot/lib/FastLED/examples/UITest/UITest.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/UITest/UITest.ino rename to v1.4/wrobot/lib/FastLED/examples/UITest/UITest.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/WS2816/WS2816.ino b/v1.4/wrobot/lib/FastLED/examples/WS2816/WS2816.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/WS2816/WS2816.ino rename to v1.4/wrobot/lib/FastLED/examples/WS2816/WS2816.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/WasmScreenCoords/WasmScreenCoords.ino b/v1.4/wrobot/lib/FastLED/examples/WasmScreenCoords/WasmScreenCoords.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/WasmScreenCoords/WasmScreenCoords.ino rename to v1.4/wrobot/lib/FastLED/examples/WasmScreenCoords/WasmScreenCoords.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Wave/Wave.h b/v1.4/wrobot/lib/FastLED/examples/Wave/Wave.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Wave/Wave.h rename to v1.4/wrobot/lib/FastLED/examples/Wave/Wave.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Wave/Wave.ino b/v1.4/wrobot/lib/FastLED/examples/Wave/Wave.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Wave/Wave.ino rename to v1.4/wrobot/lib/FastLED/examples/Wave/Wave.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/Wave2d/Wave2d.h b/v1.4/wrobot/lib/FastLED/examples/Wave2d/Wave2d.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Wave2d/Wave2d.h rename to v1.4/wrobot/lib/FastLED/examples/Wave2d/Wave2d.h diff --git a/v2.0/wrobot/lib/FastLED/examples/Wave2d/Wave2d.ino b/v1.4/wrobot/lib/FastLED/examples/Wave2d/Wave2d.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/Wave2d/Wave2d.ino rename to v1.4/wrobot/lib/FastLED/examples/Wave2d/Wave2d.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/XYMatrix/XYMatrix.ino b/v1.4/wrobot/lib/FastLED/examples/XYMatrix/XYMatrix.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYMatrix/XYMatrix.ino rename to v1.4/wrobot/lib/FastLED/examples/XYMatrix/XYMatrix.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/XYPath.ino b/v1.4/wrobot/lib/FastLED/examples/XYPath/XYPath.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/XYPath.ino rename to v1.4/wrobot/lib/FastLED/examples/XYPath/XYPath.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/complex.h b/v1.4/wrobot/lib/FastLED/examples/XYPath/complex.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/complex.h rename to v1.4/wrobot/lib/FastLED/examples/XYPath/complex.h diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/direct.h b/v1.4/wrobot/lib/FastLED/examples/XYPath/direct.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/direct.h rename to v1.4/wrobot/lib/FastLED/examples/XYPath/direct.h diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/simple.h b/v1.4/wrobot/lib/FastLED/examples/XYPath/simple.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/simple.h rename to v1.4/wrobot/lib/FastLED/examples/XYPath/simple.h diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/src/wave.cpp b/v1.4/wrobot/lib/FastLED/examples/XYPath/src/wave.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/src/wave.cpp rename to v1.4/wrobot/lib/FastLED/examples/XYPath/src/wave.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/src/wave.h b/v1.4/wrobot/lib/FastLED/examples/XYPath/src/wave.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/src/wave.h rename to v1.4/wrobot/lib/FastLED/examples/XYPath/src/wave.h diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/src/xypaths.cpp b/v1.4/wrobot/lib/FastLED/examples/XYPath/src/xypaths.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/src/xypaths.cpp rename to v1.4/wrobot/lib/FastLED/examples/XYPath/src/xypaths.cpp diff --git a/v2.0/wrobot/lib/FastLED/examples/XYPath/src/xypaths.h b/v1.4/wrobot/lib/FastLED/examples/XYPath/src/xypaths.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/XYPath/src/xypaths.h rename to v1.4/wrobot/lib/FastLED/examples/XYPath/src/xypaths.h diff --git a/v2.0/wrobot/lib/FastLED/examples/wasm/wasm.ino b/v1.4/wrobot/lib/FastLED/examples/wasm/wasm.ino similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/wasm/wasm.ino rename to v1.4/wrobot/lib/FastLED/examples/wasm/wasm.ino diff --git a/v2.0/wrobot/lib/FastLED/examples/wasm/wasm_impl.h b/v1.4/wrobot/lib/FastLED/examples/wasm/wasm_impl.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/examples/wasm/wasm_impl.h rename to v1.4/wrobot/lib/FastLED/examples/wasm/wasm_impl.h diff --git a/v2.0/wrobot/lib/FastLED/extras/AppleII.s65 b/v1.4/wrobot/lib/FastLED/extras/AppleII.s65 similarity index 100% rename from v2.0/wrobot/lib/FastLED/extras/AppleII.s65 rename to v1.4/wrobot/lib/FastLED/extras/AppleII.s65 diff --git a/v2.0/wrobot/lib/FastLED/extras/FastLED6502.s65 b/v1.4/wrobot/lib/FastLED/extras/FastLED6502.s65 similarity index 100% rename from v2.0/wrobot/lib/FastLED/extras/FastLED6502.s65 rename to v1.4/wrobot/lib/FastLED/extras/FastLED6502.s65 diff --git a/v2.0/wrobot/lib/FastLED/extras/RainbowDemo.bin.zip b/v1.4/wrobot/lib/FastLED/extras/RainbowDemo.bin.zip similarity index 100% rename from v2.0/wrobot/lib/FastLED/extras/RainbowDemo.bin.zip rename to v1.4/wrobot/lib/FastLED/extras/RainbowDemo.bin.zip diff --git a/v2.0/wrobot/lib/FastLED/extras/RainbowDemo.s65 b/v1.4/wrobot/lib/FastLED/extras/RainbowDemo.s65 similarity index 100% rename from v2.0/wrobot/lib/FastLED/extras/RainbowDemo.s65 rename to v1.4/wrobot/lib/FastLED/extras/RainbowDemo.s65 diff --git a/v2.0/wrobot/lib/FastLED/fastled4.md b/v1.4/wrobot/lib/FastLED/fastled4.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/fastled4.md rename to v1.4/wrobot/lib/FastLED/fastled4.md diff --git a/v2.0/wrobot/lib/FastLED/generate_compile_commands.sh b/v1.4/wrobot/lib/FastLED/generate_compile_commands.sh similarity index 100% rename from v2.0/wrobot/lib/FastLED/generate_compile_commands.sh rename to v1.4/wrobot/lib/FastLED/generate_compile_commands.sh diff --git a/v2.0/wrobot/lib/FastLED/inspect b/v1.4/wrobot/lib/FastLED/inspect similarity index 100% rename from v2.0/wrobot/lib/FastLED/inspect rename to v1.4/wrobot/lib/FastLED/inspect diff --git a/v2.0/wrobot/lib/FastLED/install b/v1.4/wrobot/lib/FastLED/install similarity index 100% rename from v2.0/wrobot/lib/FastLED/install rename to v1.4/wrobot/lib/FastLED/install diff --git a/v2.0/wrobot/lib/FastLED/keywords.txt b/v1.4/wrobot/lib/FastLED/keywords.txt similarity index 100% rename from v2.0/wrobot/lib/FastLED/keywords.txt rename to v1.4/wrobot/lib/FastLED/keywords.txt diff --git a/v2.0/wrobot/lib/FastLED/legacy_index.js b/v1.4/wrobot/lib/FastLED/legacy_index.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/legacy_index.js rename to v1.4/wrobot/lib/FastLED/legacy_index.js diff --git a/v2.0/wrobot/lib/FastLED/library.json b/v1.4/wrobot/lib/FastLED/library.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/library.json rename to v1.4/wrobot/lib/FastLED/library.json diff --git a/v2.0/wrobot/lib/FastLED/library.properties b/v1.4/wrobot/lib/FastLED/library.properties similarity index 100% rename from v2.0/wrobot/lib/FastLED/library.properties rename to v1.4/wrobot/lib/FastLED/library.properties diff --git a/v2.0/wrobot/lib/FastLED/lint b/v1.4/wrobot/lib/FastLED/lint similarity index 100% rename from v2.0/wrobot/lib/FastLED/lint rename to v1.4/wrobot/lib/FastLED/lint diff --git a/v2.0/wrobot/lib/FastLED/mcp_server.py b/v1.4/wrobot/lib/FastLED/mcp_server.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/mcp_server.py rename to v1.4/wrobot/lib/FastLED/mcp_server.py diff --git a/v2.0/wrobot/lib/FastLED/platformio.ini b/v1.4/wrobot/lib/FastLED/platformio.ini similarity index 100% rename from v2.0/wrobot/lib/FastLED/platformio.ini rename to v1.4/wrobot/lib/FastLED/platformio.ini diff --git a/v2.0/wrobot/lib/FastLED/pyproject.toml b/v1.4/wrobot/lib/FastLED/pyproject.toml similarity index 100% rename from v2.0/wrobot/lib/FastLED/pyproject.toml rename to v1.4/wrobot/lib/FastLED/pyproject.toml diff --git a/v2.0/wrobot/lib/FastLED/release_notes.md b/v1.4/wrobot/lib/FastLED/release_notes.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/release_notes.md rename to v1.4/wrobot/lib/FastLED/release_notes.md diff --git a/v2.0/wrobot/lib/FastLED/scripts/all_source_build.py.disabled b/v1.4/wrobot/lib/FastLED/scripts/all_source_build.py.disabled similarity index 100% rename from v2.0/wrobot/lib/FastLED/scripts/all_source_build.py.disabled rename to v1.4/wrobot/lib/FastLED/scripts/all_source_build.py.disabled diff --git a/v2.0/wrobot/lib/FastLED/scripts/disable_all_source_build.py.disabled b/v1.4/wrobot/lib/FastLED/scripts/disable_all_source_build.py.disabled similarity index 100% rename from v2.0/wrobot/lib/FastLED/scripts/disable_all_source_build.py.disabled rename to v1.4/wrobot/lib/FastLED/scripts/disable_all_source_build.py.disabled diff --git a/v2.0/wrobot/lib/FastLED/scripts/enhance-js-typing.py b/v1.4/wrobot/lib/FastLED/scripts/enhance-js-typing.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/scripts/enhance-js-typing.py rename to v1.4/wrobot/lib/FastLED/scripts/enhance-js-typing.py diff --git a/v2.0/wrobot/lib/FastLED/src/CMakeLists.txt b/v1.4/wrobot/lib/FastLED/src/CMakeLists.txt similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/CMakeLists.txt rename to v1.4/wrobot/lib/FastLED/src/CMakeLists.txt diff --git a/v2.0/wrobot/lib/FastLED/src/FastLED.cpp b/v1.4/wrobot/lib/FastLED/src/FastLED.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/FastLED.cpp rename to v1.4/wrobot/lib/FastLED/src/FastLED.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/FastLED.h b/v1.4/wrobot/lib/FastLED/src/FastLED.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/FastLED.h rename to v1.4/wrobot/lib/FastLED/src/FastLED.h diff --git a/v2.0/wrobot/lib/FastLED/src/README.md b/v1.4/wrobot/lib/FastLED/src/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/README.md rename to v1.4/wrobot/lib/FastLED/src/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/_readme b/v1.4/wrobot/lib/FastLED/src/_readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/_readme rename to v1.4/wrobot/lib/FastLED/src/_readme diff --git a/v2.0/wrobot/lib/FastLED/src/bitswap.cpp b/v1.4/wrobot/lib/FastLED/src/bitswap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/bitswap.cpp rename to v1.4/wrobot/lib/FastLED/src/bitswap.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/bitswap.h b/v1.4/wrobot/lib/FastLED/src/bitswap.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/bitswap.h rename to v1.4/wrobot/lib/FastLED/src/bitswap.h diff --git a/v2.0/wrobot/lib/FastLED/src/chipsets.h b/v1.4/wrobot/lib/FastLED/src/chipsets.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/chipsets.h rename to v1.4/wrobot/lib/FastLED/src/chipsets.h diff --git a/v2.0/wrobot/lib/FastLED/src/chsv.h b/v1.4/wrobot/lib/FastLED/src/chsv.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/chsv.h rename to v1.4/wrobot/lib/FastLED/src/chsv.h diff --git a/v2.0/wrobot/lib/FastLED/src/cled_controller.cpp b/v1.4/wrobot/lib/FastLED/src/cled_controller.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/cled_controller.cpp rename to v1.4/wrobot/lib/FastLED/src/cled_controller.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/cled_controller.h b/v1.4/wrobot/lib/FastLED/src/cled_controller.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/cled_controller.h rename to v1.4/wrobot/lib/FastLED/src/cled_controller.h diff --git a/v2.0/wrobot/lib/FastLED/src/color.h b/v1.4/wrobot/lib/FastLED/src/color.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/color.h rename to v1.4/wrobot/lib/FastLED/src/color.h diff --git a/v2.0/wrobot/lib/FastLED/src/colorpalettes.cpp b/v1.4/wrobot/lib/FastLED/src/colorpalettes.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/colorpalettes.cpp rename to v1.4/wrobot/lib/FastLED/src/colorpalettes.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/colorpalettes.h b/v1.4/wrobot/lib/FastLED/src/colorpalettes.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/colorpalettes.h rename to v1.4/wrobot/lib/FastLED/src/colorpalettes.h diff --git a/v2.0/wrobot/lib/FastLED/src/colorutils.h b/v1.4/wrobot/lib/FastLED/src/colorutils.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/colorutils.h rename to v1.4/wrobot/lib/FastLED/src/colorutils.h diff --git a/v2.0/wrobot/lib/FastLED/src/controller.h b/v1.4/wrobot/lib/FastLED/src/controller.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/controller.h rename to v1.4/wrobot/lib/FastLED/src/controller.h diff --git a/v2.0/wrobot/lib/FastLED/src/cpixel_ledcontroller.h b/v1.4/wrobot/lib/FastLED/src/cpixel_ledcontroller.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/cpixel_ledcontroller.h rename to v1.4/wrobot/lib/FastLED/src/cpixel_ledcontroller.h diff --git a/v2.0/wrobot/lib/FastLED/src/cpp_compat.h b/v1.4/wrobot/lib/FastLED/src/cpp_compat.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/cpp_compat.h rename to v1.4/wrobot/lib/FastLED/src/cpp_compat.h diff --git a/v2.0/wrobot/lib/FastLED/src/crgb.cpp b/v1.4/wrobot/lib/FastLED/src/crgb.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/crgb.cpp rename to v1.4/wrobot/lib/FastLED/src/crgb.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/crgb.h b/v1.4/wrobot/lib/FastLED/src/crgb.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/crgb.h rename to v1.4/wrobot/lib/FastLED/src/crgb.h diff --git a/v2.0/wrobot/lib/FastLED/src/crgb.hpp b/v1.4/wrobot/lib/FastLED/src/crgb.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/crgb.hpp rename to v1.4/wrobot/lib/FastLED/src/crgb.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/dither_mode.h b/v1.4/wrobot/lib/FastLED/src/dither_mode.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/dither_mode.h rename to v1.4/wrobot/lib/FastLED/src/dither_mode.h diff --git a/v2.0/wrobot/lib/FastLED/src/dmx.h b/v1.4/wrobot/lib/FastLED/src/dmx.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/dmx.h rename to v1.4/wrobot/lib/FastLED/src/dmx.h diff --git a/v2.0/wrobot/lib/FastLED/src/eorder.h b/v1.4/wrobot/lib/FastLED/src/eorder.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/eorder.h rename to v1.4/wrobot/lib/FastLED/src/eorder.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastled_config.h b/v1.4/wrobot/lib/FastLED/src/fastled_config.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastled_config.h rename to v1.4/wrobot/lib/FastLED/src/fastled_config.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastled_delay.h b/v1.4/wrobot/lib/FastLED/src/fastled_delay.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastled_delay.h rename to v1.4/wrobot/lib/FastLED/src/fastled_delay.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastled_progmem.h b/v1.4/wrobot/lib/FastLED/src/fastled_progmem.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastled_progmem.h rename to v1.4/wrobot/lib/FastLED/src/fastled_progmem.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastpin.h b/v1.4/wrobot/lib/FastLED/src/fastpin.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastpin.h rename to v1.4/wrobot/lib/FastLED/src/fastpin.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastspi.h b/v1.4/wrobot/lib/FastLED/src/fastspi.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastspi.h rename to v1.4/wrobot/lib/FastLED/src/fastspi.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastspi_bitbang.h b/v1.4/wrobot/lib/FastLED/src/fastspi_bitbang.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastspi_bitbang.h rename to v1.4/wrobot/lib/FastLED/src/fastspi_bitbang.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastspi_dma.h b/v1.4/wrobot/lib/FastLED/src/fastspi_dma.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastspi_dma.h rename to v1.4/wrobot/lib/FastLED/src/fastspi_dma.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastspi_nop.h b/v1.4/wrobot/lib/FastLED/src/fastspi_nop.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastspi_nop.h rename to v1.4/wrobot/lib/FastLED/src/fastspi_nop.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastspi_ref.h b/v1.4/wrobot/lib/FastLED/src/fastspi_ref.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastspi_ref.h rename to v1.4/wrobot/lib/FastLED/src/fastspi_ref.h diff --git a/v2.0/wrobot/lib/FastLED/src/fastspi_types.h b/v1.4/wrobot/lib/FastLED/src/fastspi_types.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fastspi_types.h rename to v1.4/wrobot/lib/FastLED/src/fastspi_types.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/README.md b/v1.4/wrobot/lib/FastLED/src/fl/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/README.md rename to v1.4/wrobot/lib/FastLED/src/fl/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/fl/_readme b/v1.4/wrobot/lib/FastLED/src/fl/_readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/_readme rename to v1.4/wrobot/lib/FastLED/src/fl/_readme diff --git a/v2.0/wrobot/lib/FastLED/src/fl/algorithm.h b/v1.4/wrobot/lib/FastLED/src/fl/algorithm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/algorithm.h rename to v1.4/wrobot/lib/FastLED/src/fl/algorithm.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/align.h b/v1.4/wrobot/lib/FastLED/src/fl/align.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/align.h rename to v1.4/wrobot/lib/FastLED/src/fl/align.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/allocator.cpp b/v1.4/wrobot/lib/FastLED/src/fl/allocator.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/allocator.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/allocator.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/allocator.h b/v1.4/wrobot/lib/FastLED/src/fl/allocator.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/allocator.h rename to v1.4/wrobot/lib/FastLED/src/fl/allocator.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/array.h b/v1.4/wrobot/lib/FastLED/src/fl/array.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/array.h rename to v1.4/wrobot/lib/FastLED/src/fl/array.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/assert.h b/v1.4/wrobot/lib/FastLED/src/fl/assert.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/assert.h rename to v1.4/wrobot/lib/FastLED/src/fl/assert.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/async.cpp b/v1.4/wrobot/lib/FastLED/src/fl/async.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/async.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/async.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/async.h b/v1.4/wrobot/lib/FastLED/src/fl/async.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/async.h rename to v1.4/wrobot/lib/FastLED/src/fl/async.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/atomic.h b/v1.4/wrobot/lib/FastLED/src/fl/atomic.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/atomic.h rename to v1.4/wrobot/lib/FastLED/src/fl/atomic.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/audio.cpp b/v1.4/wrobot/lib/FastLED/src/fl/audio.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/audio.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/audio.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/audio.h b/v1.4/wrobot/lib/FastLED/src/fl/audio.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/audio.h rename to v1.4/wrobot/lib/FastLED/src/fl/audio.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/audio_input.cpp b/v1.4/wrobot/lib/FastLED/src/fl/audio_input.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/audio_input.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/audio_input.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/audio_input.h b/v1.4/wrobot/lib/FastLED/src/fl/audio_input.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/audio_input.h rename to v1.4/wrobot/lib/FastLED/src/fl/audio_input.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/audio_reactive.cpp b/v1.4/wrobot/lib/FastLED/src/fl/audio_reactive.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/audio_reactive.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/audio_reactive.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/audio_reactive.h b/v1.4/wrobot/lib/FastLED/src/fl/audio_reactive.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/audio_reactive.h rename to v1.4/wrobot/lib/FastLED/src/fl/audio_reactive.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/avr_disallowed.h b/v1.4/wrobot/lib/FastLED/src/fl/avr_disallowed.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/avr_disallowed.h rename to v1.4/wrobot/lib/FastLED/src/fl/avr_disallowed.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/bit_cast.h b/v1.4/wrobot/lib/FastLED/src/fl/bit_cast.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/bit_cast.h rename to v1.4/wrobot/lib/FastLED/src/fl/bit_cast.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/bitset.cpp b/v1.4/wrobot/lib/FastLED/src/fl/bitset.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/bitset.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/bitset.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/bitset.h b/v1.4/wrobot/lib/FastLED/src/fl/bitset.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/bitset.h rename to v1.4/wrobot/lib/FastLED/src/fl/bitset.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/bitset_dynamic.h b/v1.4/wrobot/lib/FastLED/src/fl/bitset_dynamic.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/bitset_dynamic.h rename to v1.4/wrobot/lib/FastLED/src/fl/bitset_dynamic.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/blur.cpp b/v1.4/wrobot/lib/FastLED/src/fl/blur.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/blur.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/blur.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/blur.h b/v1.4/wrobot/lib/FastLED/src/fl/blur.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/blur.h rename to v1.4/wrobot/lib/FastLED/src/fl/blur.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/bytestream.h b/v1.4/wrobot/lib/FastLED/src/fl/bytestream.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/bytestream.h rename to v1.4/wrobot/lib/FastLED/src/fl/bytestream.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/bytestreammemory.cpp b/v1.4/wrobot/lib/FastLED/src/fl/bytestreammemory.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/bytestreammemory.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/bytestreammemory.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/bytestreammemory.h b/v1.4/wrobot/lib/FastLED/src/fl/bytestreammemory.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/bytestreammemory.h rename to v1.4/wrobot/lib/FastLED/src/fl/bytestreammemory.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/circular_buffer.h b/v1.4/wrobot/lib/FastLED/src/fl/circular_buffer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/circular_buffer.h rename to v1.4/wrobot/lib/FastLED/src/fl/circular_buffer.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/clamp.h b/v1.4/wrobot/lib/FastLED/src/fl/clamp.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/clamp.h rename to v1.4/wrobot/lib/FastLED/src/fl/clamp.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/clear.h b/v1.4/wrobot/lib/FastLED/src/fl/clear.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/clear.h rename to v1.4/wrobot/lib/FastLED/src/fl/clear.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/colorutils.cpp b/v1.4/wrobot/lib/FastLED/src/fl/colorutils.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/colorutils.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/colorutils.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/colorutils.h b/v1.4/wrobot/lib/FastLED/src/fl/colorutils.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/colorutils.h rename to v1.4/wrobot/lib/FastLED/src/fl/colorutils.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/colorutils_misc.h b/v1.4/wrobot/lib/FastLED/src/fl/colorutils_misc.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/colorutils_misc.h rename to v1.4/wrobot/lib/FastLED/src/fl/colorutils_misc.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/comparators.h b/v1.4/wrobot/lib/FastLED/src/fl/comparators.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/comparators.h rename to v1.4/wrobot/lib/FastLED/src/fl/comparators.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/compiler_control.h b/v1.4/wrobot/lib/FastLED/src/fl/compiler_control.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/compiler_control.h rename to v1.4/wrobot/lib/FastLED/src/fl/compiler_control.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/convert.h b/v1.4/wrobot/lib/FastLED/src/fl/convert.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/convert.h rename to v1.4/wrobot/lib/FastLED/src/fl/convert.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/corkscrew.cpp b/v1.4/wrobot/lib/FastLED/src/fl/corkscrew.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/corkscrew.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/corkscrew.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/corkscrew.h b/v1.4/wrobot/lib/FastLED/src/fl/corkscrew.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/corkscrew.h rename to v1.4/wrobot/lib/FastLED/src/fl/corkscrew.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/crgb_hsv16.cpp b/v1.4/wrobot/lib/FastLED/src/fl/crgb_hsv16.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/crgb_hsv16.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/crgb_hsv16.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/cstddef.h b/v1.4/wrobot/lib/FastLED/src/fl/cstddef.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/cstddef.h rename to v1.4/wrobot/lib/FastLED/src/fl/cstddef.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/dbg.h b/v1.4/wrobot/lib/FastLED/src/fl/dbg.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/dbg.h rename to v1.4/wrobot/lib/FastLED/src/fl/dbg.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/deprecated.h b/v1.4/wrobot/lib/FastLED/src/fl/deprecated.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/deprecated.h rename to v1.4/wrobot/lib/FastLED/src/fl/deprecated.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/deque.h b/v1.4/wrobot/lib/FastLED/src/fl/deque.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/deque.h rename to v1.4/wrobot/lib/FastLED/src/fl/deque.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/dll.h b/v1.4/wrobot/lib/FastLED/src/fl/dll.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/dll.h rename to v1.4/wrobot/lib/FastLED/src/fl/dll.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/downscale.cpp b/v1.4/wrobot/lib/FastLED/src/fl/downscale.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/downscale.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/downscale.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/downscale.h b/v1.4/wrobot/lib/FastLED/src/fl/downscale.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/downscale.h rename to v1.4/wrobot/lib/FastLED/src/fl/downscale.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/draw_mode.h b/v1.4/wrobot/lib/FastLED/src/fl/draw_mode.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/draw_mode.h rename to v1.4/wrobot/lib/FastLED/src/fl/draw_mode.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/draw_visitor.h b/v1.4/wrobot/lib/FastLED/src/fl/draw_visitor.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/draw_visitor.h rename to v1.4/wrobot/lib/FastLED/src/fl/draw_visitor.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ease.cpp b/v1.4/wrobot/lib/FastLED/src/fl/ease.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ease.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/ease.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ease.h b/v1.4/wrobot/lib/FastLED/src/fl/ease.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ease.h rename to v1.4/wrobot/lib/FastLED/src/fl/ease.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/engine_events.cpp b/v1.4/wrobot/lib/FastLED/src/fl/engine_events.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/engine_events.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/engine_events.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/engine_events.h b/v1.4/wrobot/lib/FastLED/src/fl/engine_events.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/engine_events.h rename to v1.4/wrobot/lib/FastLED/src/fl/engine_events.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/eorder.h b/v1.4/wrobot/lib/FastLED/src/fl/eorder.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/eorder.h rename to v1.4/wrobot/lib/FastLED/src/fl/eorder.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/export.h b/v1.4/wrobot/lib/FastLED/src/fl/export.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/export.h rename to v1.4/wrobot/lib/FastLED/src/fl/export.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fetch.cpp b/v1.4/wrobot/lib/FastLED/src/fl/fetch.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fetch.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/fetch.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fetch.h b/v1.4/wrobot/lib/FastLED/src/fl/fetch.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fetch.h rename to v1.4/wrobot/lib/FastLED/src/fl/fetch.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fft.cpp b/v1.4/wrobot/lib/FastLED/src/fl/fft.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fft.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/fft.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fft.h b/v1.4/wrobot/lib/FastLED/src/fl/fft.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fft.h rename to v1.4/wrobot/lib/FastLED/src/fl/fft.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fft_impl.cpp b/v1.4/wrobot/lib/FastLED/src/fl/fft_impl.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fft_impl.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/fft_impl.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fft_impl.h b/v1.4/wrobot/lib/FastLED/src/fl/fft_impl.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fft_impl.h rename to v1.4/wrobot/lib/FastLED/src/fl/fft_impl.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/file_system.cpp b/v1.4/wrobot/lib/FastLED/src/fl/file_system.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/file_system.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/file_system.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/file_system.h b/v1.4/wrobot/lib/FastLED/src/fl/file_system.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/file_system.h rename to v1.4/wrobot/lib/FastLED/src/fl/file_system.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fill.cpp b/v1.4/wrobot/lib/FastLED/src/fl/fill.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fill.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/fill.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/fill.h b/v1.4/wrobot/lib/FastLED/src/fl/fill.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/fill.h rename to v1.4/wrobot/lib/FastLED/src/fl/fill.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/five_bit_hd_gamma.h b/v1.4/wrobot/lib/FastLED/src/fl/five_bit_hd_gamma.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/five_bit_hd_gamma.h rename to v1.4/wrobot/lib/FastLED/src/fl/five_bit_hd_gamma.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/force_inline.h b/v1.4/wrobot/lib/FastLED/src/fl/force_inline.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/force_inline.h rename to v1.4/wrobot/lib/FastLED/src/fl/force_inline.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/function.h b/v1.4/wrobot/lib/FastLED/src/fl/function.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/function.h rename to v1.4/wrobot/lib/FastLED/src/fl/function.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/function_list.h b/v1.4/wrobot/lib/FastLED/src/fl/function_list.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/function_list.h rename to v1.4/wrobot/lib/FastLED/src/fl/function_list.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/functional.h b/v1.4/wrobot/lib/FastLED/src/fl/functional.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/functional.h rename to v1.4/wrobot/lib/FastLED/src/fl/functional.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/gamma.cpp b/v1.4/wrobot/lib/FastLED/src/fl/gamma.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/gamma.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/gamma.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/gamma.h b/v1.4/wrobot/lib/FastLED/src/fl/gamma.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/gamma.h rename to v1.4/wrobot/lib/FastLED/src/fl/gamma.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/geometry.h b/v1.4/wrobot/lib/FastLED/src/fl/geometry.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/geometry.h rename to v1.4/wrobot/lib/FastLED/src/fl/geometry.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/gradient.cpp b/v1.4/wrobot/lib/FastLED/src/fl/gradient.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/gradient.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/gradient.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/gradient.h b/v1.4/wrobot/lib/FastLED/src/fl/gradient.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/gradient.h rename to v1.4/wrobot/lib/FastLED/src/fl/gradient.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/grid.h b/v1.4/wrobot/lib/FastLED/src/fl/grid.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/grid.h rename to v1.4/wrobot/lib/FastLED/src/fl/grid.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/has_include.h b/v1.4/wrobot/lib/FastLED/src/fl/has_include.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/has_include.h rename to v1.4/wrobot/lib/FastLED/src/fl/has_include.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/hash.h b/v1.4/wrobot/lib/FastLED/src/fl/hash.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/hash.h rename to v1.4/wrobot/lib/FastLED/src/fl/hash.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/hash_map.h b/v1.4/wrobot/lib/FastLED/src/fl/hash_map.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/hash_map.h rename to v1.4/wrobot/lib/FastLED/src/fl/hash_map.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/hash_map_lru.h b/v1.4/wrobot/lib/FastLED/src/fl/hash_map_lru.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/hash_map_lru.h rename to v1.4/wrobot/lib/FastLED/src/fl/hash_map_lru.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/hash_set.h b/v1.4/wrobot/lib/FastLED/src/fl/hash_set.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/hash_set.h rename to v1.4/wrobot/lib/FastLED/src/fl/hash_set.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/hsv.h b/v1.4/wrobot/lib/FastLED/src/fl/hsv.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/hsv.h rename to v1.4/wrobot/lib/FastLED/src/fl/hsv.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/hsv16.cpp b/v1.4/wrobot/lib/FastLED/src/fl/hsv16.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/hsv16.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/hsv16.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/hsv16.h b/v1.4/wrobot/lib/FastLED/src/fl/hsv16.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/hsv16.h rename to v1.4/wrobot/lib/FastLED/src/fl/hsv16.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/id_tracker.cpp b/v1.4/wrobot/lib/FastLED/src/fl/id_tracker.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/id_tracker.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/id_tracker.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/id_tracker.h b/v1.4/wrobot/lib/FastLED/src/fl/id_tracker.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/id_tracker.h rename to v1.4/wrobot/lib/FastLED/src/fl/id_tracker.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/initializer_list.h b/v1.4/wrobot/lib/FastLED/src/fl/initializer_list.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/initializer_list.h rename to v1.4/wrobot/lib/FastLED/src/fl/initializer_list.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/inplacenew.h b/v1.4/wrobot/lib/FastLED/src/fl/inplacenew.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/inplacenew.h rename to v1.4/wrobot/lib/FastLED/src/fl/inplacenew.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/insert_result.h b/v1.4/wrobot/lib/FastLED/src/fl/insert_result.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/insert_result.h rename to v1.4/wrobot/lib/FastLED/src/fl/insert_result.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/int.h b/v1.4/wrobot/lib/FastLED/src/fl/int.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/int.h rename to v1.4/wrobot/lib/FastLED/src/fl/int.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/io.cpp b/v1.4/wrobot/lib/FastLED/src/fl/io.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/io.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/io.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/io.h b/v1.4/wrobot/lib/FastLED/src/fl/io.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/io.h rename to v1.4/wrobot/lib/FastLED/src/fl/io.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/iostream.h b/v1.4/wrobot/lib/FastLED/src/fl/iostream.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/iostream.h rename to v1.4/wrobot/lib/FastLED/src/fl/iostream.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/istream.cpp b/v1.4/wrobot/lib/FastLED/src/fl/istream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/istream.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/istream.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/istream.h b/v1.4/wrobot/lib/FastLED/src/fl/istream.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/istream.h rename to v1.4/wrobot/lib/FastLED/src/fl/istream.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/json.cpp b/v1.4/wrobot/lib/FastLED/src/fl/json.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/json.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/json.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/json.h b/v1.4/wrobot/lib/FastLED/src/fl/json.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/json.h rename to v1.4/wrobot/lib/FastLED/src/fl/json.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/leds.cpp b/v1.4/wrobot/lib/FastLED/src/fl/leds.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/leds.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/leds.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/leds.h b/v1.4/wrobot/lib/FastLED/src/fl/leds.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/leds.h rename to v1.4/wrobot/lib/FastLED/src/fl/leds.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/line_simplification.cpp b/v1.4/wrobot/lib/FastLED/src/fl/line_simplification.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/line_simplification.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/line_simplification.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/line_simplification.h b/v1.4/wrobot/lib/FastLED/src/fl/line_simplification.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/line_simplification.h rename to v1.4/wrobot/lib/FastLED/src/fl/line_simplification.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/lut.h b/v1.4/wrobot/lib/FastLED/src/fl/lut.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/lut.h rename to v1.4/wrobot/lib/FastLED/src/fl/lut.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/map.h b/v1.4/wrobot/lib/FastLED/src/fl/map.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/map.h rename to v1.4/wrobot/lib/FastLED/src/fl/map.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/map_range.h b/v1.4/wrobot/lib/FastLED/src/fl/map_range.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/map_range.h rename to v1.4/wrobot/lib/FastLED/src/fl/map_range.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/math.h b/v1.4/wrobot/lib/FastLED/src/fl/math.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/math.h rename to v1.4/wrobot/lib/FastLED/src/fl/math.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/math_macros.h b/v1.4/wrobot/lib/FastLED/src/fl/math_macros.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/math_macros.h rename to v1.4/wrobot/lib/FastLED/src/fl/math_macros.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/memfill.h b/v1.4/wrobot/lib/FastLED/src/fl/memfill.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/memfill.h rename to v1.4/wrobot/lib/FastLED/src/fl/memfill.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/memory.h b/v1.4/wrobot/lib/FastLED/src/fl/memory.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/memory.h rename to v1.4/wrobot/lib/FastLED/src/fl/memory.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/move.h b/v1.4/wrobot/lib/FastLED/src/fl/move.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/move.h rename to v1.4/wrobot/lib/FastLED/src/fl/move.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/mutex.h b/v1.4/wrobot/lib/FastLED/src/fl/mutex.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/mutex.h rename to v1.4/wrobot/lib/FastLED/src/fl/mutex.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/namespace.h b/v1.4/wrobot/lib/FastLED/src/fl/namespace.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/namespace.h rename to v1.4/wrobot/lib/FastLED/src/fl/namespace.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/noise_woryley.cpp b/v1.4/wrobot/lib/FastLED/src/fl/noise_woryley.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/noise_woryley.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/noise_woryley.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/noise_woryley.h b/v1.4/wrobot/lib/FastLED/src/fl/noise_woryley.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/noise_woryley.h rename to v1.4/wrobot/lib/FastLED/src/fl/noise_woryley.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/optional.h b/v1.4/wrobot/lib/FastLED/src/fl/optional.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/optional.h rename to v1.4/wrobot/lib/FastLED/src/fl/optional.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ostream.cpp b/v1.4/wrobot/lib/FastLED/src/fl/ostream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ostream.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/ostream.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ostream.h b/v1.4/wrobot/lib/FastLED/src/fl/ostream.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ostream.h rename to v1.4/wrobot/lib/FastLED/src/fl/ostream.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/pair.h b/v1.4/wrobot/lib/FastLED/src/fl/pair.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/pair.h rename to v1.4/wrobot/lib/FastLED/src/fl/pair.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/point.h b/v1.4/wrobot/lib/FastLED/src/fl/point.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/point.h rename to v1.4/wrobot/lib/FastLED/src/fl/point.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/printf.h b/v1.4/wrobot/lib/FastLED/src/fl/printf.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/printf.h rename to v1.4/wrobot/lib/FastLED/src/fl/printf.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/priority_queue.h b/v1.4/wrobot/lib/FastLED/src/fl/priority_queue.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/priority_queue.h rename to v1.4/wrobot/lib/FastLED/src/fl/priority_queue.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/promise.h b/v1.4/wrobot/lib/FastLED/src/fl/promise.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/promise.h rename to v1.4/wrobot/lib/FastLED/src/fl/promise.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/promise_result.h b/v1.4/wrobot/lib/FastLED/src/fl/promise_result.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/promise_result.h rename to v1.4/wrobot/lib/FastLED/src/fl/promise_result.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ptr.cpp b/v1.4/wrobot/lib/FastLED/src/fl/ptr.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ptr.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/ptr.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ptr.h b/v1.4/wrobot/lib/FastLED/src/fl/ptr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ptr.h rename to v1.4/wrobot/lib/FastLED/src/fl/ptr.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ptr_impl.h b/v1.4/wrobot/lib/FastLED/src/fl/ptr_impl.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ptr_impl.h rename to v1.4/wrobot/lib/FastLED/src/fl/ptr_impl.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/queue.h b/v1.4/wrobot/lib/FastLED/src/fl/queue.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/queue.h rename to v1.4/wrobot/lib/FastLED/src/fl/queue.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/random.cpp b/v1.4/wrobot/lib/FastLED/src/fl/random.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/random.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/random.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/random.h b/v1.4/wrobot/lib/FastLED/src/fl/random.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/random.h rename to v1.4/wrobot/lib/FastLED/src/fl/random.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/range_access.h b/v1.4/wrobot/lib/FastLED/src/fl/range_access.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/range_access.h rename to v1.4/wrobot/lib/FastLED/src/fl/range_access.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/raster.h b/v1.4/wrobot/lib/FastLED/src/fl/raster.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/raster.h rename to v1.4/wrobot/lib/FastLED/src/fl/raster.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/raster_sparse.cpp b/v1.4/wrobot/lib/FastLED/src/fl/raster_sparse.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/raster_sparse.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/raster_sparse.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/raster_sparse.h b/v1.4/wrobot/lib/FastLED/src/fl/raster_sparse.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/raster_sparse.h rename to v1.4/wrobot/lib/FastLED/src/fl/raster_sparse.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/rbtree.h b/v1.4/wrobot/lib/FastLED/src/fl/rbtree.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/rbtree.h rename to v1.4/wrobot/lib/FastLED/src/fl/rbtree.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.cpp b/v1.4/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.h b/v1.4/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.h rename to v1.4/wrobot/lib/FastLED/src/fl/rectangular_draw_buffer.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/referent.cpp b/v1.4/wrobot/lib/FastLED/src/fl/referent.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/referent.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/referent.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/referent.h b/v1.4/wrobot/lib/FastLED/src/fl/referent.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/referent.h rename to v1.4/wrobot/lib/FastLED/src/fl/referent.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/register.h b/v1.4/wrobot/lib/FastLED/src/fl/register.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/register.h rename to v1.4/wrobot/lib/FastLED/src/fl/register.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/rgbw.cpp b/v1.4/wrobot/lib/FastLED/src/fl/rgbw.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/rgbw.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/rgbw.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/rgbw.h b/v1.4/wrobot/lib/FastLED/src/fl/rgbw.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/rgbw.h rename to v1.4/wrobot/lib/FastLED/src/fl/rgbw.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/scoped_array.h b/v1.4/wrobot/lib/FastLED/src/fl/scoped_array.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/scoped_array.h rename to v1.4/wrobot/lib/FastLED/src/fl/scoped_array.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/scoped_ptr.h b/v1.4/wrobot/lib/FastLED/src/fl/scoped_ptr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/scoped_ptr.h rename to v1.4/wrobot/lib/FastLED/src/fl/scoped_ptr.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/screenmap.cpp b/v1.4/wrobot/lib/FastLED/src/fl/screenmap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/screenmap.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/screenmap.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/screenmap.h b/v1.4/wrobot/lib/FastLED/src/fl/screenmap.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/screenmap.h rename to v1.4/wrobot/lib/FastLED/src/fl/screenmap.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/set.h b/v1.4/wrobot/lib/FastLED/src/fl/set.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/set.h rename to v1.4/wrobot/lib/FastLED/src/fl/set.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/shared_ptr.h b/v1.4/wrobot/lib/FastLED/src/fl/shared_ptr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/shared_ptr.h rename to v1.4/wrobot/lib/FastLED/src/fl/shared_ptr.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/sin32.cpp b/v1.4/wrobot/lib/FastLED/src/fl/sin32.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/sin32.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/sin32.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/sin32.h b/v1.4/wrobot/lib/FastLED/src/fl/sin32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/sin32.h rename to v1.4/wrobot/lib/FastLED/src/fl/sin32.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/singleton.h b/v1.4/wrobot/lib/FastLED/src/fl/singleton.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/singleton.h rename to v1.4/wrobot/lib/FastLED/src/fl/singleton.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/sketch_macros.h b/v1.4/wrobot/lib/FastLED/src/fl/sketch_macros.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/sketch_macros.h rename to v1.4/wrobot/lib/FastLED/src/fl/sketch_macros.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/slice.h b/v1.4/wrobot/lib/FastLED/src/fl/slice.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/slice.h rename to v1.4/wrobot/lib/FastLED/src/fl/slice.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/span.h b/v1.4/wrobot/lib/FastLED/src/fl/span.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/span.h rename to v1.4/wrobot/lib/FastLED/src/fl/span.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/splat.cpp b/v1.4/wrobot/lib/FastLED/src/fl/splat.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/splat.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/splat.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/splat.h b/v1.4/wrobot/lib/FastLED/src/fl/splat.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/splat.h rename to v1.4/wrobot/lib/FastLED/src/fl/splat.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/sstream.h b/v1.4/wrobot/lib/FastLED/src/fl/sstream.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/sstream.h rename to v1.4/wrobot/lib/FastLED/src/fl/sstream.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/stdint.h b/v1.4/wrobot/lib/FastLED/src/fl/stdint.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/stdint.h rename to v1.4/wrobot/lib/FastLED/src/fl/stdint.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/str.cpp b/v1.4/wrobot/lib/FastLED/src/fl/str.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/str.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/str.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/str.h b/v1.4/wrobot/lib/FastLED/src/fl/str.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/str.h rename to v1.4/wrobot/lib/FastLED/src/fl/str.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/str_ui.cpp b/v1.4/wrobot/lib/FastLED/src/fl/str_ui.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/str_ui.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/str_ui.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/string.h b/v1.4/wrobot/lib/FastLED/src/fl/string.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/string.h rename to v1.4/wrobot/lib/FastLED/src/fl/string.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/strstream.cpp b/v1.4/wrobot/lib/FastLED/src/fl/strstream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/strstream.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/strstream.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/strstream.h b/v1.4/wrobot/lib/FastLED/src/fl/strstream.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/strstream.h rename to v1.4/wrobot/lib/FastLED/src/fl/strstream.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/stub_main.cpp b/v1.4/wrobot/lib/FastLED/src/fl/stub_main.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/stub_main.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/stub_main.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/supersample.h b/v1.4/wrobot/lib/FastLED/src/fl/supersample.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/supersample.h rename to v1.4/wrobot/lib/FastLED/src/fl/supersample.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/task.cpp b/v1.4/wrobot/lib/FastLED/src/fl/task.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/task.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/task.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/task.h b/v1.4/wrobot/lib/FastLED/src/fl/task.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/task.h rename to v1.4/wrobot/lib/FastLED/src/fl/task.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/template_magic.h b/v1.4/wrobot/lib/FastLED/src/fl/template_magic.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/template_magic.h rename to v1.4/wrobot/lib/FastLED/src/fl/template_magic.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/thread.h b/v1.4/wrobot/lib/FastLED/src/fl/thread.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/thread.h rename to v1.4/wrobot/lib/FastLED/src/fl/thread.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/thread_local.h b/v1.4/wrobot/lib/FastLED/src/fl/thread_local.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/thread_local.h rename to v1.4/wrobot/lib/FastLED/src/fl/thread_local.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/tile2x2.cpp b/v1.4/wrobot/lib/FastLED/src/fl/tile2x2.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/tile2x2.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/tile2x2.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/tile2x2.h b/v1.4/wrobot/lib/FastLED/src/fl/tile2x2.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/tile2x2.h rename to v1.4/wrobot/lib/FastLED/src/fl/tile2x2.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/time.cpp b/v1.4/wrobot/lib/FastLED/src/fl/time.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/time.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/time.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/time.h b/v1.4/wrobot/lib/FastLED/src/fl/time.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/time.h rename to v1.4/wrobot/lib/FastLED/src/fl/time.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/time_alpha.cpp b/v1.4/wrobot/lib/FastLED/src/fl/time_alpha.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/time_alpha.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/time_alpha.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/time_alpha.h b/v1.4/wrobot/lib/FastLED/src/fl/time_alpha.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/time_alpha.h rename to v1.4/wrobot/lib/FastLED/src/fl/time_alpha.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/trace.h b/v1.4/wrobot/lib/FastLED/src/fl/trace.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/trace.h rename to v1.4/wrobot/lib/FastLED/src/fl/trace.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/transform.cpp b/v1.4/wrobot/lib/FastLED/src/fl/transform.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/transform.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/transform.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/transform.h b/v1.4/wrobot/lib/FastLED/src/fl/transform.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/transform.h rename to v1.4/wrobot/lib/FastLED/src/fl/transform.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/traverse_grid.h b/v1.4/wrobot/lib/FastLED/src/fl/traverse_grid.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/traverse_grid.h rename to v1.4/wrobot/lib/FastLED/src/fl/traverse_grid.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/tuple.h b/v1.4/wrobot/lib/FastLED/src/fl/tuple.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/tuple.h rename to v1.4/wrobot/lib/FastLED/src/fl/tuple.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/type_traits.cpp b/v1.4/wrobot/lib/FastLED/src/fl/type_traits.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/type_traits.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/type_traits.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/type_traits.h b/v1.4/wrobot/lib/FastLED/src/fl/type_traits.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/type_traits.h rename to v1.4/wrobot/lib/FastLED/src/fl/type_traits.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/types.h b/v1.4/wrobot/lib/FastLED/src/fl/types.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/types.h rename to v1.4/wrobot/lib/FastLED/src/fl/types.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ui.cpp b/v1.4/wrobot/lib/FastLED/src/fl/ui.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ui.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/ui.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ui.h b/v1.4/wrobot/lib/FastLED/src/fl/ui.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ui.h rename to v1.4/wrobot/lib/FastLED/src/fl/ui.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/ui_impl.h b/v1.4/wrobot/lib/FastLED/src/fl/ui_impl.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/ui_impl.h rename to v1.4/wrobot/lib/FastLED/src/fl/ui_impl.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/unique_ptr.h b/v1.4/wrobot/lib/FastLED/src/fl/unique_ptr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/unique_ptr.h rename to v1.4/wrobot/lib/FastLED/src/fl/unique_ptr.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/unordered_set.h b/v1.4/wrobot/lib/FastLED/src/fl/unordered_set.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/unordered_set.h rename to v1.4/wrobot/lib/FastLED/src/fl/unordered_set.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/unused.h b/v1.4/wrobot/lib/FastLED/src/fl/unused.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/unused.h rename to v1.4/wrobot/lib/FastLED/src/fl/unused.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/upscale.cpp b/v1.4/wrobot/lib/FastLED/src/fl/upscale.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/upscale.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/upscale.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/upscale.h b/v1.4/wrobot/lib/FastLED/src/fl/upscale.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/upscale.h rename to v1.4/wrobot/lib/FastLED/src/fl/upscale.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/utility.h b/v1.4/wrobot/lib/FastLED/src/fl/utility.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/utility.h rename to v1.4/wrobot/lib/FastLED/src/fl/utility.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/variant.h b/v1.4/wrobot/lib/FastLED/src/fl/variant.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/variant.h rename to v1.4/wrobot/lib/FastLED/src/fl/variant.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/vector.h b/v1.4/wrobot/lib/FastLED/src/fl/vector.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/vector.h rename to v1.4/wrobot/lib/FastLED/src/fl/vector.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/virtual_if_not_avr.h b/v1.4/wrobot/lib/FastLED/src/fl/virtual_if_not_avr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/virtual_if_not_avr.h rename to v1.4/wrobot/lib/FastLED/src/fl/virtual_if_not_avr.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/warn.h b/v1.4/wrobot/lib/FastLED/src/fl/warn.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/warn.h rename to v1.4/wrobot/lib/FastLED/src/fl/warn.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/wave_simulation.cpp b/v1.4/wrobot/lib/FastLED/src/fl/wave_simulation.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/wave_simulation.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/wave_simulation.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/wave_simulation.h b/v1.4/wrobot/lib/FastLED/src/fl/wave_simulation.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/wave_simulation.h rename to v1.4/wrobot/lib/FastLED/src/fl/wave_simulation.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/wave_simulation_real.cpp b/v1.4/wrobot/lib/FastLED/src/fl/wave_simulation_real.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/wave_simulation_real.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/wave_simulation_real.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/wave_simulation_real.h b/v1.4/wrobot/lib/FastLED/src/fl/wave_simulation_real.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/wave_simulation_real.h rename to v1.4/wrobot/lib/FastLED/src/fl/wave_simulation_real.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/weak_ptr.h b/v1.4/wrobot/lib/FastLED/src/fl/weak_ptr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/weak_ptr.h rename to v1.4/wrobot/lib/FastLED/src/fl/weak_ptr.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xmap.cpp b/v1.4/wrobot/lib/FastLED/src/fl/xmap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xmap.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/xmap.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xmap.h b/v1.4/wrobot/lib/FastLED/src/fl/xmap.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xmap.h rename to v1.4/wrobot/lib/FastLED/src/fl/xmap.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xymap.cpp b/v1.4/wrobot/lib/FastLED/src/fl/xymap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xymap.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/xymap.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xymap.h b/v1.4/wrobot/lib/FastLED/src/fl/xymap.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xymap.h rename to v1.4/wrobot/lib/FastLED/src/fl/xymap.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xypath.cpp b/v1.4/wrobot/lib/FastLED/src/fl/xypath.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xypath.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/xypath.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xypath.h b/v1.4/wrobot/lib/FastLED/src/fl/xypath.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xypath.h rename to v1.4/wrobot/lib/FastLED/src/fl/xypath.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xypath_impls.cpp b/v1.4/wrobot/lib/FastLED/src/fl/xypath_impls.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xypath_impls.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/xypath_impls.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xypath_impls.h b/v1.4/wrobot/lib/FastLED/src/fl/xypath_impls.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xypath_impls.h rename to v1.4/wrobot/lib/FastLED/src/fl/xypath_impls.h diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xypath_renderer.cpp b/v1.4/wrobot/lib/FastLED/src/fl/xypath_renderer.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xypath_renderer.cpp rename to v1.4/wrobot/lib/FastLED/src/fl/xypath_renderer.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fl/xypath_renderer.h b/v1.4/wrobot/lib/FastLED/src/fl/xypath_renderer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fl/xypath_renderer.h rename to v1.4/wrobot/lib/FastLED/src/fl/xypath_renderer.h diff --git a/v2.0/wrobot/lib/FastLED/src/fonts/_readme b/v1.4/wrobot/lib/FastLED/src/fonts/_readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fonts/_readme rename to v1.4/wrobot/lib/FastLED/src/fonts/_readme diff --git a/v2.0/wrobot/lib/FastLED/src/fonts/console_font_4x6.h b/v1.4/wrobot/lib/FastLED/src/fonts/console_font_4x6.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fonts/console_font_4x6.h rename to v1.4/wrobot/lib/FastLED/src/fonts/console_font_4x6.h diff --git a/v2.0/wrobot/lib/FastLED/src/fonts/console_font_5x12.h b/v1.4/wrobot/lib/FastLED/src/fonts/console_font_5x12.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fonts/console_font_5x12.h rename to v1.4/wrobot/lib/FastLED/src/fonts/console_font_5x12.h diff --git a/v2.0/wrobot/lib/FastLED/src/fonts/console_font_5x8.h b/v1.4/wrobot/lib/FastLED/src/fonts/console_font_5x8.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fonts/console_font_5x8.h rename to v1.4/wrobot/lib/FastLED/src/fonts/console_font_5x8.h diff --git a/v2.0/wrobot/lib/FastLED/src/fonts/console_font_6x8.h b/v1.4/wrobot/lib/FastLED/src/fonts/console_font_6x8.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fonts/console_font_6x8.h rename to v1.4/wrobot/lib/FastLED/src/fonts/console_font_6x8.h diff --git a/v2.0/wrobot/lib/FastLED/src/fonts/console_font_7x9.h b/v1.4/wrobot/lib/FastLED/src/fonts/console_font_7x9.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fonts/console_font_7x9.h rename to v1.4/wrobot/lib/FastLED/src/fonts/console_font_7x9.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/README.md b/v1.4/wrobot/lib/FastLED/src/fx/1d/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/README.md rename to v1.4/wrobot/lib/FastLED/src/fx/1d/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/cylon.h b/v1.4/wrobot/lib/FastLED/src/fx/1d/cylon.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/cylon.h rename to v1.4/wrobot/lib/FastLED/src/fx/1d/cylon.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/demoreel100.h b/v1.4/wrobot/lib/FastLED/src/fx/1d/demoreel100.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/demoreel100.h rename to v1.4/wrobot/lib/FastLED/src/fx/1d/demoreel100.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/fire2012.h b/v1.4/wrobot/lib/FastLED/src/fx/1d/fire2012.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/fire2012.h rename to v1.4/wrobot/lib/FastLED/src/fx/1d/fire2012.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/noisewave.h b/v1.4/wrobot/lib/FastLED/src/fx/1d/noisewave.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/noisewave.h rename to v1.4/wrobot/lib/FastLED/src/fx/1d/noisewave.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/pacifica.h b/v1.4/wrobot/lib/FastLED/src/fx/1d/pacifica.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/pacifica.h rename to v1.4/wrobot/lib/FastLED/src/fx/1d/pacifica.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/pride2015.h b/v1.4/wrobot/lib/FastLED/src/fx/1d/pride2015.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/pride2015.h rename to v1.4/wrobot/lib/FastLED/src/fx/1d/pride2015.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/1d/twinklefox.h b/v1.4/wrobot/lib/FastLED/src/fx/1d/twinklefox.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/1d/twinklefox.h rename to v1.4/wrobot/lib/FastLED/src/fx/1d/twinklefox.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/README.md b/v1.4/wrobot/lib/FastLED/src/fx/2d/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/README.md rename to v1.4/wrobot/lib/FastLED/src/fx/2d/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/animartrix.hpp b/v1.4/wrobot/lib/FastLED/src/fx/2d/animartrix.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/animartrix.hpp rename to v1.4/wrobot/lib/FastLED/src/fx/2d/animartrix.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/animartrix_detail.hpp b/v1.4/wrobot/lib/FastLED/src/fx/2d/animartrix_detail.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/animartrix_detail.hpp rename to v1.4/wrobot/lib/FastLED/src/fx/2d/animartrix_detail.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/blend.cpp b/v1.4/wrobot/lib/FastLED/src/fx/2d/blend.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/blend.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/2d/blend.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/blend.h b/v1.4/wrobot/lib/FastLED/src/fx/2d/blend.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/blend.h rename to v1.4/wrobot/lib/FastLED/src/fx/2d/blend.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/luminova.cpp b/v1.4/wrobot/lib/FastLED/src/fx/2d/luminova.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/luminova.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/2d/luminova.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/luminova.h b/v1.4/wrobot/lib/FastLED/src/fx/2d/luminova.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/luminova.h rename to v1.4/wrobot/lib/FastLED/src/fx/2d/luminova.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/noisepalette.cpp b/v1.4/wrobot/lib/FastLED/src/fx/2d/noisepalette.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/noisepalette.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/2d/noisepalette.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/noisepalette.h b/v1.4/wrobot/lib/FastLED/src/fx/2d/noisepalette.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/noisepalette.h rename to v1.4/wrobot/lib/FastLED/src/fx/2d/noisepalette.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/redsquare.h b/v1.4/wrobot/lib/FastLED/src/fx/2d/redsquare.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/redsquare.h rename to v1.4/wrobot/lib/FastLED/src/fx/2d/redsquare.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/scale_up.cpp b/v1.4/wrobot/lib/FastLED/src/fx/2d/scale_up.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/scale_up.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/2d/scale_up.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/scale_up.h b/v1.4/wrobot/lib/FastLED/src/fx/2d/scale_up.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/scale_up.h rename to v1.4/wrobot/lib/FastLED/src/fx/2d/scale_up.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/wave.cpp b/v1.4/wrobot/lib/FastLED/src/fx/2d/wave.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/wave.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/2d/wave.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/2d/wave.h b/v1.4/wrobot/lib/FastLED/src/fx/2d/wave.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/2d/wave.h rename to v1.4/wrobot/lib/FastLED/src/fx/2d/wave.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/README.md b/v1.4/wrobot/lib/FastLED/src/fx/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/README.md rename to v1.4/wrobot/lib/FastLED/src/fx/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/fx/detail/README.md b/v1.4/wrobot/lib/FastLED/src/fx/detail/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/detail/README.md rename to v1.4/wrobot/lib/FastLED/src/fx/detail/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/fx/detail/draw_context.h b/v1.4/wrobot/lib/FastLED/src/fx/detail/draw_context.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/detail/draw_context.h rename to v1.4/wrobot/lib/FastLED/src/fx/detail/draw_context.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/detail/fx_compositor.h b/v1.4/wrobot/lib/FastLED/src/fx/detail/fx_compositor.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/detail/fx_compositor.h rename to v1.4/wrobot/lib/FastLED/src/fx/detail/fx_compositor.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/detail/fx_layer.cpp b/v1.4/wrobot/lib/FastLED/src/fx/detail/fx_layer.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/detail/fx_layer.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/detail/fx_layer.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/detail/fx_layer.h b/v1.4/wrobot/lib/FastLED/src/fx/detail/fx_layer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/detail/fx_layer.h rename to v1.4/wrobot/lib/FastLED/src/fx/detail/fx_layer.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/detail/transition.h b/v1.4/wrobot/lib/FastLED/src/fx/detail/transition.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/detail/transition.h rename to v1.4/wrobot/lib/FastLED/src/fx/detail/transition.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/frame.cpp b/v1.4/wrobot/lib/FastLED/src/fx/frame.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/frame.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/frame.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/frame.h b/v1.4/wrobot/lib/FastLED/src/fx/frame.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/frame.h rename to v1.4/wrobot/lib/FastLED/src/fx/frame.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/fx.h b/v1.4/wrobot/lib/FastLED/src/fx/fx.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/fx.h rename to v1.4/wrobot/lib/FastLED/src/fx/fx.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/fx1d.h b/v1.4/wrobot/lib/FastLED/src/fx/fx1d.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/fx1d.h rename to v1.4/wrobot/lib/FastLED/src/fx/fx1d.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/fx2d.h b/v1.4/wrobot/lib/FastLED/src/fx/fx2d.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/fx2d.h rename to v1.4/wrobot/lib/FastLED/src/fx/fx2d.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/fx_engine.cpp b/v1.4/wrobot/lib/FastLED/src/fx/fx_engine.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/fx_engine.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/fx_engine.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/fx_engine.h b/v1.4/wrobot/lib/FastLED/src/fx/fx_engine.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/fx_engine.h rename to v1.4/wrobot/lib/FastLED/src/fx/fx_engine.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/readme b/v1.4/wrobot/lib/FastLED/src/fx/readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/readme rename to v1.4/wrobot/lib/FastLED/src/fx/readme diff --git a/v2.0/wrobot/lib/FastLED/src/fx/time.cpp b/v1.4/wrobot/lib/FastLED/src/fx/time.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/time.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/time.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/time.h b/v1.4/wrobot/lib/FastLED/src/fx/time.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/time.h rename to v1.4/wrobot/lib/FastLED/src/fx/time.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video.cpp b/v1.4/wrobot/lib/FastLED/src/fx/video.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/video.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video.h b/v1.4/wrobot/lib/FastLED/src/fx/video.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video.h rename to v1.4/wrobot/lib/FastLED/src/fx/video.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/README.md b/v1.4/wrobot/lib/FastLED/src/fx/video/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/README.md rename to v1.4/wrobot/lib/FastLED/src/fx/video/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/frame_interpolator.cpp b/v1.4/wrobot/lib/FastLED/src/fx/video/frame_interpolator.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/frame_interpolator.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/video/frame_interpolator.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/frame_interpolator.h b/v1.4/wrobot/lib/FastLED/src/fx/video/frame_interpolator.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/frame_interpolator.h rename to v1.4/wrobot/lib/FastLED/src/fx/video/frame_interpolator.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/frame_tracker.cpp b/v1.4/wrobot/lib/FastLED/src/fx/video/frame_tracker.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/frame_tracker.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/video/frame_tracker.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/frame_tracker.h b/v1.4/wrobot/lib/FastLED/src/fx/video/frame_tracker.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/frame_tracker.h rename to v1.4/wrobot/lib/FastLED/src/fx/video/frame_tracker.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/pixel_stream.cpp b/v1.4/wrobot/lib/FastLED/src/fx/video/pixel_stream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/pixel_stream.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/video/pixel_stream.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/pixel_stream.h b/v1.4/wrobot/lib/FastLED/src/fx/video/pixel_stream.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/pixel_stream.h rename to v1.4/wrobot/lib/FastLED/src/fx/video/pixel_stream.h diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/video_impl.cpp b/v1.4/wrobot/lib/FastLED/src/fx/video/video_impl.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/video_impl.cpp rename to v1.4/wrobot/lib/FastLED/src/fx/video/video_impl.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/fx/video/video_impl.h b/v1.4/wrobot/lib/FastLED/src/fx/video/video_impl.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/fx/video/video_impl.h rename to v1.4/wrobot/lib/FastLED/src/fx/video/video_impl.h diff --git a/v2.0/wrobot/lib/FastLED/src/hsv2rgb.cpp b/v1.4/wrobot/lib/FastLED/src/hsv2rgb.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/hsv2rgb.cpp rename to v1.4/wrobot/lib/FastLED/src/hsv2rgb.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/hsv2rgb.h b/v1.4/wrobot/lib/FastLED/src/hsv2rgb.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/hsv2rgb.h rename to v1.4/wrobot/lib/FastLED/src/hsv2rgb.h diff --git a/v2.0/wrobot/lib/FastLED/src/led_sysdefs.h b/v1.4/wrobot/lib/FastLED/src/led_sysdefs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/led_sysdefs.h rename to v1.4/wrobot/lib/FastLED/src/led_sysdefs.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion.cpp b/v1.4/wrobot/lib/FastLED/src/lib8tion.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion.cpp rename to v1.4/wrobot/lib/FastLED/src/lib8tion.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion.h b/v1.4/wrobot/lib/FastLED/src/lib8tion.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/config.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/config.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/config.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/config.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/intmap.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/intmap.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/intmap.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/intmap.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/lib8static.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/lib8static.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/lib8static.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/lib8static.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/math8.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/math8.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/math8.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/math8.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/memmove.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/memmove.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/memmove.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/memmove.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/qfx.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/qfx.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/qfx.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/qfx.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/random8.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/random8.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/random8.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/random8.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/scale8.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/scale8.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/scale8.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/scale8.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/trig8.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/trig8.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/trig8.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/trig8.h diff --git a/v2.0/wrobot/lib/FastLED/src/lib8tion/types.h b/v1.4/wrobot/lib/FastLED/src/lib8tion/types.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/lib8tion/types.h rename to v1.4/wrobot/lib/FastLED/src/lib8tion/types.h diff --git a/v2.0/wrobot/lib/FastLED/src/noise.cpp b/v1.4/wrobot/lib/FastLED/src/noise.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/noise.cpp rename to v1.4/wrobot/lib/FastLED/src/noise.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/noise.h b/v1.4/wrobot/lib/FastLED/src/noise.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/noise.h rename to v1.4/wrobot/lib/FastLED/src/noise.h diff --git a/v2.0/wrobot/lib/FastLED/src/noisegen.h b/v1.4/wrobot/lib/FastLED/src/noisegen.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/noisegen.h rename to v1.4/wrobot/lib/FastLED/src/noisegen.h diff --git a/v2.0/wrobot/lib/FastLED/src/pixel_controller.h b/v1.4/wrobot/lib/FastLED/src/pixel_controller.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/pixel_controller.h rename to v1.4/wrobot/lib/FastLED/src/pixel_controller.h diff --git a/v2.0/wrobot/lib/FastLED/src/pixel_iterator.h b/v1.4/wrobot/lib/FastLED/src/pixel_iterator.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/pixel_iterator.h rename to v1.4/wrobot/lib/FastLED/src/pixel_iterator.h diff --git a/v2.0/wrobot/lib/FastLED/src/pixelset.h b/v1.4/wrobot/lib/FastLED/src/pixelset.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/pixelset.h rename to v1.4/wrobot/lib/FastLED/src/pixelset.h diff --git a/v2.0/wrobot/lib/FastLED/src/pixeltypes.h b/v1.4/wrobot/lib/FastLED/src/pixeltypes.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/pixeltypes.h rename to v1.4/wrobot/lib/FastLED/src/pixeltypes.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms.cpp b/v1.4/wrobot/lib/FastLED/src/platforms.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms.h b/v1.4/wrobot/lib/FastLED/src/platforms.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms.h rename to v1.4/wrobot/lib/FastLED/src/platforms.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/adafruit/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/adafruit/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/adafruit/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/adafruit/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless.h b/v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless.h rename to v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless_fake.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless_fake.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless_fake.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless_fake.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless_real.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless_real.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/adafruit/clockless_real.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/adafruit/clockless_real.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/adafruit/driver.h b/v1.4/wrobot/lib/FastLED/src/platforms/adafruit/driver.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/adafruit/driver.h rename to v1.4/wrobot/lib/FastLED/src/platforms/adafruit/driver.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/apollo3/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/apollo3/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/apollo3/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/apollo3/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/apollo3/clockless_apollo3.h b/v1.4/wrobot/lib/FastLED/src/platforms/apollo3/clockless_apollo3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/apollo3/clockless_apollo3.h rename to v1.4/wrobot/lib/FastLED/src/platforms/apollo3/clockless_apollo3.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/apollo3/compile_test.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/apollo3/compile_test.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/apollo3/compile_test.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/apollo3/compile_test.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/apollo3/fastled_apollo3.h b/v1.4/wrobot/lib/FastLED/src/platforms/apollo3/fastled_apollo3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/apollo3/fastled_apollo3.h rename to v1.4/wrobot/lib/FastLED/src/platforms/apollo3/fastled_apollo3.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/apollo3/fastpin_apollo3.h b/v1.4/wrobot/lib/FastLED/src/platforms/apollo3/fastpin_apollo3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/apollo3/fastpin_apollo3.h rename to v1.4/wrobot/lib/FastLED/src/platforms/apollo3/fastpin_apollo3.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/apollo3/fastspi_apollo3.h b/v1.4/wrobot/lib/FastLED/src/platforms/apollo3/fastspi_apollo3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/apollo3/fastspi_apollo3.h rename to v1.4/wrobot/lib/FastLED/src/platforms/apollo3/fastspi_apollo3.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/apollo3/led_sysdefs_apollo3.h b/v1.4/wrobot/lib/FastLED/src/platforms/apollo3/led_sysdefs_apollo3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/apollo3/led_sysdefs_apollo3.h rename to v1.4/wrobot/lib/FastLED/src/platforms/apollo3/led_sysdefs_apollo3.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arduino/audio_input.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/arduino/audio_input.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arduino/audio_input.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/arduino/audio_input.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/arm_compile.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/arm/arm_compile.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/arm_compile.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/arm_compile.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/common/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/common/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/common/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/common/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/common/m0clockless.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/common/m0clockless.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/common/m0clockless.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/common/m0clockless.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/compile_test.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/arm/compile_test.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/compile_test.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/compile_test.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/clockless_arm_d21.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/clockless_arm_d21.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/clockless_arm_d21.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/clockless_arm_d21.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/fastled_arm_d21.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/fastled_arm_d21.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/fastled_arm_d21.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/fastled_arm_d21.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/fastpin_arm_d21.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/fastpin_arm_d21.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/fastpin_arm_d21.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/fastpin_arm_d21.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/led_sysdefs_arm_d21.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/led_sysdefs_arm_d21.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d21/led_sysdefs_arm_d21.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d21/led_sysdefs_arm_d21.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/README.txt b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/README.txt similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/README.txt rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/README.txt diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/clockless_arm_d51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/clockless_arm_d51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/clockless_arm_d51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/clockless_arm_d51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/fastled_arm_d51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/fastled_arm_d51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/fastled_arm_d51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/fastled_arm_d51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/fastpin_arm_d51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/fastpin_arm_d51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/fastpin_arm_d51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/fastpin_arm_d51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/led_sysdefs_arm_d51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/led_sysdefs_arm_d51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/d51/led_sysdefs_arm_d51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/d51/led_sysdefs_arm_d51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/armpin.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/armpin.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/armpin.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/armpin.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/clockless_arm_giga.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/clockless_arm_giga.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/clockless_arm_giga.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/clockless_arm_giga.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/fastled_arm_giga.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/fastled_arm_giga.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/fastled_arm_giga.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/fastled_arm_giga.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/fastpin_arm_giga.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/fastpin_arm_giga.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/fastpin_arm_giga.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/fastpin_arm_giga.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/led_sysdef_arm_giga.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/led_sysdef_arm_giga.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/giga/led_sysdef_arm_giga.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/giga/led_sysdef_arm_giga.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/int.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/int.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/int.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/int.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/is_arm.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/is_arm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/is_arm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/is_arm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_arm_k20.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_arm_k20.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_arm_k20.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_arm_k20.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_block_arm_k20.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_block_arm_k20.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_block_arm_k20.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_block_arm_k20.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/clockless_objectfled.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/fastled_arm_k20.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/fastled_arm_k20.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/fastled_arm_k20.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/fastled_arm_k20.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/fastpin_arm_k20.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/fastpin_arm_k20.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/fastpin_arm_k20.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/fastpin_arm_k20.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/fastspi_arm_k20.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/fastspi_arm_k20.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/fastspi_arm_k20.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/fastspi_arm_k20.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/led_sysdefs_arm_k20.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/led_sysdefs_arm_k20.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/led_sysdefs_arm_k20.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/led_sysdefs_arm_k20.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/octows2811_controller.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/octows2811_controller.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/octows2811_controller.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/octows2811_controller.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/smartmatrix_t3.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/smartmatrix_t3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/smartmatrix_t3.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/smartmatrix_t3.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/ws2812serial_controller.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/ws2812serial_controller.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k20/ws2812serial_controller.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k20/ws2812serial_controller.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_arm_k66.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_arm_k66.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_arm_k66.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_arm_k66.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_block_arm_k66.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_block_arm_k66.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_block_arm_k66.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/clockless_block_arm_k66.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/fastled_arm_k66.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/fastled_arm_k66.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/fastled_arm_k66.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/fastled_arm_k66.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/fastpin_arm_k66.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/fastpin_arm_k66.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/fastpin_arm_k66.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/fastpin_arm_k66.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/fastspi_arm_k66.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/fastspi_arm_k66.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/fastspi_arm_k66.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/fastspi_arm_k66.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/led_sysdefs_arm_k66.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/led_sysdefs_arm_k66.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/k66/led_sysdefs_arm_k66.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/k66/led_sysdefs_arm_k66.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/clockless_arm_kl26.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/clockless_arm_kl26.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/clockless_arm_kl26.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/clockless_arm_kl26.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/fastled_arm_kl26.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/fastled_arm_kl26.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/fastled_arm_kl26.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/fastled_arm_kl26.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/fastpin_arm_kl26.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/fastpin_arm_kl26.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/fastpin_arm_kl26.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/fastpin_arm_kl26.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/fastspi_arm_kl26.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/fastspi_arm_kl26.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/fastspi_arm_kl26.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/fastspi_arm_kl26.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/led_sysdefs_arm_kl26.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/led_sysdefs_arm_kl26.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/kl26/led_sysdefs_arm_kl26.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/kl26/led_sysdefs_arm_kl26.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/clockless_arm_mgm240.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/clockless_arm_mgm240.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/clockless_arm_mgm240.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/clockless_arm_mgm240.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastled_arm_mgm240.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastled_arm_mgm240.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastled_arm_mgm240.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastled_arm_mgm240.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/fastpin_arm_mgm240.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/led_sysdefs_arm_mgm240.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/led_sysdefs_arm_mgm240.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mgm240/led_sysdefs_arm_mgm240.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mgm240/led_sysdefs_arm_mgm240.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/block_clockless_arm_mxrt1062.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/block_clockless_arm_mxrt1062.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/block_clockless_arm_mxrt1062.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/block_clockless_arm_mxrt1062.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/clockless_arm_mxrt1062.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/clockless_arm_mxrt1062.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/clockless_arm_mxrt1062.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/clockless_arm_mxrt1062.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastled_arm_mxrt1062.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastled_arm_mxrt1062.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastled_arm_mxrt1062.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastled_arm_mxrt1062.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastpin_arm_mxrt1062.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastpin_arm_mxrt1062.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastpin_arm_mxrt1062.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastpin_arm_mxrt1062.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastspi_arm_mxrt1062.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastspi_arm_mxrt1062.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastspi_arm_mxrt1062.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/fastspi_arm_mxrt1062.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/led_sysdefs_arm_mxrt1062.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/led_sysdefs_arm_mxrt1062.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/led_sysdefs_arm_mxrt1062.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/led_sysdefs_arm_mxrt1062.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/octows2811_controller.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/octows2811_controller.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/octows2811_controller.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/mxrt1062/octows2811_controller.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/clockless_arm_nrf51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/clockless_arm_nrf51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/clockless_arm_nrf51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/clockless_arm_nrf51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastled_arm_nrf51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastled_arm_nrf51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastled_arm_nrf51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastled_arm_nrf51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastpin_arm_nrf51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastpin_arm_nrf51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastpin_arm_nrf51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastpin_arm_nrf51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastspi_arm_nrf51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastspi_arm_nrf51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastspi_arm_nrf51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/fastspi_arm_nrf51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/led_sysdefs_arm_nrf51.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/led_sysdefs_arm_nrf51.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf51/led_sysdefs_arm_nrf51.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf51/led_sysdefs_arm_nrf51.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/arbiter_nrf52.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/arbiter_nrf52.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/arbiter_nrf52.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/arbiter_nrf52.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/clockless_arm_nrf52.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/clockless_arm_nrf52.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/clockless_arm_nrf52.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/clockless_arm_nrf52.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastled_arm_nrf52.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastled_arm_nrf52.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastled_arm_nrf52.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastled_arm_nrf52.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52_variants.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52_variants.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52_variants.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastpin_arm_nrf52_variants.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastspi_arm_nrf52.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastspi_arm_nrf52.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastspi_arm_nrf52.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/fastspi_arm_nrf52.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/led_sysdefs_arm_nrf52.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/led_sysdefs_arm_nrf52.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/nrf52/led_sysdefs_arm_nrf52.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/nrf52/led_sysdefs_arm_nrf52.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/clockless_arm_renesas.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/clockless_arm_renesas.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/clockless_arm_renesas.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/clockless_arm_renesas.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/fastled_arm_renesas.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/fastled_arm_renesas.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/fastled_arm_renesas.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/fastled_arm_renesas.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/fastpin_arm_renesas.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/fastpin_arm_renesas.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/fastpin_arm_renesas.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/fastpin_arm_renesas.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/led_sysdef_arm_renesas.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/led_sysdef_arm_renesas.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/renesas/led_sysdef_arm_renesas.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/renesas/led_sysdef_arm_renesas.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/clockless_arm_rp2040.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/clockless_arm_rp2040.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/clockless_arm_rp2040.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/clockless_arm_rp2040.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastled_arm_rp2040.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastled_arm_rp2040.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastled_arm_rp2040.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastled_arm_rp2040.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastpin_arm_rp2040.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastpin_arm_rp2040.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastpin_arm_rp2040.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/fastpin_arm_rp2040.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/led_sysdefs_arm_rp2040.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/led_sysdefs_arm_rp2040.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/led_sysdefs_arm_rp2040.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/led_sysdefs_arm_rp2040.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_asm.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_asm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_asm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_asm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_gen.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_gen.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_gen.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/rp2040/pio_gen.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_arm_sam.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_arm_sam.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_arm_sam.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_arm_sam.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_block_arm_sam.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_block_arm_sam.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_block_arm_sam.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/clockless_block_arm_sam.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/fastled_arm_sam.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/fastled_arm_sam.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/fastled_arm_sam.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/fastled_arm_sam.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/fastpin_arm_sam.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/fastpin_arm_sam.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/fastpin_arm_sam.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/fastpin_arm_sam.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/fastspi_arm_sam.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/fastspi_arm_sam.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/fastspi_arm_sam.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/fastspi_arm_sam.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/led_sysdefs_arm_sam.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/led_sysdefs_arm_sam.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/sam/led_sysdefs_arm_sam.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/sam/led_sysdefs_arm_sam.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/armpin.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/armpin.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/armpin.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/armpin.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/clockless_arm_stm32.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/clockless_arm_stm32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/clockless_arm_stm32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/clockless_arm_stm32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/cm3_regs.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/cm3_regs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/cm3_regs.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/cm3_regs.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastled_arm_stm32.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastled_arm_stm32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastled_arm_stm32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastled_arm_stm32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm32.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_legacy.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_legacy.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_legacy.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_legacy.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_new.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_new.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_new.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/fastpin_arm_stm_new.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/led_sysdefs_arm_stm32.h b/v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/led_sysdefs_arm_stm32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/arm/stm32/led_sysdefs_arm_stm32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/arm/stm32/led_sysdefs_arm_stm32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/assert_defs.h b/v1.4/wrobot/lib/FastLED/src/platforms/assert_defs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/assert_defs.h rename to v1.4/wrobot/lib/FastLED/src/platforms/assert_defs.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/audio.h b/v1.4/wrobot/lib/FastLED/src/platforms/audio.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/audio.h rename to v1.4/wrobot/lib/FastLED/src/platforms/audio.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/audio_input_null.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/audio_input_null.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/audio_input_null.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/audio_input_null.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/avr/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_compile.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_compile.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_compile.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_compile.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer0_impl_source.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer0_impl_source.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer0_impl_source.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer0_impl_source.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_null_counter.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_null_counter.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_null_counter.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_null_counter.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_source.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_source.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_source.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_millis_timer_source.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_pin.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_pin.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/avr_pin.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/avr_pin.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/clockless_trinket.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/clockless_trinket.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/clockless_trinket.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/clockless_trinket.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/compile_test.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/avr/compile_test.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/compile_test.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/compile_test.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/fastled_avr.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/fastled_avr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/fastled_avr.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/fastled_avr.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_atmega4809.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_atmega4809.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_atmega4809.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_atmega4809.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_legacy.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_legacy.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_legacy.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_legacy.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_nano_every.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_nano_every.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_nano_every.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/fastpin_avr_nano_every.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/fastspi_avr.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/fastspi_avr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/fastspi_avr.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/fastspi_avr.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/int.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/int.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/int.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/int.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/io_avr.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/io_avr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/io_avr.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/io_avr.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/io_avr_lowlevel.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/io_avr_lowlevel.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/io_avr_lowlevel.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/io_avr_lowlevel.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/avr/led_sysdefs_avr.h b/v1.4/wrobot/lib/FastLED/src/platforms/avr/led_sysdefs_avr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/avr/led_sysdefs_avr.h rename to v1.4/wrobot/lib/FastLED/src/platforms/avr/led_sysdefs_avr.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/chipsets_specialized_ws2812.h b/v1.4/wrobot/lib/FastLED/src/platforms/chipsets_specialized_ws2812.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/chipsets_specialized_ws2812.h rename to v1.4/wrobot/lib/FastLED/src/platforms/chipsets_specialized_ws2812.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/compile_test.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/compile_test.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/compile_test.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/compile_test.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio_impl.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio_impl.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio_impl.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/audio_impl.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/i2s.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/i2s.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/i2s.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/i2s.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf4_i2s_context.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf4_i2s_context.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf4_i2s_context.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf4_i2s_context.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf5_i2s_context.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf5_i2s_context.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf5_i2s_context.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/devices/idf5_i2s_context.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/audio/sound_util.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clock_cycles.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clock_cycles.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clock_cycles.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clock_cycles.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_block_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_block_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_block_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_block_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_i2s_esp32s3.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_rmt_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_rmt_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_rmt_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_rmt_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_spi_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_spi_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/clockless_spi_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/clockless_spi_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/esp_log_control.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/esp_log_control.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/esp_log_control.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/esp_log_control.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/fastled_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/fastled_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/fastled_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/fastled_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/fastpin_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/fastpin_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/fastpin_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/fastpin_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/fastspi_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/fastspi_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/fastspi_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/fastspi_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/i2s/i2s_esp32dev.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/INVESTIGATE.md b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/INVESTIGATE.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/INVESTIGATE.md rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/INVESTIGATE.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/riscv.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/riscv.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/riscv.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/riscv.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx6.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx6.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx6.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx6.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx7.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx7.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx7.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/interrupts/xtensa_lx7.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/led_sysdefs_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/led_sysdefs_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/led_sysdefs_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/led_sysdefs_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_clockless_rmt_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_clockless_rmt_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_clockless_rmt_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_clockless_rmt_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_4/idf4_rmt_impl.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/README_WORKER_POOL.md b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/README_WORKER_POOL.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/README_WORKER_POOL.md rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/README_WORKER_POOL.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_clockless_rmt_esp32.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_clockless_rmt_esp32.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_clockless_rmt_esp32.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_clockless_rmt_esp32.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/idf5_rmt.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/rmt_5/strip_rmt.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/32/spi_ws2812/strip_spi.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_block_esp8266.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_block_esp8266.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_block_esp8266.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_block_esp8266.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_esp8266.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_esp8266.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_esp8266.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/clockless_esp8266.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/fastled_esp8266.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/fastled_esp8266.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/fastled_esp8266.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/fastled_esp8266.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/fastpin_esp8266.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/fastpin_esp8266.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/fastpin_esp8266.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/fastpin_esp8266.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/fastspi_esp8266.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/fastspi_esp8266.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/fastspi_esp8266.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/fastspi_esp8266.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/led_sysdefs_esp8266.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/led_sysdefs_esp8266.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/led_sysdefs_esp8266.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/led_sysdefs_esp8266.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/progmem_esp8266.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/progmem_esp8266.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/8266/progmem_esp8266.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/8266/progmem_esp8266.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/esp/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/compile_test.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/compile_test.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/compile_test.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/compile_test.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/esp_assert.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/esp_assert.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/esp_assert.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/esp_assert.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/esp_compile.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/esp/esp_compile.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/esp_compile.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/esp_compile.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/esp_version.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/esp_version.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/esp_version.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/esp_version.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/int.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/int.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/int.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/int.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/io_esp.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/io_esp.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/io_esp.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/io_esp.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/esp/print_esp.h b/v1.4/wrobot/lib/FastLED/src/platforms/esp/print_esp.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/esp/print_esp.h rename to v1.4/wrobot/lib/FastLED/src/platforms/esp/print_esp.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/fastspi_ardunio_core.h b/v1.4/wrobot/lib/FastLED/src/platforms/fastspi_ardunio_core.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/fastspi_ardunio_core.h rename to v1.4/wrobot/lib/FastLED/src/platforms/fastspi_ardunio_core.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/fs_sdcard_arduino.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/fs_sdcard_arduino.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/fs_sdcard_arduino.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/fs_sdcard_arduino.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/int.h b/v1.4/wrobot/lib/FastLED/src/platforms/int.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/int.h rename to v1.4/wrobot/lib/FastLED/src/platforms/int.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/io_arduino.h b/v1.4/wrobot/lib/FastLED/src/platforms/io_arduino.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/io_arduino.h rename to v1.4/wrobot/lib/FastLED/src/platforms/io_arduino.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/io_native.h b/v1.4/wrobot/lib/FastLED/src/platforms/io_native.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/io_native.h rename to v1.4/wrobot/lib/FastLED/src/platforms/io_native.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/io_null.h b/v1.4/wrobot/lib/FastLED/src/platforms/io_null.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/io_null.h rename to v1.4/wrobot/lib/FastLED/src/platforms/io_null.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/io_teensy.h b/v1.4/wrobot/lib/FastLED/src/platforms/io_teensy.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/io_teensy.h rename to v1.4/wrobot/lib/FastLED/src/platforms/io_teensy.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/io_teensy_lc.h b/v1.4/wrobot/lib/FastLED/src/platforms/io_teensy_lc.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/io_teensy_lc.h rename to v1.4/wrobot/lib/FastLED/src/platforms/io_teensy_lc.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/neopixelbus/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/neopixelbus/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/neopixelbus/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/neopixelbus/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/neopixelbus/clockless.h b/v1.4/wrobot/lib/FastLED/src/platforms/neopixelbus/clockless.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/neopixelbus/clockless.h rename to v1.4/wrobot/lib/FastLED/src/platforms/neopixelbus/clockless.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/null_progmem.h b/v1.4/wrobot/lib/FastLED/src/platforms/null_progmem.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/null_progmem.h rename to v1.4/wrobot/lib/FastLED/src/platforms/null_progmem.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/posix/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/posix/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/posix/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/posix/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/posix/socket_posix.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/posix/socket_posix.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/posix/socket_posix.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/posix/socket_posix.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/posix/socket_posix.h b/v1.4/wrobot/lib/FastLED/src/platforms/posix/socket_posix.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/posix/socket_posix.h rename to v1.4/wrobot/lib/FastLED/src/platforms/posix/socket_posix.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/print_arduino.h b/v1.4/wrobot/lib/FastLED/src/platforms/print_arduino.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/print_arduino.h rename to v1.4/wrobot/lib/FastLED/src/platforms/print_arduino.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/print_native.h b/v1.4/wrobot/lib/FastLED/src/platforms/print_native.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/print_native.h rename to v1.4/wrobot/lib/FastLED/src/platforms/print_native.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/readme.md b/v1.4/wrobot/lib/FastLED/src/platforms/readme.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/readme.md rename to v1.4/wrobot/lib/FastLED/src/platforms/readme.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/shared/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/active_strip_data/active_strip_data.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/int.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/int.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/int.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/int.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_buffer.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_buffer.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_buffer.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_buffer.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/audio_internal.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/button.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/checkbox.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/description.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/dropdown.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/help.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/json_console.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/number_field.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/readme.md b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/readme.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/readme.md rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/readme.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/slider.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/title.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_internal.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.h b/v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.h rename to v1.4/wrobot/lib/FastLED/src/platforms/shared/ui/json/ui_manager.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/sketch_fake.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/sketch_fake.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/sketch_fake.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/sketch_fake.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/socket_platform.h b/v1.4/wrobot/lib/FastLED/src/platforms/socket_platform.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/socket_platform.h rename to v1.4/wrobot/lib/FastLED/src/platforms/socket_platform.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/Arduino.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/stub/Arduino.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/Arduino.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/Arduino.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/Arduino.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/Arduino.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/Arduino.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/Arduino.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/stub/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/clockless_stub.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/clockless_stub.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/clockless_stub.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/clockless_stub.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/clockless_stub_generic.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/clockless_stub_generic.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/clockless_stub_generic.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/clockless_stub_generic.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/compile_test.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/stub/compile_test.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/compile_test.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/compile_test.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/fastled_stub.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/fastled_stub.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/fastled_stub.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/fastled_stub.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub_generic.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub_generic.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub_generic.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/fastspi_stub_generic.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/generic/led_sysdefs_generic.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/stub/generic/led_sysdefs_generic.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/generic/led_sysdefs_generic.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/generic/led_sysdefs_generic.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub_generic.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub_generic.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub_generic.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/led_sysdefs_stub_generic.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/time_stub.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/stub/time_stub.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/time_stub.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/time_stub.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub/time_stub.h b/v1.4/wrobot/lib/FastLED/src/platforms/stub/time_stub.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub/time_stub.h rename to v1.4/wrobot/lib/FastLED/src/platforms/stub/time_stub.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/stub_main.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/stub_main.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/stub_main.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/stub_main.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/ui_defs.h b/v1.4/wrobot/lib/FastLED/src/platforms/ui_defs.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/ui_defs.h rename to v1.4/wrobot/lib/FastLED/src/platforms/ui_defs.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/active_strip_data.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/clockless.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/clockless.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/clockless.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/clockless.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/Arduino.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/Arduino.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/Arduino.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/Arduino.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/build_flags.toml b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/build_flags.toml similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/build_flags.toml rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/build_flags.toml diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.css b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.css similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.css rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.css diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.html b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.html similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.html rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.html diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/index.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/jsconfig.json b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/jsconfig.json similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/jsconfig.json rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/jsconfig.json diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_manager.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_manager.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_manager.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_manager.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_worklet_processor.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_worklet_processor.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_worklet_processor.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/audio_worklet_processor.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/column_validator.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/column_validator.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/column_validator.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/column_validator.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_async_controller.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_async_controller.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_async_controller.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_async_controller.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_callbacks.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_callbacks.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_callbacks.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_callbacks.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_debug_logger.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_debug_logger.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_debug_logger.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_debug_logger.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_events.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_events.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_events.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/fastled_events.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager_threejs.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager_threejs.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager_threejs.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_manager_threejs.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_utils.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_utils.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_utils.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/graphics_utils.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.css b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.css similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.css rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.css diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/json_inspector.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/layout_state_manager.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/layout_state_manager.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/layout_state_manager.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/layout_state_manager.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/resize_coordinator.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/resize_coordinator.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/resize_coordinator.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/resize_coordinator.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_layout_placement_manager.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_layout_placement_manager.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_layout_placement_manager.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_layout_placement_manager.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_manager.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_manager.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_manager.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_manager.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_playback.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_playback.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_playback.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_playback.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder_test.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder_test.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder_test.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/ui_recorder_test.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/video_recorder.js b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/video_recorder.js similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/video_recorder.js rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/modules/video_recorder.js diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/types.d.ts b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/types.d.ts similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/compiler/types.d.ts rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/compiler/types.d.ts diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/engine_listener.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/entry_point.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/entry_point.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/entry_point.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/entry_point.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/fastspi_wasm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/fs_wasm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/int.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/int.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/int.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/int.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/io_wasm.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/io_wasm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/io_wasm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/io_wasm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_assert.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_assert.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_assert.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_assert.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_bindings.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_fetch.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_fetch_readme.md b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_fetch_readme.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_fetch_readme.md rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_fetch_readme.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_progmem.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_progmem.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/js_progmem.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/js_progmem.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/led_sysdefs_wasm.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/led_sysdefs_wasm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/led_sysdefs_wasm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/led_sysdefs_wasm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/print_wasm.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/print_wasm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/print_wasm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/print_wasm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/readme b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/readme rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/readme diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.cpp.disabled b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.cpp.disabled similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.cpp.disabled rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.cpp.disabled diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/socket_wasm.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/strip_id_map.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/strip_id_map.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/strip_id_map.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/strip_id_map.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/timer.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/timer.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/timer.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/timer.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/types.d.ts b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/types.d.ts similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/types.d.ts rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/types.d.ts diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/ui.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/ui.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/ui.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/ui.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/ui.h b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/ui.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/ui.h rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/ui.h diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/wasm/wasm_compile.hpp b/v1.4/wrobot/lib/FastLED/src/platforms/wasm/wasm_compile.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/wasm/wasm_compile.hpp rename to v1.4/wrobot/lib/FastLED/src/platforms/wasm/wasm_compile.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/win/README.md b/v1.4/wrobot/lib/FastLED/src/platforms/win/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/win/README.md rename to v1.4/wrobot/lib/FastLED/src/platforms/win/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/win/socket_win.cpp b/v1.4/wrobot/lib/FastLED/src/platforms/win/socket_win.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/win/socket_win.cpp rename to v1.4/wrobot/lib/FastLED/src/platforms/win/socket_win.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/platforms/win/socket_win.h b/v1.4/wrobot/lib/FastLED/src/platforms/win/socket_win.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/platforms/win/socket_win.h rename to v1.4/wrobot/lib/FastLED/src/platforms/win/socket_win.h diff --git a/v2.0/wrobot/lib/FastLED/src/power_mgt.cpp b/v1.4/wrobot/lib/FastLED/src/power_mgt.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/power_mgt.cpp rename to v1.4/wrobot/lib/FastLED/src/power_mgt.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/power_mgt.h b/v1.4/wrobot/lib/FastLED/src/power_mgt.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/power_mgt.h rename to v1.4/wrobot/lib/FastLED/src/power_mgt.h diff --git a/v2.0/wrobot/lib/FastLED/src/rgbw.h b/v1.4/wrobot/lib/FastLED/src/rgbw.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/rgbw.h rename to v1.4/wrobot/lib/FastLED/src/rgbw.h diff --git a/v2.0/wrobot/lib/FastLED/src/sensors/button.cpp b/v1.4/wrobot/lib/FastLED/src/sensors/button.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/sensors/button.cpp rename to v1.4/wrobot/lib/FastLED/src/sensors/button.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/sensors/button.h b/v1.4/wrobot/lib/FastLED/src/sensors/button.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/sensors/button.h rename to v1.4/wrobot/lib/FastLED/src/sensors/button.h diff --git a/v2.0/wrobot/lib/FastLED/src/sensors/digital_pin.cpp b/v1.4/wrobot/lib/FastLED/src/sensors/digital_pin.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/sensors/digital_pin.cpp rename to v1.4/wrobot/lib/FastLED/src/sensors/digital_pin.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/sensors/digital_pin.h b/v1.4/wrobot/lib/FastLED/src/sensors/digital_pin.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/sensors/digital_pin.h rename to v1.4/wrobot/lib/FastLED/src/sensors/digital_pin.h diff --git a/v2.0/wrobot/lib/FastLED/src/sensors/pir.cpp b/v1.4/wrobot/lib/FastLED/src/sensors/pir.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/sensors/pir.cpp rename to v1.4/wrobot/lib/FastLED/src/sensors/pir.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/sensors/pir.h b/v1.4/wrobot/lib/FastLED/src/sensors/pir.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/sensors/pir.h rename to v1.4/wrobot/lib/FastLED/src/sensors/pir.h diff --git a/v2.0/wrobot/lib/FastLED/src/simplex.cpp b/v1.4/wrobot/lib/FastLED/src/simplex.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/simplex.cpp rename to v1.4/wrobot/lib/FastLED/src/simplex.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/arduinojson/json.h b/v1.4/wrobot/lib/FastLED/src/third_party/arduinojson/json.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/arduinojson/json.h rename to v1.4/wrobot/lib/FastLED/src/third_party/arduinojson/json.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/arduinojson/json.hpp b/v1.4/wrobot/lib/FastLED/src/third_party/arduinojson/json.hpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/arduinojson/json.hpp rename to v1.4/wrobot/lib/FastLED/src/third_party/arduinojson/json.hpp diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/LICENSE b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/LICENSE similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/LICENSE rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/LICENSE diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/Makefile b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/Makefile similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/Makefile rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/Makefile diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/README.md b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/README.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/README.md rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/README.md diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/_kiss_fft_guts.h b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/_kiss_fft_guts.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/_kiss_fft_guts.h rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/_kiss_fft_guts.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.cpp b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.cpp rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.h b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.h rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/cq_kernel.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/example.c.disabled b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/example.c.disabled similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/example.c.disabled rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/example.c.disabled diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/example_out_chart.png b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/example_out_chart.png similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/example_out_chart.png rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/example_out_chart.png diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.cpp b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.cpp rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.h b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.h rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fft.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.cpp b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.cpp rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.h b/v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.h rename to v1.4/wrobot/lib/FastLED/src/third_party/cq_kernel/kiss_fftr.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/readme b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/readme rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/readme diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/enabled.h b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/enabled.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/enabled.h rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/enabled.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip.h b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip.h rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_api.c b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_api.c similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_api.c rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_api.c diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_interface.h b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_interface.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_interface.h rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_interface.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt.h b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt.h rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_dev.c b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_dev.c similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_dev.c rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_dev.c diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.c b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.c similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.c rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.c diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.h b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.h rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_rmt_encoder.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi.h b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi.h rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi_dev.c b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi_dev.c similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi_dev.c rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_spi_dev.c diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_types.h b/v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_types.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_types.h rename to v1.4/wrobot/lib/FastLED/src/third_party/espressif/led_strip/src/led_strip_types.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/object_fled/readme b/v1.4/wrobot/lib/FastLED/src/third_party/object_fled/readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/object_fled/readme rename to v1.4/wrobot/lib/FastLED/src/third_party/object_fled/readme diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/object_fled/src/ObjectFLED.h b/v1.4/wrobot/lib/FastLED/src/third_party/object_fled/src/ObjectFLED.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/object_fled/src/ObjectFLED.h rename to v1.4/wrobot/lib/FastLED/src/third_party/object_fled/src/ObjectFLED.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/object_fled/src/OjectFLED.cpp b/v1.4/wrobot/lib/FastLED/src/third_party/object_fled/src/OjectFLED.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/object_fled/src/OjectFLED.cpp rename to v1.4/wrobot/lib/FastLED/src/third_party/object_fled/src/OjectFLED.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/_readme b/v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/_readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/_readme rename to v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/_readme diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/driver.h b/v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/driver.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/driver.h rename to v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/driver.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/I2SClockLessLedDriveresp32s3.h b/v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/I2SClockLessLedDriveresp32s3.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/I2SClockLessLedDriveresp32s3.h rename to v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/I2SClockLessLedDriveresp32s3.h diff --git a/v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/___pixeltypes.h b/v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/___pixeltypes.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/___pixeltypes.h rename to v1.4/wrobot/lib/FastLED/src/third_party/yves/I2SClockLessLedDriveresp32s3/src/___pixeltypes.h diff --git a/v2.0/wrobot/lib/FastLED/src/transpose8x1_noinline.cpp b/v1.4/wrobot/lib/FastLED/src/transpose8x1_noinline.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/transpose8x1_noinline.cpp rename to v1.4/wrobot/lib/FastLED/src/transpose8x1_noinline.cpp diff --git a/v2.0/wrobot/lib/FastLED/src/transpose8x1_noinline.h b/v1.4/wrobot/lib/FastLED/src/transpose8x1_noinline.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/transpose8x1_noinline.h rename to v1.4/wrobot/lib/FastLED/src/transpose8x1_noinline.h diff --git a/v2.0/wrobot/lib/FastLED/src/wiring.cpp b/v1.4/wrobot/lib/FastLED/src/wiring.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/src/wiring.cpp rename to v1.4/wrobot/lib/FastLED/src/wiring.cpp diff --git a/v2.0/wrobot/lib/FastLED/test.py b/v1.4/wrobot/lib/FastLED/test.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/test.py rename to v1.4/wrobot/lib/FastLED/test.py diff --git a/v2.0/wrobot/lib/FastLED/tests/AGENTS.md b/v1.4/wrobot/lib/FastLED/tests/AGENTS.md similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/AGENTS.md rename to v1.4/wrobot/lib/FastLED/tests/AGENTS.md diff --git a/v2.0/wrobot/lib/FastLED/tests/crash_handler.h b/v1.4/wrobot/lib/FastLED/tests/crash_handler.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/crash_handler.h rename to v1.4/wrobot/lib/FastLED/tests/crash_handler.h diff --git a/v2.0/wrobot/lib/FastLED/tests/crash_handler_execinfo.h b/v1.4/wrobot/lib/FastLED/tests/crash_handler_execinfo.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/crash_handler_execinfo.h rename to v1.4/wrobot/lib/FastLED/tests/crash_handler_execinfo.h diff --git a/v2.0/wrobot/lib/FastLED/tests/crash_handler_libunwind.h b/v1.4/wrobot/lib/FastLED/tests/crash_handler_libunwind.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/crash_handler_libunwind.h rename to v1.4/wrobot/lib/FastLED/tests/crash_handler_libunwind.h diff --git a/v2.0/wrobot/lib/FastLED/tests/crash_handler_noop.h b/v1.4/wrobot/lib/FastLED/tests/crash_handler_noop.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/crash_handler_noop.h rename to v1.4/wrobot/lib/FastLED/tests/crash_handler_noop.h diff --git a/v2.0/wrobot/lib/FastLED/tests/crash_handler_win.h b/v1.4/wrobot/lib/FastLED/tests/crash_handler_win.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/crash_handler_win.h rename to v1.4/wrobot/lib/FastLED/tests/crash_handler_win.h diff --git a/v2.0/wrobot/lib/FastLED/tests/doctest.h b/v1.4/wrobot/lib/FastLED/tests/doctest.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/doctest.h rename to v1.4/wrobot/lib/FastLED/tests/doctest.h diff --git a/v2.0/wrobot/lib/FastLED/tests/doctest_main.cpp b/v1.4/wrobot/lib/FastLED/tests/doctest_main.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/doctest_main.cpp rename to v1.4/wrobot/lib/FastLED/tests/doctest_main.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/readme b/v1.4/wrobot/lib/FastLED/tests/readme similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/readme rename to v1.4/wrobot/lib/FastLED/tests/readme diff --git a/v2.0/wrobot/lib/FastLED/tests/sketch_runner_demo.cpp b/v1.4/wrobot/lib/FastLED/tests/sketch_runner_demo.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/sketch_runner_demo.cpp rename to v1.4/wrobot/lib/FastLED/tests/sketch_runner_demo.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test.h b/v1.4/wrobot/lib/FastLED/tests/test.h similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test.h rename to v1.4/wrobot/lib/FastLED/tests/test.h diff --git a/v2.0/wrobot/lib/FastLED/tests/test_active_strip_data_json.cpp b/v1.4/wrobot/lib/FastLED/tests/test_active_strip_data_json.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_active_strip_data_json.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_active_strip_data_json.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_algorithm.cpp b/v1.4/wrobot/lib/FastLED/tests/test_algorithm.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_algorithm.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_algorithm.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_alignment_asan.pdb b/v1.4/wrobot/lib/FastLED/tests/test_alignment_asan.pdb similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_alignment_asan.pdb rename to v1.4/wrobot/lib/FastLED/tests/test_alignment_asan.pdb diff --git a/v2.0/wrobot/lib/FastLED/tests/test_allocator.cpp b/v1.4/wrobot/lib/FastLED/tests/test_allocator.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_allocator.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_allocator.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_allocator_inlined.cpp b/v1.4/wrobot/lib/FastLED/tests/test_allocator_inlined.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_allocator_inlined.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_allocator_inlined.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_allocator_inlined_slab.cpp b/v1.4/wrobot/lib/FastLED/tests/test_allocator_inlined_slab.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_allocator_inlined_slab.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_allocator_inlined_slab.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_allocator_slab.cpp b/v1.4/wrobot/lib/FastLED/tests/test_allocator_slab.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_allocator_slab.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_allocator_slab.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_async.cpp b/v1.4/wrobot/lib/FastLED/tests/test_async.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_async.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_async.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_atomic.cpp b/v1.4/wrobot/lib/FastLED/tests/test_atomic.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_atomic.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_atomic.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_audio_reactive_simple.cpp b/v1.4/wrobot/lib/FastLED/tests/test_audio_reactive_simple.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_audio_reactive_simple.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_audio_reactive_simple.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_await.cpp b/v1.4/wrobot/lib/FastLED/tests/test_await.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_await.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_await.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_bitcast_asan.pdb b/v1.4/wrobot/lib/FastLED/tests/test_bitcast_asan.pdb similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_bitcast_asan.pdb rename to v1.4/wrobot/lib/FastLED/tests/test_bitcast_asan.pdb diff --git a/v2.0/wrobot/lib/FastLED/tests/test_bitset.cpp b/v1.4/wrobot/lib/FastLED/tests/test_bitset.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_bitset.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_bitset.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_bytestream.cpp b/v1.4/wrobot/lib/FastLED/tests/test_bytestream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_bytestream.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_bytestream.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_circular_buffer.cpp b/v1.4/wrobot/lib/FastLED/tests/test_circular_buffer.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_circular_buffer.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_circular_buffer.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_corkscrew.cpp b/v1.4/wrobot/lib/FastLED/tests/test_corkscrew.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_corkscrew.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_corkscrew.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_dbg.cpp b/v1.4/wrobot/lib/FastLED/tests/test_dbg.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_dbg.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_dbg.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_deque.cpp b/v1.4/wrobot/lib/FastLED/tests/test_deque.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_deque.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_deque.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_downscale.cpp b/v1.4/wrobot/lib/FastLED/tests/test_downscale.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_downscale.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_downscale.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_easing.cpp b/v1.4/wrobot/lib/FastLED/tests/test_easing.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_easing.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_easing.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_example_compilation.cpp b/v1.4/wrobot/lib/FastLED/tests/test_example_compilation.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_example_compilation.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_example_compilation.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fastled.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fastled.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fastled.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fastled.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fft.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fft.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fft.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fft.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fixed_set.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fixed_set.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fixed_set.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fixed_set.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fl_string_comprehensive.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fl_string_comprehensive.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fl_string_comprehensive.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fl_string_comprehensive.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_frame.cpp b/v1.4/wrobot/lib/FastLED/tests/test_frame.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_frame.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_frame.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_frame_tracker.cpp b/v1.4/wrobot/lib/FastLED/tests/test_frame_tracker.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_frame_tracker.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_frame_tracker.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_function.cpp b/v1.4/wrobot/lib/FastLED/tests/test_function.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_function.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_function.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fx.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fx.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fx.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fx.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fx2d_blend.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fx2d_blend.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fx2d_blend.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fx2d_blend.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fx_engine.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fx_engine.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fx_engine.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fx_engine.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_fx_time.cpp b/v1.4/wrobot/lib/FastLED/tests/test_fx_time.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_fx_time.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_fx_time.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_grid.cpp b/v1.4/wrobot/lib/FastLED/tests/test_grid.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_grid.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_grid.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_hashmap.cpp b/v1.4/wrobot/lib/FastLED/tests/test_hashmap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_hashmap.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_hashmap.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_hashmap_lru.cpp b/v1.4/wrobot/lib/FastLED/tests/test_hashmap_lru.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_hashmap_lru.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_hashmap_lru.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_hsv16.cpp b/v1.4/wrobot/lib/FastLED/tests/test_hsv16.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_hsv16.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_hsv16.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_hsv_conversion_accuracy.cpp b/v1.4/wrobot/lib/FastLED/tests/test_hsv_conversion_accuracy.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_hsv_conversion_accuracy.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_hsv_conversion_accuracy.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_intmap.cpp b/v1.4/wrobot/lib/FastLED/tests/test_intmap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_intmap.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_intmap.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_invoke.cpp b/v1.4/wrobot/lib/FastLED/tests/test_invoke.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_invoke.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_invoke.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_istream.cpp b/v1.4/wrobot/lib/FastLED/tests/test_istream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_istream.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_istream.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_json.cpp b/v1.4/wrobot/lib/FastLED/tests/test_json.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_json.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_json.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_json_roundtrip.cpp b/v1.4/wrobot/lib/FastLED/tests/test_json_roundtrip.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_json_roundtrip.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_json_roundtrip.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_line_simplification.cpp b/v1.4/wrobot/lib/FastLED/tests/test_line_simplification.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_line_simplification.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_line_simplification.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_lut.cpp b/v1.4/wrobot/lib/FastLED/tests/test_lut.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_lut.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_lut.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_malloc_hooks.cpp b/v1.4/wrobot/lib/FastLED/tests/test_malloc_hooks.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_malloc_hooks.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_malloc_hooks.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_map.cpp b/v1.4/wrobot/lib/FastLED/tests/test_map.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_map.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_map.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_map_range.cpp b/v1.4/wrobot/lib/FastLED/tests/test_map_range.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_map_range.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_map_range.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_math.cpp b/v1.4/wrobot/lib/FastLED/tests/test_math.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_math.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_math.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_memory.cpp b/v1.4/wrobot/lib/FastLED/tests/test_memory.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_memory.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_memory.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_mutex.cpp b/v1.4/wrobot/lib/FastLED/tests/test_mutex.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_mutex.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_mutex.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_noise_range.cpp b/v1.4/wrobot/lib/FastLED/tests/test_noise_range.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_noise_range.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_noise_range.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_ostream.cpp b/v1.4/wrobot/lib/FastLED/tests/test_ostream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_ostream.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_ostream.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_pixelview.cpp b/v1.4/wrobot/lib/FastLED/tests/test_pixelview.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_pixelview.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_pixelview.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_platformio_ini_typed.py b/v1.4/wrobot/lib/FastLED/tests/test_platformio_ini_typed.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_platformio_ini_typed.py rename to v1.4/wrobot/lib/FastLED/tests/test_platformio_ini_typed.py diff --git a/v2.0/wrobot/lib/FastLED/tests/test_point.cpp b/v1.4/wrobot/lib/FastLED/tests/test_point.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_point.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_point.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_printf.cpp b/v1.4/wrobot/lib/FastLED/tests/test_printf.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_printf.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_printf.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_priority_queue.cpp b/v1.4/wrobot/lib/FastLED/tests/test_priority_queue.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_priority_queue.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_priority_queue.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_promise.cpp b/v1.4/wrobot/lib/FastLED/tests/test_promise.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_promise.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_promise.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_queue.cpp b/v1.4/wrobot/lib/FastLED/tests/test_queue.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_queue.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_queue.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_raster.cpp b/v1.4/wrobot/lib/FastLED/tests/test_raster.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_raster.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_raster.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_rbtree.cpp b/v1.4/wrobot/lib/FastLED/tests/test_rbtree.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_rbtree.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_rbtree.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_rectangular_buffer.cpp b/v1.4/wrobot/lib/FastLED/tests/test_rectangular_buffer.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_rectangular_buffer.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_rectangular_buffer.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_screenmap.cpp b/v1.4/wrobot/lib/FastLED/tests/test_screenmap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_screenmap.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_screenmap.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_screenmap_serialization.cpp b/v1.4/wrobot/lib/FastLED/tests/test_screenmap_serialization.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_screenmap_serialization.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_screenmap_serialization.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_set_inlined.cpp b/v1.4/wrobot/lib/FastLED/tests/test_set_inlined.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_set_inlined.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_set_inlined.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_shared_ptr.cpp b/v1.4/wrobot/lib/FastLED/tests/test_shared_ptr.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_shared_ptr.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_shared_ptr.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_sketch_runner.cpp b/v1.4/wrobot/lib/FastLED/tests/test_sketch_runner.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_sketch_runner.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_sketch_runner.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_slab_multi_alloc.cpp b/v1.4/wrobot/lib/FastLED/tests/test_slab_multi_alloc.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_slab_multi_alloc.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_slab_multi_alloc.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_slice.cpp b/v1.4/wrobot/lib/FastLED/tests/test_slice.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_slice.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_slice.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_span_conversions.cpp b/v1.4/wrobot/lib/FastLED/tests/test_span_conversions.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_span_conversions.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_span_conversions.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_splat.cpp b/v1.4/wrobot/lib/FastLED/tests/test_splat.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_splat.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_splat.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_str.cpp b/v1.4/wrobot/lib/FastLED/tests/test_str.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_str.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_str.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_strip_id_map.cpp b/v1.4/wrobot/lib/FastLED/tests/test_strip_id_map.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_strip_id_map.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_strip_id_map.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_strstream.cpp b/v1.4/wrobot/lib/FastLED/tests/test_strstream.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_strstream.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_strstream.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_task.cpp b/v1.4/wrobot/lib/FastLED/tests/test_task.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_task.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_task.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_thread_local.cpp b/v1.4/wrobot/lib/FastLED/tests/test_thread_local.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_thread_local.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_thread_local.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_tile2x2.cpp b/v1.4/wrobot/lib/FastLED/tests/test_tile2x2.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_tile2x2.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_tile2x2.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_time.cpp b/v1.4/wrobot/lib/FastLED/tests/test_time.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_time.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_time.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_transform.cpp b/v1.4/wrobot/lib/FastLED/tests/test_transform.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_transform.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_transform.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_transition_ramp.cpp b/v1.4/wrobot/lib/FastLED/tests/test_transition_ramp.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_transition_ramp.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_transition_ramp.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_traverse_grid.cpp b/v1.4/wrobot/lib/FastLED/tests/test_traverse_grid.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_traverse_grid.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_traverse_grid.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_tuple.cpp b/v1.4/wrobot/lib/FastLED/tests/test_tuple.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_tuple.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_tuple.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_type_traits.cpp b/v1.4/wrobot/lib/FastLED/tests/test_type_traits.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_type_traits.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_type_traits.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_ui.cpp b/v1.4/wrobot/lib/FastLED/tests/test_ui.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_ui.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_ui.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_ui_help.cpp b/v1.4/wrobot/lib/FastLED/tests/test_ui_help.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_ui_help.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_ui_help.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_ui_title_bug.cpp b/v1.4/wrobot/lib/FastLED/tests/test_ui_title_bug.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_ui_title_bug.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_ui_title_bug.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_unordered_set.cpp b/v1.4/wrobot/lib/FastLED/tests/test_unordered_set.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_unordered_set.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_unordered_set.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_variant.cpp b/v1.4/wrobot/lib/FastLED/tests/test_variant.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_variant.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_variant.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_vector.cpp b/v1.4/wrobot/lib/FastLED/tests/test_vector.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_vector.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_vector.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_video.cpp b/v1.4/wrobot/lib/FastLED/tests/test_video.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_video.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_video.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_videofx_wrapper.cpp b/v1.4/wrobot/lib/FastLED/tests/test_videofx_wrapper.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_videofx_wrapper.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_videofx_wrapper.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_weak_ptr.cpp b/v1.4/wrobot/lib/FastLED/tests/test_weak_ptr.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_weak_ptr.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_weak_ptr.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_xymap.cpp b/v1.4/wrobot/lib/FastLED/tests/test_xymap.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_xymap.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_xymap.cpp diff --git a/v2.0/wrobot/lib/FastLED/tests/test_xypath.cpp b/v1.4/wrobot/lib/FastLED/tests/test_xypath.cpp similarity index 100% rename from v2.0/wrobot/lib/FastLED/tests/test_xypath.cpp rename to v1.4/wrobot/lib/FastLED/tests/test_xypath.cpp diff --git a/v2.0/wrobot/lib/FastLED/tidy.sh b/v1.4/wrobot/lib/FastLED/tidy.sh similarity index 100% rename from v2.0/wrobot/lib/FastLED/tidy.sh rename to v1.4/wrobot/lib/FastLED/tidy.sh diff --git a/v2.0/wrobot/lib/FastLED/uno b/v1.4/wrobot/lib/FastLED/uno similarity index 100% rename from v2.0/wrobot/lib/FastLED/uno rename to v1.4/wrobot/lib/FastLED/uno diff --git a/v2.0/wrobot/lib/FastLED/validate_build_system.py b/v1.4/wrobot/lib/FastLED/validate_build_system.py similarity index 100% rename from v2.0/wrobot/lib/FastLED/validate_build_system.py rename to v1.4/wrobot/lib/FastLED/validate_build_system.py diff --git a/v2.0/wrobot/lib/FastLED/wasm b/v1.4/wrobot/lib/FastLED/wasm similarity index 100% rename from v2.0/wrobot/lib/FastLED/wasm rename to v1.4/wrobot/lib/FastLED/wasm diff --git a/v2.0/wrobot/lib/MPU6050_tockn/README.md b/v1.4/wrobot/lib/MPU6050_tockn/README.md similarity index 100% rename from v2.0/wrobot/lib/MPU6050_tockn/README.md rename to v1.4/wrobot/lib/MPU6050_tockn/README.md diff --git a/v2.0/wrobot/lib/MPU6050_tockn/examples/GetAllData/GetAllData.ino b/v1.4/wrobot/lib/MPU6050_tockn/examples/GetAllData/GetAllData.ino similarity index 100% rename from v2.0/wrobot/lib/MPU6050_tockn/examples/GetAllData/GetAllData.ino rename to v1.4/wrobot/lib/MPU6050_tockn/examples/GetAllData/GetAllData.ino diff --git a/v2.0/wrobot/lib/MPU6050_tockn/examples/GetAngle/GetAngle.ino b/v1.4/wrobot/lib/MPU6050_tockn/examples/GetAngle/GetAngle.ino similarity index 100% rename from v2.0/wrobot/lib/MPU6050_tockn/examples/GetAngle/GetAngle.ino rename to v1.4/wrobot/lib/MPU6050_tockn/examples/GetAngle/GetAngle.ino diff --git a/v2.0/wrobot/lib/MPU6050_tockn/library.properties b/v1.4/wrobot/lib/MPU6050_tockn/library.properties similarity index 100% rename from v2.0/wrobot/lib/MPU6050_tockn/library.properties rename to v1.4/wrobot/lib/MPU6050_tockn/library.properties diff --git a/v2.0/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.cpp b/v1.4/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.cpp similarity index 100% rename from v2.0/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.cpp rename to v1.4/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.cpp diff --git a/v2.0/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.h b/v1.4/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.h similarity index 100% rename from v2.0/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.h rename to v1.4/wrobot/lib/MPU6050_tockn/src/MPU6050_tockn.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/CHANGELOG.md b/v1.4/wrobot/lib/NimBLE-Arduino/CHANGELOG.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/CHANGELOG.md rename to v1.4/wrobot/lib/NimBLE-Arduino/CHANGELOG.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/LICENSE b/v1.4/wrobot/lib/NimBLE-Arduino/LICENSE similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/LICENSE rename to v1.4/wrobot/lib/NimBLE-Arduino/LICENSE diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/README.md b/v1.4/wrobot/lib/NimBLE-Arduino/README.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/README.md rename to v1.4/wrobot/lib/NimBLE-Arduino/README.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/Bluetooth 5 features.md b/v1.4/wrobot/lib/NimBLE-Arduino/docs/Bluetooth 5 features.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/Bluetooth 5 features.md rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/Bluetooth 5 features.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/Command_line_config.md b/v1.4/wrobot/lib/NimBLE-Arduino/docs/Command_line_config.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/Command_line_config.md rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/Command_line_config.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/Doxyfile b/v1.4/wrobot/lib/NimBLE-Arduino/docs/Doxyfile similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/Doxyfile rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/Doxyfile diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/Improvements_and_updates.md b/v1.4/wrobot/lib/NimBLE-Arduino/docs/Improvements_and_updates.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/Improvements_and_updates.md rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/Improvements_and_updates.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/Migration_guide.md b/v1.4/wrobot/lib/NimBLE-Arduino/docs/Migration_guide.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/Migration_guide.md rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/Migration_guide.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/New_user_guide.md b/v1.4/wrobot/lib/NimBLE-Arduino/docs/New_user_guide.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/New_user_guide.md rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/New_user_guide.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/Usage_tips.md b/v1.4/wrobot/lib/NimBLE-Arduino/docs/Usage_tips.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/Usage_tips.md rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/Usage_tips.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/docs/index.md b/v1.4/wrobot/lib/NimBLE-Arduino/docs/index.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/docs/index.md rename to v1.4/wrobot/lib/NimBLE-Arduino/docs/index.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.md b/v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.md rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_Beacon_Scanner/BLE_Beacon_Scanner.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.md b/v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.md rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneTLM_Beacon/BLE_EddystoneTLM_Beacon.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.md b/v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.md rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/BLE_EddystoneURL_Beacon/BLE_EddystoneURL_Beacon.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_client/NimBLE_extended_client.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_client/NimBLE_extended_client.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_client/NimBLE_extended_client.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_client/NimBLE_extended_client.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_server/NimBLE_extended_server.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_server/NimBLE_extended_server.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_server/NimBLE_extended_server.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_extended_server/NimBLE_extended_server.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_multi_advertiser/NimBLE_multi_advertiser.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_multi_advertiser/NimBLE_multi_advertiser.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_multi_advertiser/NimBLE_multi_advertiser.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Bluetooth_5/NimBLE_multi_advertiser/NimBLE_multi_advertiser.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Whitelist/NimBLE_Scan_whitelist.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Whitelist/NimBLE_Scan_whitelist.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Whitelist/NimBLE_Scan_whitelist.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Scan_Whitelist/NimBLE_Scan_whitelist.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server/NimBLE_Server.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server/NimBLE_Server.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server/NimBLE_Server.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server/NimBLE_Server.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server_Whitelist/NimBLE_Server_Whitelist.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server_Whitelist/NimBLE_Server_Whitelist.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server_Whitelist/NimBLE_Server_Whitelist.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Server_Whitelist/NimBLE_Server_Whitelist.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Service_Data_Advertiser/NimBLE_Service_Data_Advertiser.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Service_Data_Advertiser/NimBLE_Service_Data_Advertiser.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Service_Data_Advertiser/NimBLE_Service_Data_Advertiser.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/NimBLE_Service_Data_Advertiser/NimBLE_Service_Data_Advertiser.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_client/BLE_client.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_client/BLE_client.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_client/BLE_client.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_client/BLE_client.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_iBeacon/BLE_iBeacon.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_iBeacon/BLE_iBeacon.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_iBeacon/BLE_iBeacon.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_iBeacon/BLE_iBeacon.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_scan/BLE_scan.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_scan/BLE_scan.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_scan/BLE_scan.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_scan/BLE_scan.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_uart/BLE_uart.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_uart/BLE_uart.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_uart/BLE_uart.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_uart/BLE_uart.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_write/BLE_write.ino b/v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_write/BLE_write.ino similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_write/BLE_write.ino rename to v1.4/wrobot/lib/NimBLE-Arduino/examples/Refactored_original_examples/BLE_write/BLE_write.ino diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/library.properties b/v1.4/wrobot/lib/NimBLE-Arduino/library.properties similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/library.properties rename to v1.4/wrobot/lib/NimBLE-Arduino/library.properties diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/HIDKeyboardTypes.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/HIDKeyboardTypes.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/HIDKeyboardTypes.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/HIDKeyboardTypes.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/HIDTypes.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/HIDTypes.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/HIDTypes.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/HIDTypes.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLE2904.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAddress.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertisedDevice.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAdvertising.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAttValue.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAttValue.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEAttValue.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEAttValue.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEBeacon.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLECharacteristic.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEClient.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEConnInfo.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEConnInfo.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEConnInfo.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEConnInfo.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDescriptor.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEDevice.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneTLM.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEEddystoneURL.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEExtAdvertising.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEHIDDevice.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLELog.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLELog.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLELog.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLELog.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteCharacteristic.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteDescriptor.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLERemoteService.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEScan.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLESecurity.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEServer.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEService.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEService.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEService.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEService.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEService.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEService.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEService.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEService.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUUID.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.cpp b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.cpp similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.cpp rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.cpp diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/NimBLEUtils.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/CODING_STANDARDS.md b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/CODING_STANDARDS.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/CODING_STANDARDS.md rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/CODING_STANDARDS.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/LICENSE b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/LICENSE similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/LICENSE rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/LICENSE diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/NOTICE b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/NOTICE similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/NOTICE rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/NOTICE diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/README.md b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/README.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/README.md rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/README.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/RELEASE_NOTES.md b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/RELEASE_NOTES.md similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/RELEASE_NOTES.md rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/RELEASE_NOTES.md diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/console/console.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/console/console.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/console/console.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/console/console.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_compiler.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_compiler.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_compiler.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_compiler.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_nimble_hci.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_nimble_hci.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_nimble_hci.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/include/esp_nimble_hci.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/src/esp_nimble_hci.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/src/esp_nimble_hci.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/src/esp_nimble_hci.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/esp-hci/src/esp_nimble_hci.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_cfg.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_cfg.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_cfg.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_cfg.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_mem.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_mem.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_mem.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/include/esp_nimble_mem.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/src/esp_nimble_mem.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/src/esp_nimble_mem.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/src/esp_nimble_mem.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/esp_port/port/src/esp_nimble_mem.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/AUTHORS b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/AUTHORS similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/AUTHORS rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/AUTHORS diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/LICENSE b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/LICENSE similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/LICENSE rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/LICENSE diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/README b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/README similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/README rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/README diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/VERSION b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/VERSION similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/VERSION rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/VERSION diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/documentation/tinycrypt.rst b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/documentation/tinycrypt.rst similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/documentation/tinycrypt.rst rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/documentation/tinycrypt.rst diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/aes.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/aes.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/aes.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/aes.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cbc_mode.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cbc_mode.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cbc_mode.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cbc_mode.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ccm_mode.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ccm_mode.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ccm_mode.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ccm_mode.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cmac_mode.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cmac_mode.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cmac_mode.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/cmac_mode.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/constants.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/constants.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/constants.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/constants.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_mode.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_mode.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_mode.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_mode.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_prng.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_prng.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_prng.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ctr_prng.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dh.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dh.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dh.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dh.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dsa.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dsa.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dsa.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_dsa.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_platform_specific.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_platform_specific.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_platform_specific.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/ecc_platform_specific.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac_prng.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac_prng.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac_prng.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/hmac_prng.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/sha256.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/sha256.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/sha256.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/sha256.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/utils.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/utils.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/utils.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/include/tinycrypt/utils.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_decrypt.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_decrypt.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_decrypt.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_decrypt.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_encrypt.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_encrypt.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_encrypt.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/aes_encrypt.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cbc_mode.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cbc_mode.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cbc_mode.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cbc_mode.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ccm_mode.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ccm_mode.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ccm_mode.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ccm_mode.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cmac_mode.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cmac_mode.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cmac_mode.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/cmac_mode.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_mode.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_mode.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_mode.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_mode.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_prng.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_prng.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_prng.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ctr_prng.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dh.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dh.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dh.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dh.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dsa.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dsa.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dsa.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_dsa.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_platform_specific.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_platform_specific.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_platform_specific.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/ecc_platform_specific.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac_prng.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac_prng.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac_prng.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/hmac_prng.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/sha256.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/sha256.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/sha256.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/sha256.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/utils.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/utils.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/utils.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/ext/tinycrypt/src/utils.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_hw.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_hw.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_hw.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_hw.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_adv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_adv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_adv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_adv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_conn.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_conn.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_conn.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_conn.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_ctrl.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_ctrl.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_ctrl.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_ctrl.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_hci.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_hci.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_hci.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_hci.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_iso.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_iso.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_iso.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_iso.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_resolv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_resolv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_resolv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_resolv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_rfmgmt.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_rfmgmt.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_rfmgmt.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_rfmgmt.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_scan.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_scan.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_scan.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_scan.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sched.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sched.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sched.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sched.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sync.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sync.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sync.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_sync.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_test.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_test.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_test.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_test.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_trace.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_trace.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_trace.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_trace.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_utils.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_utils.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_utils.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_utils.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_whitelist.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_whitelist.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_whitelist.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_ll_whitelist.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy_trace.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy_trace.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy_trace.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/include/controller/ble_phy_trace.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_adv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_adv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_adv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_adv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_hci.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_hci.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_hci.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_hci.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_conn_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_ctrl.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_ctrl.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_ctrl.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_ctrl.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_dtm_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci_ev.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci_ev.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci_ev.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_hci_ev.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_iso.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_iso.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_iso.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_iso.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rand.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rand.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rand.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rand.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_resolv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_resolv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_resolv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_resolv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rfmgmt.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rfmgmt.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rfmgmt.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_rfmgmt.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_scan.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_scan.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_scan.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_scan.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sched.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sched.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sched.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sched.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_supp_cmd.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_supp_cmd.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_supp_cmd.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_supp_cmd.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sync.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sync.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sync.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_sync.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_trace.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_trace.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_trace.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_trace.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_utils.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_utils.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_utils.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_utils.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_whitelist.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_whitelist.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_whitelist.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/controller/src/ble_ll_whitelist.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/include/ble/xcvr.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/include/ble/xcvr.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/include/ble/xcvr.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/include/ble/xcvr.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_hw.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_hw.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_hw.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_hw.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_phy.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_phy.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_phy.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf51/src/ble_phy.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/include/ble/xcvr.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/include/ble/xcvr.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/include/ble/xcvr.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/include/ble/xcvr.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_hw.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_hw.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_hw.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_hw.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy_trace.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy_trace.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy_trace.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/drivers/nrf52/src/ble_phy_trace.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_att.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_att.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_att.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_att.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_eddystone.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_eddystone.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_eddystone.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_eddystone.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gap.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gap.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gap.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gap.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gatt.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gatt.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gatt.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_gatt.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_adv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_adv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_adv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_adv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_hci.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_hci.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_hci.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_hci.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_id.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_id.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_id.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_id.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_log.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_log.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_log.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_log.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_mbuf.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_mbuf.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_mbuf.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_mbuf.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_pvcy.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_pvcy.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_pvcy.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_pvcy.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_stop.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_stop.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_stop.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_hs_stop.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_ibeacon.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_ibeacon.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_ibeacon.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_ibeacon.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_l2cap.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_l2cap.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_l2cap.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_l2cap.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_monitor.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_monitor.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_monitor.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_monitor.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_sm.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_sm.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_sm.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_sm.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_store.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_store.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_store.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_store.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_uuid.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_uuid.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_uuid.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/include/host/ble_uuid.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/access.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/access.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/access.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/access.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/atomic.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/atomic.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/atomic.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/atomic.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cdb.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cdb.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cdb.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cdb.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_cli.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_cli.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_cli.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_cli.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_srv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_srv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_srv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/cfg_srv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/glue.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/glue.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/glue.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/glue.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_cli.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_cli.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_cli.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_cli.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_srv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_srv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_srv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/health_srv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/heartbeat.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/heartbeat.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/heartbeat.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/heartbeat.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/main.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/main.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/main.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/main.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/mesh.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/mesh.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/mesh.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/mesh.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_cli.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_cli.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_cli.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_cli.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_srv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_srv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_srv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/model_srv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/porting.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/porting.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/porting.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/porting.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/proxy.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/proxy.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/proxy.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/proxy.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/slist.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/slist.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/slist.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/slist.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/testing.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/testing.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/testing.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/include/mesh/testing.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/access.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/adv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/aes-ccm.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/aes-ccm.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/aes-ccm.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/aes-ccm.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/app_keys.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/atomic.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/atomic.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/atomic.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/atomic.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/beacon.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cdb.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cdb.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cdb.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cdb.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_cli.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_cli.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_cli.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_cli.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_srv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_srv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_srv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/cfg_srv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/crypto.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/foundation.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/foundation.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/foundation.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/foundation.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/friend.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/glue.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/glue.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/glue.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/glue.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_cli.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_cli.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_cli.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_cli.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_srv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_srv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_srv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/health_srv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/heartbeat.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/light_model.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/lpn.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/mesh_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_cli.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_cli.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_cli.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_cli.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_srv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_srv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_srv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/model_srv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/net.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/nodes.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_adv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_adv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_adv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_adv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_gatt.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_gatt.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_gatt.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/pb_gatt.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_bearer.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_bearer.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_bearer.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_bearer.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_device.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_device.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_device.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/prov_device.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/provisioner.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/proxy.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/rpl.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/settings.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/shell.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/subnet.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/mesh/src/transport.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/include/services/ans/ble_svc_ans.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/include/services/ans/ble_svc_ans.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/include/services/ans/ble_svc_ans.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/include/services/ans/ble_svc_ans.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/src/ble_svc_ans.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/src/ble_svc_ans.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/src/ble_svc_ans.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ans/src/ble_svc_ans.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/include/services/bas/ble_svc_bas.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/include/services/bas/ble_svc_bas.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/include/services/bas/ble_svc_bas.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/include/services/bas/ble_svc_bas.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/src/ble_svc_bas.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/src/ble_svc_bas.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/src/ble_svc_bas.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/bas/src/ble_svc_bas.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/include/services/dis/ble_svc_dis.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/include/services/dis/ble_svc_dis.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/include/services/dis/ble_svc_dis.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/include/services/dis/ble_svc_dis.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/src/ble_svc_dis.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/src/ble_svc_dis.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/src/ble_svc_dis.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/dis/src/ble_svc_dis.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/include/services/gap/ble_svc_gap.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/include/services/gap/ble_svc_gap.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/include/services/gap/ble_svc_gap.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/include/services/gap/ble_svc_gap.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/src/ble_svc_gap.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/src/ble_svc_gap.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/src/ble_svc_gap.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gap/src/ble_svc_gap.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/src/ble_svc_gatt.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/src/ble_svc_gatt.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/src/ble_svc_gatt.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/gatt/src/ble_svc_gatt.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/include/services/ias/ble_svc_ias.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/include/services/ias/ble_svc_ias.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/include/services/ias/ble_svc_ias.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/include/services/ias/ble_svc_ias.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/src/ble_svc_ias.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/src/ble_svc_ias.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/src/ble_svc_ias.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ias/src/ble_svc_ias.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/include/services/ipss/ble_svc_ipss.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/include/services/ipss/ble_svc_ipss.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/include/services/ipss/ble_svc_ipss.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/include/services/ipss/ble_svc_ipss.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/src/ble_svc_ipss.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/src/ble_svc_ipss.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/src/ble_svc_ipss.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/ipss/src/ble_svc_ipss.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/include/services/lls/ble_svc_lls.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/include/services/lls/ble_svc_lls.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/include/services/lls/ble_svc_lls.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/include/services/lls/ble_svc_lls.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/src/ble_svc_lls.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/src/ble_svc_lls.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/src/ble_svc_lls.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/services/lls/src/ble_svc_lls.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_clt.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_clt.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_clt.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_clt.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_cmd_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_svr.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_svr.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_svr.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_att_svr.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_eddystone.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_eddystone.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_eddystone.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_eddystone.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gap_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatt_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatt_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatt_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatt_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gattc.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gattc.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gattc.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gattc.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts_lcl.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts_lcl.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts_lcl.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_gatts_lcl.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_adv_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_atomic_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_cfg.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_cfg.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_cfg.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_cfg.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_conn_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_flow_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_cmd.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_cmd.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_cmd.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_cmd.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_evt.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_evt.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_evt.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_evt.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_util.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_util.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_util.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_hci_util.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_id_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_log.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_log.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_log.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_log.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mbuf_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_misc.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_misc.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_misc.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_misc.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mqueue.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mqueue.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mqueue.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_mqueue.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_periodic_sync_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_pvcy_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_resolv_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_shutdown.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_shutdown.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_shutdown.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_shutdown.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_startup_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_stop.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_stop.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_stop.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_hs_stop.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_ibeacon.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_ibeacon.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_ibeacon.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_ibeacon.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_coc_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_cmd.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_cmd.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_cmd.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_cmd.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_l2cap_sig_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_monitor_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_alg.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_alg.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_alg.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_alg.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_cmd.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_cmd.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_cmd.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_cmd.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_lgcy.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_lgcy.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_lgcy.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_lgcy.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_sc.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_sc.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_sc.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_sm_sc.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store_util.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store_util.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store_util.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_store_util.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/src/ble_uuid_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/include/store/config/ble_store_config.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/include/store/config/ble_store_config.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/include/store/config/ble_store_config.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/include/store/config/ble_store_config.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_conf.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_conf.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_conf.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_conf.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_priv.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_priv.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_priv.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_config_priv.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_nvs.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_nvs.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_nvs.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/store/config/src/ble_store_nvs.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/include/host/util/util.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/include/host/util/util.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/include/host/util/util.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/include/host/util/util.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/src/addr.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/src/addr.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/src/addr.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/host/util/src/addr.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble_hci_trans.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble_hci_trans.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble_hci_trans.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/ble_hci_trans.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/hci_common.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/hci_common.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/hci_common.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/hci_common.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_npl.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_npl.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_npl.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_npl.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt_auto.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt_auto.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt_auto.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/include/nimble/nimble_opt_auto.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/include/transport/ram/ble_hci_ram.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/include/transport/ram/ble_hci_ram.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/include/transport/ram/ble_hci_ram.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/include/transport/ram/ble_hci_ram.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/src/ble_hci_ram.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/src/ble_hci_ram.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/src/ble_hci_ram.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/nimble/transport/ram/src/ble_hci_ram.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/hal/hal_timer.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/hal/hal_timer.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/hal/hal_timer.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/hal/hal_timer.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log/log.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log/log.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log/log.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log/log.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/ignore.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/ignore.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/ignore.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/ignore.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/log_common.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/log_common.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/log_common.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/log_common/log_common.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/logcfg/logcfg.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/logcfg/logcfg.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/logcfg/logcfg.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/logcfg/logcfg.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/mem/mem.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/mem/mem.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/mem/mem.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/mem/mem.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/modlog/modlog.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/modlog/modlog.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/modlog/modlog.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/modlog/modlog.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/nimble/nimble_port.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/nimble/nimble_port.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/nimble/nimble_port.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/nimble/nimble_port.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/endian.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/endian.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/endian.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/endian.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_cputime.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_cputime.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_cputime.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_cputime.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_error.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_error.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_error.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_error.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mbuf.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mbuf.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mbuf.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mbuf.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mempool.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mempool.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mempool.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_mempool.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_trace_api.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_trace_api.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_trace_api.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/os_trace_api.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/queue.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/queue.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/queue.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/queue.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/util.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/util.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/util.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/os/util.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/stats/stats.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/stats/stats.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/stats/stats.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/stats/stats.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/syscfg/syscfg.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/syscfg/syscfg.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/syscfg/syscfg.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/syscfg/syscfg.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysflash/sysflash.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysflash/sysflash.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysflash/sysflash.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysflash/sysflash.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysinit/sysinit.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysinit/sysinit.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysinit/sysinit.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/include/sysinit/sysinit.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/endian.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/endian.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/endian.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/endian.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_timer.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_timer.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_timer.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_timer.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_uart.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_uart.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_uart.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/hal_uart.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/mem.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/mem.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/mem.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/mem.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/nimble_port.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/nimble_port.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/nimble_port.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/nimble_port.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime_pwr2.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime_pwr2.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime_pwr2.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_cputime_pwr2.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mbuf.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mbuf.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mbuf.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mbuf.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mempool.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mempool.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mempool.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_mempool.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_msys_init.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_msys_init.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_msys_init.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/nimble/src/os_msys_init.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_npl_os.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_npl_os.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_npl_os.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_npl_os.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_port_freertos.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_port_freertos.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_port_freertos.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/nimble_port_freertos.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/npl_freertos.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/npl_freertos.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/npl_freertos.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/include/nimble/npl_freertos.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/nimble_port_freertos.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/nimble_port_freertos.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/nimble_port_freertos.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/nimble_port_freertos.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/npl_os_freertos.c b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/npl_os_freertos.c similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/npl_os_freertos.c rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimble/porting/npl/freertos/src/npl_os_freertos.c diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimconfig.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimconfig.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimconfig.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimconfig.h diff --git a/v2.0/wrobot/lib/NimBLE-Arduino/src/nimconfig_rename.h b/v1.4/wrobot/lib/NimBLE-Arduino/src/nimconfig_rename.h similarity index 100% rename from v2.0/wrobot/lib/NimBLE-Arduino/src/nimconfig_rename.h rename to v1.4/wrobot/lib/NimBLE-Arduino/src/nimconfig_rename.h diff --git a/v2.0/wrobot/lib/README b/v1.4/wrobot/lib/README similarity index 100% rename from v2.0/wrobot/lib/README rename to v1.4/wrobot/lib/README diff --git a/v2.0/wrobot/lib/SCServo/INST.h b/v1.4/wrobot/lib/SCServo/INST.h similarity index 100% rename from v2.0/wrobot/lib/SCServo/INST.h rename to v1.4/wrobot/lib/SCServo/INST.h diff --git a/v2.0/wrobot/lib/SCServo/SCS.cpp b/v1.4/wrobot/lib/SCServo/SCS.cpp similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCS.cpp rename to v1.4/wrobot/lib/SCServo/SCS.cpp diff --git a/v2.0/wrobot/lib/SCServo/SCS.h b/v1.4/wrobot/lib/SCServo/SCS.h similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCS.h rename to v1.4/wrobot/lib/SCServo/SCS.h diff --git a/v2.0/wrobot/lib/SCServo/SCSCL.cpp b/v1.4/wrobot/lib/SCServo/SCSCL.cpp similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCSCL.cpp rename to v1.4/wrobot/lib/SCServo/SCSCL.cpp diff --git a/v2.0/wrobot/lib/SCServo/SCSCL.h b/v1.4/wrobot/lib/SCServo/SCSCL.h similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCSCL.h rename to v1.4/wrobot/lib/SCServo/SCSCL.h diff --git a/v2.0/wrobot/lib/SCServo/SCSerial.cpp b/v1.4/wrobot/lib/SCServo/SCSerial.cpp similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCSerial.cpp rename to v1.4/wrobot/lib/SCServo/SCSerial.cpp diff --git a/v2.0/wrobot/lib/SCServo/SCSerial.h b/v1.4/wrobot/lib/SCServo/SCSerial.h similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCSerial.h rename to v1.4/wrobot/lib/SCServo/SCSerial.h diff --git a/v2.0/wrobot/lib/SCServo/SCServo.h b/v1.4/wrobot/lib/SCServo/SCServo.h similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCServo.h rename to v1.4/wrobot/lib/SCServo/SCServo.h diff --git a/v2.0/wrobot/lib/SCServo/SCServo.txt b/v1.4/wrobot/lib/SCServo/SCServo.txt similarity index 100% rename from v2.0/wrobot/lib/SCServo/SCServo.txt rename to v1.4/wrobot/lib/SCServo/SCServo.txt diff --git a/v2.0/wrobot/lib/SCServo/SMS_STS.cpp b/v1.4/wrobot/lib/SCServo/SMS_STS.cpp similarity index 100% rename from v2.0/wrobot/lib/SCServo/SMS_STS.cpp rename to v1.4/wrobot/lib/SCServo/SMS_STS.cpp diff --git a/v2.0/wrobot/lib/SCServo/SMS_STS.h b/v1.4/wrobot/lib/SCServo/SMS_STS.h similarity index 100% rename from v2.0/wrobot/lib/SCServo/SMS_STS.h rename to v1.4/wrobot/lib/SCServo/SMS_STS.h diff --git a/v2.0/wrobot/lib/SCServo/examples/SCSCL/Broadcast/Broadcast.ino b/v1.4/wrobot/lib/SCServo/examples/SCSCL/Broadcast/Broadcast.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/SCSCL/Broadcast/Broadcast.ino rename to v1.4/wrobot/lib/SCServo/examples/SCSCL/Broadcast/Broadcast.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/SCSCL/FeedBack/FeedBack.ino b/v1.4/wrobot/lib/SCServo/examples/SCSCL/FeedBack/FeedBack.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/SCSCL/FeedBack/FeedBack.ino rename to v1.4/wrobot/lib/SCServo/examples/SCSCL/FeedBack/FeedBack.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/SCSCL/Ping/Ping.ino b/v1.4/wrobot/lib/SCServo/examples/SCSCL/Ping/Ping.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/SCSCL/Ping/Ping.ino rename to v1.4/wrobot/lib/SCServo/examples/SCSCL/Ping/Ping.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/SCSCL/ProgramEprom/ProgramEprom.ino b/v1.4/wrobot/lib/SCServo/examples/SCSCL/ProgramEprom/ProgramEprom.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/SCSCL/ProgramEprom/ProgramEprom.ino rename to v1.4/wrobot/lib/SCServo/examples/SCSCL/ProgramEprom/ProgramEprom.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/SCSCL/RegWritePos/RegWritePos.ino b/v1.4/wrobot/lib/SCServo/examples/SCSCL/RegWritePos/RegWritePos.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/SCSCL/RegWritePos/RegWritePos.ino rename to v1.4/wrobot/lib/SCServo/examples/SCSCL/RegWritePos/RegWritePos.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/SCSCL/SyncWritePos/SyncWritePos.ino b/v1.4/wrobot/lib/SCServo/examples/SCSCL/SyncWritePos/SyncWritePos.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/SCSCL/SyncWritePos/SyncWritePos.ino rename to v1.4/wrobot/lib/SCServo/examples/SCSCL/SyncWritePos/SyncWritePos.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/SCSCL/WritePos/WritePos.ino b/v1.4/wrobot/lib/SCServo/examples/SCSCL/WritePos/WritePos.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/SCSCL/WritePos/WritePos.ino rename to v1.4/wrobot/lib/SCServo/examples/SCSCL/WritePos/WritePos.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/STSCL/CalibrationOfs/CalibrationOfs.ino b/v1.4/wrobot/lib/SCServo/examples/STSCL/CalibrationOfs/CalibrationOfs.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/STSCL/CalibrationOfs/CalibrationOfs.ino rename to v1.4/wrobot/lib/SCServo/examples/STSCL/CalibrationOfs/CalibrationOfs.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/STSCL/FeedBack/FeedBack.ino b/v1.4/wrobot/lib/SCServo/examples/STSCL/FeedBack/FeedBack.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/STSCL/FeedBack/FeedBack.ino rename to v1.4/wrobot/lib/SCServo/examples/STSCL/FeedBack/FeedBack.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/STSCL/Ping/Ping.ino b/v1.4/wrobot/lib/SCServo/examples/STSCL/Ping/Ping.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/STSCL/Ping/Ping.ino rename to v1.4/wrobot/lib/SCServo/examples/STSCL/Ping/Ping.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/STSCL/ProgramEprom/ProgramEprom.ino b/v1.4/wrobot/lib/SCServo/examples/STSCL/ProgramEprom/ProgramEprom.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/STSCL/ProgramEprom/ProgramEprom.ino rename to v1.4/wrobot/lib/SCServo/examples/STSCL/ProgramEprom/ProgramEprom.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/STSCL/RegWritePos/RegWritePos.ino b/v1.4/wrobot/lib/SCServo/examples/STSCL/RegWritePos/RegWritePos.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/STSCL/RegWritePos/RegWritePos.ino rename to v1.4/wrobot/lib/SCServo/examples/STSCL/RegWritePos/RegWritePos.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/STSCL/SyncWritePos/SyncWritePos.ino b/v1.4/wrobot/lib/SCServo/examples/STSCL/SyncWritePos/SyncWritePos.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/STSCL/SyncWritePos/SyncWritePos.ino rename to v1.4/wrobot/lib/SCServo/examples/STSCL/SyncWritePos/SyncWritePos.ino diff --git a/v2.0/wrobot/lib/SCServo/examples/STSCL/WritePos/WritePos.ino b/v1.4/wrobot/lib/SCServo/examples/STSCL/WritePos/WritePos.ino similarity index 100% rename from v2.0/wrobot/lib/SCServo/examples/STSCL/WritePos/WritePos.ino rename to v1.4/wrobot/lib/SCServo/examples/STSCL/WritePos/WritePos.ino diff --git a/v2.0/wrobot/lib/SCServo/library.properties b/v1.4/wrobot/lib/SCServo/library.properties similarity index 100% rename from v2.0/wrobot/lib/SCServo/library.properties rename to v1.4/wrobot/lib/SCServo/library.properties diff --git a/v2.0/wrobot/lib/Simple_FOC/LICENSE b/v1.4/wrobot/lib/Simple_FOC/LICENSE similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/LICENSE rename to v1.4/wrobot/lib/Simple_FOC/LICENSE diff --git a/v2.0/wrobot/lib/Simple_FOC/README.md b/v1.4/wrobot/lib/Simple_FOC/README.md similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/README.md rename to v1.4/wrobot/lib/Simple_FOC/README.md diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/README.md b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/README.md similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/README.md rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/README.md diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino b/v1.4/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/README.md b/v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/README.md similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/README.md rename to v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/README.md diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc b/v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc rename to v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino b/v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino b/v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd b/v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd rename to v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me b/v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me rename to v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_fullcontrol_screenshot.png b/v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_fullcontrol_screenshot.png similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_fullcontrol_screenshot.png rename to v1.4/wrobot/lib/Simple_FOC/examples/osc_control_examples/osc_fullcontrol_screenshot.png diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensors_software_interrupts_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensors_software_interrupts_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensors_software_interrupts_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensors_software_interrupts_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino b/v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino rename to v1.4/wrobot/lib/Simple_FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino diff --git a/v2.0/wrobot/lib/Simple_FOC/keywords.txt b/v1.4/wrobot/lib/Simple_FOC/keywords.txt similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/keywords.txt rename to v1.4/wrobot/lib/Simple_FOC/keywords.txt diff --git a/v2.0/wrobot/lib/Simple_FOC/library.properties b/v1.4/wrobot/lib/Simple_FOC/library.properties similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/library.properties rename to v1.4/wrobot/lib/Simple_FOC/library.properties diff --git a/v2.0/wrobot/lib/Simple_FOC/src/BLDCMotor.cpp b/v1.4/wrobot/lib/Simple_FOC/src/BLDCMotor.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/BLDCMotor.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/BLDCMotor.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/BLDCMotor.h b/v1.4/wrobot/lib/Simple_FOC/src/BLDCMotor.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/BLDCMotor.h rename to v1.4/wrobot/lib/Simple_FOC/src/BLDCMotor.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/SimpleFOC.h b/v1.4/wrobot/lib/Simple_FOC/src/SimpleFOC.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/SimpleFOC.h rename to v1.4/wrobot/lib/Simple_FOC/src/SimpleFOC.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/StepperMotor.cpp b/v1.4/wrobot/lib/Simple_FOC/src/StepperMotor.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/StepperMotor.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/StepperMotor.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/StepperMotor.h b/v1.4/wrobot/lib/Simple_FOC/src/StepperMotor.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/StepperMotor.h rename to v1.4/wrobot/lib/Simple_FOC/src/StepperMotor.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/BLDCDriver.h b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/BLDCDriver.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/BLDCDriver.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/BLDCDriver.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.cpp b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.h b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/CurrentSense.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.cpp b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.h b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/FOCMotor.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.cpp b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.h b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/Sensor.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/StepperDriver.h b/v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/StepperDriver.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/base_classes/StepperDriver.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/base_classes/StepperDriver.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/defaults.h b/v1.4/wrobot/lib/Simple_FOC/src/common/defaults.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/defaults.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/defaults.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/foc_utils.cpp b/v1.4/wrobot/lib/Simple_FOC/src/common/foc_utils.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/foc_utils.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/common/foc_utils.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/foc_utils.h b/v1.4/wrobot/lib/Simple_FOC/src/common/foc_utils.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/foc_utils.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/foc_utils.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/lowpass_filter.cpp b/v1.4/wrobot/lib/Simple_FOC/src/common/lowpass_filter.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/lowpass_filter.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/common/lowpass_filter.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/lowpass_filter.h b/v1.4/wrobot/lib/Simple_FOC/src/common/lowpass_filter.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/lowpass_filter.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/lowpass_filter.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/pid.cpp b/v1.4/wrobot/lib/Simple_FOC/src/common/pid.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/pid.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/common/pid.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/pid.h b/v1.4/wrobot/lib/Simple_FOC/src/common/pid.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/pid.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/pid.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/time_utils.cpp b/v1.4/wrobot/lib/Simple_FOC/src/common/time_utils.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/time_utils.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/common/time_utils.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/common/time_utils.h b/v1.4/wrobot/lib/Simple_FOC/src/common/time_utils.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/common/time_utils.h rename to v1.4/wrobot/lib/Simple_FOC/src/common/time_utils.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/communication/Commander.cpp b/v1.4/wrobot/lib/Simple_FOC/src/communication/Commander.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/communication/Commander.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/communication/Commander.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/communication/Commander.h b/v1.4/wrobot/lib/Simple_FOC/src/communication/Commander.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/communication/Commander.h rename to v1.4/wrobot/lib/Simple_FOC/src/communication/Commander.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/communication/StepDirListener.cpp b/v1.4/wrobot/lib/Simple_FOC/src/communication/StepDirListener.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/communication/StepDirListener.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/communication/StepDirListener.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/communication/StepDirListener.h b/v1.4/wrobot/lib/Simple_FOC/src/communication/StepDirListener.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/communication/StepDirListener.h rename to v1.4/wrobot/lib/Simple_FOC/src/communication/StepDirListener.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/communication/commands.h b/v1.4/wrobot/lib/Simple_FOC/src/communication/commands.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/communication/commands.h rename to v1.4/wrobot/lib/Simple_FOC/src/communication/commands.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.h b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.h rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/GenericCurrentSense.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.h b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.h rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/InlineCurrentSense.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.h b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.h rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/LowsideCurrentSense.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_api.h b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_api.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_api.h rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_api.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/atmega_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/atmega_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/atmega_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/atmega_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/due_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/due_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/due_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/due_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.h b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.h rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_adc_driver.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_ledc_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_ledc_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_ledc_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_ledc_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/esp32_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/generic_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/generic_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/generic_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/generic_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.h b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.h rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd21_mcu.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/samd_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.h b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.h rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_hal.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/stm32g4_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/teensy_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/teensy_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/teensy_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/current_sense/hardware_specific/teensy_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.h b/v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.h rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver3PWM.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.h b/v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.h rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/BLDCDriver6PWM.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.h b/v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.h rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver2PWM.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.h b/v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.h rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/StepperDriver4PWM.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_api.h b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_api.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_api.h rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_api.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega2560_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega2560_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega2560_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega2560_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega328_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega328_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega328_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega328_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega32u4_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega32u4_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega32u4_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/atmega32u4_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/due_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/due_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/due_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/due_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_ledc_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_ledc_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_ledc_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_ledc_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp32_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp8266_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp8266_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp8266_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/esp8266_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/generic_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/generic_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/generic_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/generic_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/nrf52_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/nrf52_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/nrf52_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/nrf52_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/rp2040_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/rp2040_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/rp2040_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/rp2040_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd21_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd21_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd21_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd21_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd51_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd51_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd51_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd51_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.h b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.h rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/samd_mcu.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/stm32_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/stm32_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/stm32_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/stm32_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/teensy_mcu.cpp b/v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/teensy_mcu.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/teensy_mcu.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/drivers/hardware_specific/teensy_mcu.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/Encoder.cpp b/v1.4/wrobot/lib/Simple_FOC/src/sensors/Encoder.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/Encoder.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/Encoder.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/Encoder.h b/v1.4/wrobot/lib/Simple_FOC/src/sensors/Encoder.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/Encoder.h rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/Encoder.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.cpp b/v1.4/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.h b/v1.4/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.h rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/GenericSensor.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/HallSensor.cpp b/v1.4/wrobot/lib/Simple_FOC/src/sensors/HallSensor.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/HallSensor.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/HallSensor.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/HallSensor.h b/v1.4/wrobot/lib/Simple_FOC/src/sensors/HallSensor.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/HallSensor.h rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/HallSensor.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.cpp b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.h b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.h rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorAnalog.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.cpp b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.h b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.h rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorI2C.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.cpp b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.h b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.h rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorPWM.h diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.cpp b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.cpp similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.cpp rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.cpp diff --git a/v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.h b/v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.h similarity index 100% rename from v2.0/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.h rename to v1.4/wrobot/lib/Simple_FOC/src/sensors/MagneticSensorSPI.h diff --git a/v2.0/wrobot/platformio.ini b/v1.4/wrobot/platformio.ini similarity index 100% rename from v2.0/wrobot/platformio.ini rename to v1.4/wrobot/platformio.ini diff --git a/v2.0/wrobot/src/PreferencesManager.cpp b/v1.4/wrobot/src/PreferencesManager.cpp similarity index 100% rename from v2.0/wrobot/src/PreferencesManager.cpp rename to v1.4/wrobot/src/PreferencesManager.cpp diff --git a/v2.0/wrobot/src/PreferencesManager.h b/v1.4/wrobot/src/PreferencesManager.h similarity index 100% rename from v2.0/wrobot/src/PreferencesManager.h rename to v1.4/wrobot/src/PreferencesManager.h diff --git a/v2.0/wrobot/src/RGBController.cpp b/v1.4/wrobot/src/RGBController.cpp similarity index 100% rename from v2.0/wrobot/src/RGBController.cpp rename to v1.4/wrobot/src/RGBController.cpp diff --git a/v2.0/wrobot/src/RGBController.h b/v1.4/wrobot/src/RGBController.h similarity index 100% rename from v2.0/wrobot/src/RGBController.h rename to v1.4/wrobot/src/RGBController.h diff --git a/v2.0/wrobot/src/RobotActions.h b/v1.4/wrobot/src/RobotActions.h similarity index 100% rename from v2.0/wrobot/src/RobotActions.h rename to v1.4/wrobot/src/RobotActions.h diff --git a/v2.0/wrobot/src/SerialParser.cpp b/v1.4/wrobot/src/SerialParser.cpp similarity index 100% rename from v2.0/wrobot/src/SerialParser.cpp rename to v1.4/wrobot/src/SerialParser.cpp diff --git a/v2.0/wrobot/src/SerialParser.h b/v1.4/wrobot/src/SerialParser.h similarity index 100% rename from v2.0/wrobot/src/SerialParser.h rename to v1.4/wrobot/src/SerialParser.h diff --git a/v2.0/wrobot/src/VoltageMonitor.cpp b/v1.4/wrobot/src/VoltageMonitor.cpp similarity index 100% rename from v2.0/wrobot/src/VoltageMonitor.cpp rename to v1.4/wrobot/src/VoltageMonitor.cpp diff --git a/v2.0/wrobot/src/VoltageMonitor.h b/v1.4/wrobot/src/VoltageMonitor.h similarity index 100% rename from v2.0/wrobot/src/VoltageMonitor.h rename to v1.4/wrobot/src/VoltageMonitor.h diff --git a/v2.0/wrobot/src/XboxBluetooth.h b/v1.4/wrobot/src/XboxBluetooth.h similarity index 100% rename from v2.0/wrobot/src/XboxBluetooth.h rename to v1.4/wrobot/src/XboxBluetooth.h diff --git a/v2.0/wrobot/src/XboxControllerNotificationParser.cpp b/v1.4/wrobot/src/XboxControllerNotificationParser.cpp similarity index 100% rename from v2.0/wrobot/src/XboxControllerNotificationParser.cpp rename to v1.4/wrobot/src/XboxControllerNotificationParser.cpp diff --git a/v2.0/wrobot/src/XboxControllerNotificationParser.h b/v1.4/wrobot/src/XboxControllerNotificationParser.h similarity index 100% rename from v2.0/wrobot/src/XboxControllerNotificationParser.h rename to v1.4/wrobot/src/XboxControllerNotificationParser.h diff --git a/v2.0/wrobot/src/XboxSeriesXControllerESP32_asukiaaa.hpp b/v1.4/wrobot/src/XboxSeriesXControllerESP32_asukiaaa.hpp similarity index 100% rename from v2.0/wrobot/src/XboxSeriesXControllerESP32_asukiaaa.hpp rename to v1.4/wrobot/src/XboxSeriesXControllerESP32_asukiaaa.hpp diff --git a/v2.0/wrobot/src/XboxSeriesXHIDReportBuilder_asukiaaa.hpp b/v1.4/wrobot/src/XboxSeriesXHIDReportBuilder_asukiaaa.hpp similarity index 100% rename from v2.0/wrobot/src/XboxSeriesXHIDReportBuilder_asukiaaa.hpp rename to v1.4/wrobot/src/XboxSeriesXHIDReportBuilder_asukiaaa.hpp diff --git a/v2.0/wrobot/src/main.cpp b/v1.4/wrobot/src/main.cpp similarity index 100% rename from v2.0/wrobot/src/main.cpp rename to v1.4/wrobot/src/main.cpp diff --git a/v1.5/.gitignore b/v1.5/.gitignore new file mode 100644 index 0000000..c4f2dba --- /dev/null +++ b/v1.5/.gitignore @@ -0,0 +1,6 @@ +.pio +.test +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/v1.5/README.md b/v1.5/README.md new file mode 100644 index 0000000..d19efab --- /dev/null +++ b/v1.5/README.md @@ -0,0 +1,26 @@ +此项目是世博同学轮足机器人的Arduino工程转换而成的PlatformIO工程 + +可以使用vscode进行编译下载,解决了Arduino编译巨慢的问题。 + +世博同学轮足机器人原项目地址:https://github.com/MuShibo/Micro-Wheeled_leg-Robot +世博开源轮足机器人TOMMY化版项目地址: https://gitee.com/myperworld/wrobot + +2025-08-16 优化了原世博开源轮足平衡逻辑,代码增加了较多备注。 +2025-08-11 增加开机自平衡站立,简化控制,通过 esp32 遥控器直接控制,无需配网。 +2025-08-16 对几种运动中倒地的极限状态进行优化,实现自平衡起身。 +2025-09-14 增加坐下和起立功能。 +2025-09-16 修复舵机发热问题。 +2025-09-19 实现向前跳跃,运动中跳跃,落地平衡功能。 +2025-09-29 将舵机标定2048位置统一为大腿与小腿碰到一起的位置 +2025-10-01 增加rgb彩灯测试功能 +2025-10-09 重新适配更加丰富的遥控器 + 优化前进控制系数,消除转向的 “累加记忆” + 智能调整机身高度 + 转弯左右摆动角度 + 跳跃动作前增加蓄能 + 增加原地向前、后、左、右跳动动作 + 完善坐下、起立,尤其是坐下后使舵机失能,更加省点 + 删除了原来webservice、websocket功能,不能通过小车热点连接网页发送指令 + 删除原先世博封装的舵机代码,采用飞特厂提供的原生代码库,可以使用更多舵机函数,如设置速度、加速度等。 + 删除了其他无用页面和代码,只保留main.cpp核心页面。 + diff --git a/v1.5/lib/FastLED/ADVANCED_DEVELOPMENT.md b/v1.5/lib/FastLED/ADVANCED_DEVELOPMENT.md new file mode 100644 index 0000000..b864198 --- /dev/null +++ b/v1.5/lib/FastLED/ADVANCED_DEVELOPMENT.md @@ -0,0 +1,66 @@ +# Advanced Development with FastLED + + +![perpetualmaniac_neo_giving_two_pills _one_of_them_is_black_and__4b145870-9ead-4976-b031-f4df3f2bfbe1](https://github.com/user-attachments/assets/9bba6113-688f-469f-8b51-bcb4fea910e5) + + +## GDB On Unit Tests + +Yes, we have step through debugging with FastLED. + + * VSCode + * Install Plugin: GDB Debugger - Beyond + * Navigate to one of the tests in `tests/` and open in + * Hit `F5` + +If the Python Debugger pops up, then manually switch the VSCode debugger using `Launch(gdb)` + +![image](https://github.com/user-attachments/assets/c1246803-df4b-4583-8c11-8243c084afc5) + + + +## Enabling 3-second compile times using our `web-compiler` + + * You must have `docker` installed for fastest compile times. It's free. + * `cd ` + * `pip install fastled` + * `fastled examples/Blink/Blink.ino` + * Cpp changes to the fastled source can be compiled by the live fastled compiler. + +## Testing your changes + +Most of this is in the basic CONTRIBUTING.md guide. But as a reminder + * Unit Testing: `./test` + * Linting: `./lint` + * Compiling on platforms `./compile uno,teensy41,esp32s3 --examples Blink,Apa102HD` + +## Enabling AI coding + +`aider.chat` is available for advanced and high velocity coding with FastLED. To use it, have your open-ai or Anthropic api key ready. It's recommended to use Anthropic as it's performance is much better than Open-AI for coding. + +At the root of the project type: + +`./ai` and follow the prompts. Once the key is installed you will get a prompt that looks like this: + +```bash +architect> +``` + +There are two modes to use this AI, a watch mode which watches your files for changes and launches automatically, and a slow way which you add target files then instruct it to make changes: + + * watch mode (best for implimenting a function or two) + * Edit any file in the repo. Add a comment with `AI!` at the end. The ai will see this and start implementing what you just typed. + * Example: Edit `src/fl/vector.h` and put in a comment `// Add more comments AI!`, then say yes to the changes in the prompt. + * Slow mode (much better for bigger changes across the file or several) + * While you are in the `architect> ` prompt you will add a file to the chat + * `/add src/fl/vector.h` + * Now tell the AI what you want it to do, and it will do it. + * Making the AI fix it's own problems it introduced. + * At the AI command prompt, have it run the following + * Linux/Mac: `/run ./test` + * On Windows: `/run uv run test.py` + * After the test concludes, the AI will ask you if you want to add the output back into the chat, agree to it then let it try to correct it's mistakes. + +Every single time you do a change, make sure and thoroughly check it. I recommend VSCodes built in git diff tool. + +Although the AI is pretty amazing, it will inject entropy into your code and this is the source of a lot of problems. So watch all changes it makes very thoroughly. Under almost all circumstances you will have to revert unnecessary changes (like comments) line by line. diff --git a/v1.5/lib/FastLED/APA102.md b/v1.5/lib/FastLED/APA102.md new file mode 100644 index 0000000..3d530d1 --- /dev/null +++ b/v1.5/lib/FastLED/APA102.md @@ -0,0 +1,73 @@ +# Special Notes on APA102 and the 'High Definition' Mode in FastLED + +The APA102 LED driver includes a 5-bit per-LED brightness component. Previously, this feature was not fully utilized, except through a workaround that defined a global brightness affecting all LEDs uniformly rather than individually. + +In FastLED the APA102 chipset will have extra resolution in comparison to the WS2812 RGB8 mode. + +There are two modes: + * APA102 "Regular Mode" + * Has enhanced color resolution when using the "global brightness" factor + * APA102HD Mode + * Applies automatic gamma correction at the driver level using "pseudo 13-bit" color mixing. + +**APA102HD Mode** + +[example: examples/APA102HD](examples/Apa102HD/) + +By introducing a 5-bit gamma bit-shift algorithm, we now effectively leverage this per-LED brightness control. Faced with the decision to either rewrite the entire `CRGB` library to expose the 5-bit brightness—including adaptations for formats like RGBW—or to retain the existing RGB8 format used by FastLED and implement the enhancement at the driver level, the latter option was chosen. This approach avoids widespread changes and maintains compatibility; if RGB8 suffices for game development, it is adequate for LED development as well. + +The term "Pseudo-13-bit" arises because the additional resolution becomes significant only when all color components are at low values. For example, colors like `CRGB(255, 255, 254)` or `CRGB(255, 1, 1)` do not benefit from increased resolution due to the dominance of the brighter components. However, in low-light conditions with colors such as `CRGB(8, 8, 8)`, where the maximum component value is low, the pseudo-13-bit algorithm significantly enhances resolution—precisely where increased resolution is most desired. + +Gamma correction is applied to preserve the RGB8 format and because future LEDs are expected to support gamma correction inherently. In game development, the 0-255 color values are based on the gamma scale rather than the linear power scale. LEDs like the WS2812 operate on a linear power scale, which results in washed-out, undersaturated colors when displaying captured video directly. Implementing software gamma correction for RGB8 severely reduces color resolution. + +To address this, an internal gamma scale mapping is applied: + +``` +RGB8 → RGB16 + 5-bit gamma → RGB8 + 5-bit gamma +``` + +During the conversion back to RGB8, the brightness from the 5-bit gamma is optimally distributed using a closed-form mathematical algorithm. Rather than the iterative bit-shifting approach used in earlier versions, the current implementation uses a sophisticated quantization and scaling method that provides superior accuracy. + +**Closed-Form Algorithm (Current Implementation)** + +The algorithm works by: + +1. **Finding the maximum component**: Determines which RGB component requires the highest brightness level +2. **Quantizing to 5-bit scale**: Computes the optimal 5-bit brightness value that accommodates the maximum component +3. **Applying pre-computed scaling**: Uses a lookup table of precise scaling factors to convert the 16-bit gamma-corrected values back to 8-bit RGB components + +The key insight is the quantization formula: +``` +scale = (max_component + (2047 - (max_component >> 5))) >> 11 +``` + +This ensures the 5-bit brightness value is always sufficient to represent the maximum component while minimizing quantization error. + +The scaling factors in the lookup table are computed as: +``` +bright_scale[i] = (i/31) * (255/65536) * 256 +``` + +These pre-computed values eliminate floating-point arithmetic while maintaining precision. The final RGB values are calculated with proper rounding: +``` +rgb_out = (rgb16 * scale_factor + 0x808000) >> 24 +``` + +**Algorithm Performance** + +This closed-form approach delivers exceptional accuracy: +- **Worst-case error**: 0.2% over the full 16-bit input range +- **Average error**: 0.05% across the range +- **Perfect match**: to floating-point reference implementation + +By eliminating iterative bit-shifting, this method also provides consistent performance regardless of input values and significantly reduces code size on embedded platforms. + +**Version History** + +In version 3.9.0, the algorithm was completely rewritten to function natively on 8-bit controllers like the `__AVR__` chipsets without significant performance loss. The current closed-form implementation replaced the previous iterative approach, providing both better accuracy and smaller code footprint. + +**Further Enhancements in Version 3.9.0** + +Additionally, version 3.9.0 separated the color temperature from the global brightness scale. Before this update, global brightness was pre-mixed with the component scales—a method suitable for the WS2812's RGB8 format but not for the APA102's RGB8 plus 5-bit brightness. The update saw the global brightness and color scales separated for non-AVR chipsets. While the WS2812 continues to use pre-mixed values for performance reasons on AVR chipsets, the APA102 now performs component mixing within the "pseudo-13-bit space." + +Although APA102HD mode offers the highest dynamic range, the standard APA102 mode also benefits from increased resolution when adjusting global brightness. In this mode, instead of pre-mixing scales and multiplying them against each `CRGB` value, the global brightness is applied to the 5-bit brightness component, and only the color scales are multiplied against the `CRGB` values. This approach is superior because each component of the color scale typically exceeds 127, providing ample high-order bits to preserve color information. diff --git a/v1.5/lib/FastLED/BACKGROUND_AGENT_TESTING_REQUIREMENTS.md b/v1.5/lib/FastLED/BACKGROUND_AGENT_TESTING_REQUIREMENTS.md new file mode 100644 index 0000000..d8610c7 --- /dev/null +++ b/v1.5/lib/FastLED/BACKGROUND_AGENT_TESTING_REQUIREMENTS.md @@ -0,0 +1,168 @@ +# 🚨🚨🚨 **CRITICAL REQUIREMENTS FOR BACKGROUND AGENTS** 🚨🚨🚨 + +# ⚠️ **STOP! READ THIS FIRST!** ⚠️ + +## **MANDATORY TESTING REQUIREMENT - NO EXCEPTIONS!** + +### 🔥 **RULE #1: ALWAYS RUN `bash test` BEFORE COMPLETION** 🔥 + +**THIS IS THE MOST IMPORTANT RULE FOR BACKGROUND AGENTS:** + +- **🚨 EVERY BACKGROUND AGENT MUST RUN `bash test` BEFORE SAYING THEY ARE DONE** +- **🚨 NO EXCEPTIONS - THIS IS MANDATORY AND NON-NEGOTIABLE** +- **🚨 IF YOU DON'T RUN `bash test`, YOUR WORK IS INCOMPLETE** +- **🚨 SAYING "DONE" WITHOUT RUNNING `bash test` IS BREAKING THE RULES** + +### **WHY THIS IS CRITICAL:** +- The `bash test` command runs the full test suite including unit tests and compilation checks +- It catches breaking changes, compilation errors, and regressions +- It ensures the codebase remains stable and functional +- **WITHOUT THIS, YOU WILL BREAK THE PROJECT** + +### **WHAT `bash test` DOES:** +- Runs all C++ unit tests +- Runs Python linting and formatting checks +- Validates code compilation across platforms +- Checks for syntax errors and warnings +- Ensures all examples still compile + +### 🚨 **MANDATORY STEPS FOR COMPLETION - FOLLOW EXACTLY:** 🚨 + +1. **🔥 RUN `bash test` COMMAND** + - Execute: `bash test` + - Wait for ALL tests to complete + - Check that ALL tests PASS + +2. **🔥 VERIFY TEST RESULTS** + - If ANY test fails, you MUST fix the issues + - Do NOT ignore warnings or errors + - Do NOT proceed until ALL tests pass + +3. **🔥 USE MCP SERVER VALIDATION (RECOMMENDED)** + - Use the `validate_completion` tool from MCP server: `uv run mcp_server.py` + - This tool runs `bash test` and validates results + - Only indicate completion if this tool returns SUCCESS + +4. **🔥 ZERO TOLERANCE FOR FAILURES** + - If ANY test fails, the background agent MUST fix the issues + - Do NOT indicate completion with failing tests + - Do NOT ignore test errors or warnings + - Fix the code and re-run `bash test` until ALL tests pass + +### **COMPLETION CHECKLIST - MUST COMPLETE ALL ITEMS:** +- [ ] ✅ All code changes have been made +- [ ] ✅ **`bash test` has been run and ALL tests PASS** +- [ ] ✅ No compilation errors or warnings remain +- [ ] ✅ MCP server `validate_completion` tool shows SUCCESS (if used) +- [ ] ✅ All test output has been reviewed for any issues +- [ ] ✅ **ONLY THEN** indicate task completion + +### 🔥 **CONSEQUENCES OF NOT FOLLOWING THIS RULE:** 🔥 + +- **YOUR CHANGES WILL BREAK THE PROJECT** +- **OTHER DEVELOPERS WILL HAVE TO FIX YOUR MISTAKES** +- **THE CONTINUOUS INTEGRATION SYSTEM WILL FAIL** +- **YOU WILL CAUSE REGRESSIONS AND BUGS** + +### 🚨 **FINAL WARNING:** 🚨 + +**IF YOU ARE A BACKGROUND AGENT AND YOU INDICATE COMPLETION WITHOUT RUNNING `bash test`, YOU HAVE FAILED TO FOLLOW THE MOST BASIC REQUIREMENT OF THIS PROJECT.** + +**NO EXCEPTIONS. NO SHORTCUTS. ALWAYS RUN `bash test` FIRST.** + +--- + +## **Examples of CORRECT Background Agent Behavior:** + +### ✅ **GOOD Example:** +``` +1. Made code changes +2. Ran `bash test` +3. All tests passed +4. Indicated completion: "Task completed successfully. All tests pass." +``` + +### ❌ **BAD Example:** +``` +1. Made code changes +2. Skipped running tests +3. Indicated completion: "Task completed successfully." +``` +**^ THIS IS WRONG AND BREAKS THE PROJECT** + +### ✅ **GOOD Example with Test Failures:** +``` +1. Made code changes +2. Ran `bash test` +3. Some tests failed +4. Fixed the failing code +5. Ran `bash test` again +6. All tests passed +7. Indicated completion: "Task completed successfully. All tests pass." +``` + +### ❌ **BAD Example with Test Failures:** +``` +1. Made code changes +2. Ran `bash test` +3. Some tests failed +4. Ignored failures and indicated completion anyway +``` +**^ THIS IS WRONG AND BREAKS THE PROJECT** + +--- + +## **HOW TO RUN `bash test`:** + +### **Command:** +```bash +bash test +``` + +### **Expected Output (Success):** +``` +Running C++ tests... +All C++ tests passed ✅ +Running Python linting... +All Python checks passed ✅ +Running compilation checks... +All compilation checks passed ✅ +``` + +### **What to do if tests fail:** +1. **READ THE ERROR MESSAGES CAREFULLY** +2. **FIX THE CODE THAT'S CAUSING THE FAILURES** +3. **RUN `bash test` AGAIN** +4. **REPEAT UNTIL ALL TESTS PASS** +5. **ONLY THEN INDICATE COMPLETION** + +--- + +## **MCP SERVER VALIDATION TOOL:** + +### **How to use:** +```bash +# Start MCP server +uv run mcp_server.py + +# Use the validate_completion tool +# This automatically runs `bash test` and validates results +``` + +### **Benefits:** +- Automatically runs `bash test` +- Provides clear pass/fail results +- Ensures you don't forget to run tests +- **RECOMMENDED FOR ALL BACKGROUND AGENTS** + +--- + +## 🚨 **REMEMBER: THIS IS NOT OPTIONAL** 🚨 + +**EVERY BACKGROUND AGENT WORKING ON THIS PROJECT MUST:** +1. **RUN `bash test` BEFORE COMPLETION** +2. **ENSURE ALL TESTS PASS** +3. **FIX ANY FAILURES** +4. **ONLY THEN INDICATE COMPLETION** + +**NO SHORTCUTS. NO EXCEPTIONS. ALWAYS TEST FIRST.** diff --git a/v1.5/lib/FastLED/CLAUDE.md b/v1.5/lib/FastLED/CLAUDE.md new file mode 100644 index 0000000..9507af4 --- /dev/null +++ b/v1.5/lib/FastLED/CLAUDE.md @@ -0,0 +1,47 @@ +# FastLED AI Agent Guidelines + +## Quick Reference + +This project uses directory-specific agent guidelines. See: + +- **CI/Build Tasks**: `ci/AGENTS.md` - Python build system, compilation, MCP server tools +- **Testing**: `tests/AGENTS.md` - Unit tests, test execution, validation requirements +- **Examples**: `examples/AGENTS.md` - Arduino sketch compilation, .ino file rules + +## Key Commands + +- `uv run test.py` - Run all tests +- `uv run test.py --cpp` - Run C++ tests only +- `uv run test.py TestName` - Run specific C++ test (e.g., `uv run test.py xypath`) +- `bash lint` - Run code formatting/linting +- `uv run ci/ci-compile.py uno --examples Blink` - Compile examples for specific platform +- `uv run ci/wasm_compile.py examples/Blink --just-compile` - Compile Arduino sketches to WASM +- `uv run mcp_server.py` - Start MCP server for advanced tools + +### QEMU Commands +- `uv run ci/install-qemu.py` - Install QEMU for ESP32 emulation (standalone) +- `uv run test.py --qemu esp32s3` - Run QEMU tests (installs QEMU automatically) +- `FASTLED_QEMU_SKIP_INSTALL=true uv run test.py --qemu esp32s3` - Skip QEMU installation step + +## Core Rules + +### Command Execution (ALL AGENTS) +- **Python**: Always use `uv run python script.py` (never just `python`) +- **Stay in project root** - never `cd` to subdirectories +- **Git-bash compatibility**: Prefix commands with space: `bash test` + +### C++ Code Standards +- **Use `fl::` namespace** instead of `std::` +- **If you want to use a stdlib header like , look check for equivalent in `fl/type_traits.h` +- **Use proper warning macros** from `fl/compiler_control.h` +- **Follow existing code patterns** and naming conventions + +### JavaScript Code Standards +- **After modifying any JavaScript files**: Always run `bash lint --js` to ensure proper formatting + +### Memory Refresh Rule +**🚨 ALL AGENTS: Read the relevant AGENTS.md file before concluding work to refresh memory about current project rules and requirements.** + +--- + +*This file intentionally kept minimal. Detailed guidelines are in directory-specific AGENTS.md files.* \ No newline at end of file diff --git a/v1.5/lib/FastLED/CMakeLists.txt b/v1.5/lib/FastLED/CMakeLists.txt new file mode 100644 index 0000000..8362792 --- /dev/null +++ b/v1.5/lib/FastLED/CMakeLists.txt @@ -0,0 +1,26 @@ +# FastLED +# https://github.com/FastLED/FastLED +# MIT License + +cmake_minimum_required(VERSION 3.5) + +# Collect all source files +file(GLOB FastLED_SRCS "src/*.cpp") +file(GLOB FastLED_FL_SRCS "src/fl/*.cpp") +file(GLOB FastLED_SENSORS_SRCS "src/sensors/*.cpp") +file(GLOB FastLED_PLATFORM_ARDUINO_SRCS "src/platforms/arduino/*.cpp") +file(GLOB FastLED_FX_SRCS "src/fx/*.cpp" "src/fx/**/*.cpp") + +file(GLOB ESP32_SRCS "src/platforms/esp/32/*.cpp" "src/platforms/esp/32/rmt_5/*.cpp") +file(GLOB ESP32_THIRD_PARTY_SRCS "src/third_party/**/src/*.c" "src/third_party/**/src/*.cpp") +file(GLOB ESP32_LED_STRIP_SRCS "src/third_party/espressif/led_strip/src/*.c") + +# Combine all source files into a single list +list(APPEND FastLED_SRCS ${FastLED_FL_SRCS} ${FastLED_SENSORS_SRCS} ${FastLED_FX_SRCS} ${ESP32_SRCS} ${ESP32_THIRD_PARTY_SRCS} ${ESP32_LED_STRIP_SRCS} ${FastLED_PLATFORM_ARDUINO_SRCS}) + +# Register the component with ESP-IDF +idf_component_register(SRCS ${FastLED_SRCS} + INCLUDE_DIRS "src" "src/third_party/espressif/led_strip/src" + REQUIRES arduino-esp32 esp_driver_rmt esp_lcd driver) + +project(FastLED) diff --git a/v1.5/lib/FastLED/CONTRIBUTING.md b/v1.5/lib/FastLED/CONTRIBUTING.md new file mode 100644 index 0000000..c437495 --- /dev/null +++ b/v1.5/lib/FastLED/CONTRIBUTING.md @@ -0,0 +1,243 @@ +## Contributing + +The most important part about contributing to FastLED is knowing how to test your changes. + +The FastLED library includes a powerful cli that can compile to any device. It will run if you have either [python](https://www.python.org/downloads/) or [uv](https://github.com/astral-sh/uv) installed on the system. + +## FastLED compiler cli + +[![clone and compile](https://github.com/FastLED/FastLED/actions/workflows/build_clone_and_compile.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_clone_and_compile.yml) + +The FastLED compiler cli can be invoked at the project root. + +```bash (MacOS/Linux, windows us git-bsh or compile.bat) +git clone https://github.com/fastled/fastled +cd fastled +./compile uno --examples Blink # linux/macos/git-bash +# compile.bat # Windows. +``` + +## Linting and Unit Testing + +```bash +./lint +./test # runs unit tests +# Note that you do NOT need to install the C++ compiler toolchain +# for compiling + running unit tests via ./test. If `gcc` is not +# found in your system `PATH` then the `ziglang` clang compiler +# will be swapped in automatically. +```` + + +### Testing a bunch of platforms at once. + +``` +./compile teensy41,teensy40 --examples Blink +./compile esp32dev,esp32s3,esp32c3,esp32c6,esp32s2 --examples Blink,Apa102HD +./compiles uno,digix,attiny85 --examples Blink,Apa102HD +``` + +## Unit Tests + +Shared code is unit-tested on the host machine. They can be found at `tests/` at the root of the repo. Unit testing only requires either `python` or `uv` to be installed. The C++ compiler toolchain will be installed automatically. + +The easiest way to run the tests is just use `./test` + +Alternatively, tests can be built and run for your development machine with CMake: + +```bash +cmake -S tests -B tests/.build +ctest --test-dir tests/.build --output-on-failure +# Not that this will fail if you do not have gcc installed. When in doubt +# use ./test to compile the unit tests, as a compiler is guaranteed to be +# available via this tool. +``` + +## QEMU Emulation Testing + +FastLED supports testing ESP32-S3 examples in QEMU emulation, providing a powerful way to validate code without physical hardware. + +### Running ESP32-S3 Examples in QEMU + +```bash +# Test default examples (BlinkParallel, RMT5WorkerPool) +./test --qemu esp32s3 + +# Test specific examples +./test --qemu esp32s3 BlinkParallel +./test --qemu esp32s3 RMT5WorkerPool BlinkParallel + +# Quick validation test (setup verification only) +FASTLED_QEMU_QUICK_TEST=true ./test --qemu esp32s3 +``` + +### What QEMU Testing Does + +1. **Automatic QEMU Installation**: Downloads and sets up ESP32-S3 QEMU emulator +2. **Cross-Platform Compilation**: Builds examples for ESP32-S3 target architecture +3. **Emulated Execution**: Runs compiled firmware in QEMU virtual environment +4. **Automated Validation**: Monitors execution for success/failure indicators + +### QEMU Test Output + +The QEMU tests provide detailed feedback: +- **Build Status**: Compilation success/failure for each example +- **Execution Results**: Runtime behavior in emulated environment +- **Summary Statistics**: Pass/fail counts and timing information +- **Error Details**: Specific failure reasons when tests don't pass + +### Supported Platforms + +Currently supported QEMU platforms: +- **esp32s3**: ESP32-S3 SoC emulation + +Future platforms may include additional ESP32 variants as QEMU support expands. + +### Advanced QEMU Usage + +```bash +# Run with verbose output to see detailed build and execution logs +./test --qemu esp32s3 --verbose + +# Test in non-interactive mode (useful for CI/CD) +./test --qemu esp32s3 --no-interactive +``` + +## VSCode + +We also support VSCode and IntelliSense auto-completion when the free [platformio](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide) extension is installed. The development sketch to test library changes can be found at [dev/dev.ino](dev/dev.ino). + + * Make sure you have [platformio](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide) installed. + * Click the compile button. + +![image](https://github.com/user-attachments/assets/616cc35b-1736-4bb0-b53c-468580be66f4) +*Changes in non platform specific code can be tested quickly in our webcompiler by invoking the script `./wasm` at the project root* + +## VSCode Debugging Guide + +FastLED includes comprehensive VSCode debugging support with GDB and Clang-generated debug symbols, providing professional-grade step-through debugging capabilities for the test suite. + +### Quick Start + +1. **Prerequisites**: Ensure you have VSCode with the C/C++ extension installed +2. **Open a test file**: e.g., `tests/test_allocator.cpp` +3. **Set breakpoints**: Click the left margin next to line numbers +4. **Press F5**: Automatically debugs the corresponding test executable +5. **Debug**: Use F10 (step over), F11 (step into), F5 (continue) + +### Available Debug Configurations + +#### 🎯 **"Debug FastLED Test (Current File)"** ⭐ *Most Common* +- **When to use**: When you have any test file open (e.g., `test_allocator.cpp`) +- **What it does**: Automatically detects and debugs the corresponding test executable +- **How to use**: Open any `test_*.cpp` file and press `F5` + +#### 🔧 **Specific Test Configurations** +- **"Debug test_allocator"** - Memory allocation and deallocation debugging +- **"Debug test_math"** - Mathematical functions and color calculations +- **"Debug test_fastled"** - Core FastLED functionality and API +- **"Debug test_hsv16"** - Color space conversions and accuracy +- **"Debug test_corkscrew"** - LED layout and geometric calculations + +#### 🎛️ **Advanced Configurations** +- **"Debug with Specific Test Filter"** - Run only specific test cases + - Example: Filter "allocator_inlined" to debug only those tests +- **"Debug with Custom Args"** - Pass custom command-line arguments + - Example: `--verbose`, `--list-test-cases`, etc. + +### Debugging Features + +#### Step Commands +- **F5**: Continue execution +- **F10**: Step over (execute current line) +- **F11**: Step into (enter function calls) +- **Shift+F11**: Step out (exit current function) +- **Ctrl+Shift+F5**: Restart debugging +- **Shift+F5**: Stop debugging + +#### Variable Inspection +- **Variables panel**: See all local variables and their values +- **Watch panel**: Add expressions to monitor continuously +- **Hover inspection**: Hover over variables to see values +- **Debug console**: Type expressions to evaluate + +### Build Tasks for Debugging + +Access via `Ctrl+Shift+P` → "Tasks: Run Task": + +- **"Build FastLED Tests"** - Quick incremental build (default: `Ctrl+Shift+B`) +- **"Build FastLED Tests (Full)"** - Complete build including Python tests +- **"Build Single Test"** - Build and run specific test only +- **"Clean Build"** - Remove all build artifacts and rebuild + +### Debugging Tips for FastLED + +#### Memory Issues +```cpp +// Use test_allocator for memory debugging +// Set breakpoints on allocate/deallocate functions +// Watch pointer values in Variables panel +// Monitor memory patterns for corruption +``` + +#### Color Conversion Issues +```cpp +// Use test_hsv16 for color space debugging +// Watch RGB/HSV values during conversion +// Step through algorithms with F11 +// Compare expected vs actual in Watch panel +``` + +#### Template Debugging +```cpp +// Clang generates excellent template debug info +// Step into template functions with F11 +// Watch template parameters in Variables panel +// Use Call Stack to understand instantiation chain +``` + +### Technical Setup + +#### Clang + GDB Benefits +This setup provides the **best of both worlds**: +- **Clang's superior symbol generation**: Better template debugging, modern C++ support +- **GDB's mature debugging features**: Robust breakpoint handling, memory inspection +- **Cross-platform compatibility**: Works on Linux, macOS, Windows +- **FastLED optimization**: Unified compilation testing with `FASTLED_ALL_SRC=1` + +#### Debug Build Configuration +The FastLED test system automatically uses optimal debug settings: +- **Compiler**: Clang (when available) or GCC fallback +- **Debug info**: `-g3` (full debug information including macros) +- **Optimization**: `-O0` (no optimization for accurate debugging) +- **Frame pointers**: `-fno-omit-frame-pointer` (for accurate stack traces) + +### Troubleshooting + +#### "Program not found" Error +1. **Build first**: Run "Build FastLED Tests" task +2. **Check executable exists**: `ls tests/.build/bin/test_*` +3. **Verify path**: Ensure executable path in launch.json is correct + +#### Breakpoints Not Hit +1. **Check file paths**: Ensure source file matches executable +2. **Verify compilation**: Code might be optimized out +3. **Try function breakpoints**: Sometimes more reliable than line breakpoints + +#### Variables Show "Optimized Out" +1. **Use debug build**: Already configured with `-O0` (no optimization) +2. **Check variable scope**: Variable might be out of scope +3. **Try different breakpoint**: Move breakpoint to where variable is active + +For complete debugging documentation, see [DEBUGGING.md](DEBUGGING.md). + + +## Once you are done + * run `./test` + * run `./lint` + * Then submit your code via a git pull request. + + +## Going deeper + +[ADVANCED_DEVELOPMENT.md](https://github.com/FastLED/FastLED/blob/master/ADVANCED_DEVELOPMENT.md) diff --git a/v1.5/lib/FastLED/CORKSCREW.md b/v1.5/lib/FastLED/CORKSCREW.md new file mode 100644 index 0000000..89868a2 --- /dev/null +++ b/v1.5/lib/FastLED/CORKSCREW.md @@ -0,0 +1,203 @@ +# Corkscrew Pipeline: computeTile and multiSample Integration + +## Overview + +The FastLED corkscrew allows the user to write to a regular rectangular buffer and have it displayd on a dense corkscrew of LEDs. + +Dense 144LED @ 3.28 are cheap and readily avialable. They are cheap, have high density. + +## Pipeline Components + +### 1. User Paints to XY Grid + +Users create patterns on a 2D rectangular grid (`fl::Grid`) using standard XY coordinates. + +```cpp +// Grid dimensions calculated from corkscrew parameters +uint16_t width = input.calculateWidth(); // LEDs per turn +uint16_t height = input.calculateHeight(); // Total vertical segments +fl::Grid sourceGrid(width, height); +``` + +### 2. LED Projection: Corkscrew → XY Grid + +Each LED in the corkscrew has a corresponding floating-point position on the XY grid: + +```cpp +// From corkscrew.cpp - calculateLedPositionExtended +vec2f calculateLedPosition(uint16_t ledIndex, uint16_t numLeds, uint16_t width) { + const float ledProgress = static_cast(ledIndex) / static_cast(numLeds - 1); + const uint16_t row = ledIndex / width; // Which turn (vertical position) + const uint16_t remainder = ledIndex % width; // Position within turn + const float alpha = static_cast(remainder) / static_cast(width); + + const float width_pos = ledProgress * numLeds; + const float height_pos = static_cast(row) + alpha; + + return vec2f(width_pos, height_pos); +} +``` + +### 3. computeTile: Splat Pixel Rendering + +The `splat` function implements the "computeTile" concept by converting floating-point positions to `Tile2x2_u8` structures representing neighbor intensities: + +```cpp +// From splat.cpp +Tile2x2_u8 splat(vec2f xy) { + // 1) Get integer cell indices + int16_t cx = static_cast(floorf(xy.x)); + int16_t cy = static_cast(floorf(xy.y)); + + // 2) Calculate fractional offsets [0..1) + float fx = xy.x - cx; + float fy = xy.y - cy; + + // 3) Compute bilinear weights for 4 neighbors + float w_ll = (1 - fx) * (1 - fy); // lower-left + float w_lr = fx * (1 - fy); // lower-right + float w_ul = (1 - fx) * fy; // upper-left + float w_ur = fx * fy; // upper-right + + // 4) Build Tile2x2_u8 with weights as intensities [0..255] + Tile2x2_u8 out(vec2(cx, cy)); + out.lower_left() = to_uint8(w_ll); + out.lower_right() = to_uint8(w_lr); + out.upper_left() = to_uint8(w_ul); + out.upper_right() = to_uint8(w_ur); + + return out; +} +``` + +### 4. Tile2x2_u8: Neighbor Intensity Representation + +The `Tile2x2_u8` structure represents the sampling strength from the four nearest neighbors: + +```cpp +class Tile2x2_u8 { + uint8_t mTile[2][2]; // 4 neighbor intensities [0..255] + vec2 mOrigin; // Base grid coordinate (cx, cy) + + // Access methods for the 4 neighbors: + uint8_t& lower_left(); // (0,0) - weight for pixel at (cx, cy) + uint8_t& lower_right(); // (1,0) - weight for pixel at (cx+1, cy) + uint8_t& upper_left(); // (0,1) - weight for pixel at (cx, cy+1) + uint8_t& upper_right(); // (1,1) - weight for pixel at (cx+1, cy+1) +}; +``` + +### 5. Cylindrical Wrapping with Tile2x2_u8_wrap + +For corkscrew mapping, the tile needs cylindrical wrapping: + +```cpp +// From corkscrew.cpp - at_wrap() +Tile2x2_u8_wrap Corkscrew::at_wrap(float i) const { + Tile2x2_u8 tile = at_splat_extrapolate(i); // Get base tile + Tile2x2_u8_wrap::Entry data[2][2]; + vec2i16 origin = tile.origin(); + + for (uint8_t x = 0; x < 2; x++) { + for (uint8_t y = 0; y < 2; y++) { + vec2i16 pos = origin + vec2i16(x, y); + // Apply cylindrical wrapping to x-coordinate + pos.x = fmodf(pos.x, static_cast(mState.width)); + data[x][y] = {pos, tile.at(x, y)}; // {position, intensity} + } + } + return Tile2x2_u8_wrap(data); +} +``` + +### 6. multiSample: Weighted Color Sampling + +The `readFromMulti` method implements the "multiSample" concept by using tile intensities to determine sampling strength: + +```cpp +// From corkscrew.cpp - readFromMulti() +void Corkscrew::readFromMulti(const fl::Grid& source_grid) const { + for (size_t led_idx = 0; led_idx < mInput.numLeds; ++led_idx) { + // Get wrapped tile for this LED position + Tile2x2_u8_wrap tile = at_wrap(static_cast(led_idx)); + + uint32_t r_accum = 0, g_accum = 0, b_accum = 0; + uint32_t total_weight = 0; + + // Sample from each of the 4 neighbors + for (uint8_t x = 0; x < 2; x++) { + for (uint8_t y = 0; y < 2; y++) { + const auto& entry = tile.at(x, y); + vec2i16 pos = entry.first; // Grid position + uint8_t weight = entry.second; // Sampling intensity [0..255] + + if (inBounds(source_grid, pos)) { + CRGB sample_color = source_grid.at(pos.x, pos.y); + + // Weighted accumulation + r_accum += static_cast(sample_color.r) * weight; + g_accum += static_cast(sample_color.g) * weight; + b_accum += static_cast(sample_color.b) * weight; + total_weight += weight; + } + } + } + + // Final color = weighted average + CRGB final_color = CRGB::Black; + if (total_weight > 0) { + final_color.r = static_cast(r_accum / total_weight); + final_color.g = static_cast(g_accum / total_weight); + final_color.b = static_cast(b_accum / total_weight); + } + + mCorkscrewLeds[led_idx] = final_color; + } +} +``` + +## Complete Pipeline Flow + +``` +1. User draws → XY Grid (CRGB values at integer coordinates) + ↓ +2. LED projection → vec2f position on grid (floating-point) + ↓ +3. computeTile → Tile2x2_u8 (4 neighbor intensities) + (splat) ↓ +4. Wrap for → Tile2x2_u8_wrap (cylindrical coordinates + intensities) + cylinder ↓ +5. multiSample → Weighted sampling from 4 neighbors + (readFromMulti) ↓ +6. Final LED color → CRGB value for corkscrew LED +``` + +## Key Insights + +### Sub-Pixel Accuracy +The system achieves sub-pixel accuracy by: +- Using floating-point LED positions on the grid +- Converting to bilinear weights for 4 nearest neighbors +- Performing weighted color sampling instead of nearest-neighbor + +### Cylindrical Mapping +- X-coordinates wrap around the cylinder circumference +- Y-coordinates represent vertical position along the helix +- Width = LEDs per turn, Height = total vertical segments + +### Anti-Aliasing +The weighted sampling naturally provides anti-aliasing: +- Sharp grid patterns become smoothly interpolated on the corkscrew +- Reduces visual artifacts from the discrete→continuous→discrete mapping + +## Performance Characteristics + +- **Memory**: O(W×H) for grid (O(N) for corkscrew LEDs where O(N) <= O(WxH) == O(WxW)) +- **Computation**: O(N) with 4 samples per LED (constant factor) +- **Quality**: Sub-pixel accurate with built-in anti-aliasing + + +## Future Work + +Often led strips are soldered together. These leaves a gap between the other +leds on the strip. This gab should be accounted for to maximize spatial accuracy with rendering straight lines (e.g. text). diff --git a/v1.5/lib/FastLED/FEATURE.md b/v1.5/lib/FastLED/FEATURE.md new file mode 100644 index 0000000..d20b860 --- /dev/null +++ b/v1.5/lib/FastLED/FEATURE.md @@ -0,0 +1,368 @@ +# FastLED Audio Reactive Component Analysis & Improvement Recommendations + +## Executive Summary + +After comprehensive analysis of the FastLED codebase's audio reactive components and research into industry best practices, significant opportunities exist to enhance performance, accuracy, and musical responsiveness. The current implementation provides a solid foundation but lacks advanced algorithms for rhythm analysis, perceptual audio weighting, and embedded system optimization. + +## Current Implementation Analysis + +### Strengths + +**1. Clean Architecture** +- Well-structured `AudioReactive` class with clear separation of concerns +- WLED-compatible 16-bin frequency analysis +- Proper attack/decay smoothing with configurable parameters +- Support for various audio input sources through `AudioSample` abstraction + +**2. Core Processing Pipeline** +- FFT-based frequency analysis using optimized KissFFT implementation +- Automatic Gain Control (AGC) with sensitivity adjustment +- Volume and peak detection with RMS computation +- Basic beat detection with cooldown period + +**3. Embedded-Friendly Design** +- Fixed-size frequency bins (16 channels) +- Stack-allocated FFT buffers using appropriate FastLED containers +- No dynamic memory allocation in audio processing path +- Configurable sample rates and processing parameters + +### Critical Limitations + +**1. Primitive Beat Detection Algorithm** +```cpp +// Current implementation (lines 184-214 in audio_reactive.cpp) +void AudioReactive::detectBeat(fl::u32 currentTimeMs) { + // Simple threshold-based detection + if (currentVolume > mPreviousVolume + mVolumeThreshold && + currentVolume > 5.0f) { + mCurrentData.beatDetected = true; + } +} +``` +This approach suffers from: +- False positives on sustained loud passages +- Poor detection of complex rhythmic patterns +- No tempo tracking or beat prediction +- Inability to distinguish kick drums from other transients + +**2. Lack of Perceptual Audio Weighting** +- No psychoacoustic modeling for human auditory perception +- Linear frequency binning doesn't match musical octaves +- Missing A-weighting or loudness compensation +- No consideration of critical bands or masking effects + +**3. Limited Spectral Analysis** +- Fixed 16-bin resolution may miss important frequency details +- No spectral flux calculation for onset detection +- Missing harmonic analysis for musical note detection +- No adaptive frequency mapping based on musical content + +**4. Insufficient Real-Time Optimization** +- Missing SIMD optimization for FFT calculations +- No circular buffer implementation for streaming +- Potential cache misses in frequency bin processing +- No memory layout optimization for embedded constraints + +## Industry Best Practices Research + +### Advanced Beat Detection Algorithms + +**1. Spectral Flux-Based Onset Detection** +- Analyzes changes in frequency domain magnitude over time +- Significantly more accurate than simple amplitude thresholding +- Reduces false positives by 60-80% compared to current method + +**2. Multi-Band Onset Detection** +- Separate onset detection for bass, mid, and treble frequencies +- Enables detection of polyrhythmic patterns +- Better handling of complex musical arrangements + +**3. Tempo Tracking & Beat Prediction** +- Autocorrelation-based tempo estimation +- Predictive beat scheduling for smoother visual sync +- Adaptive tempo tracking for live performances + +### Perceptual Audio Processing + +**1. Psychoacoustic Weighting** +- Equal-loudness contours (ISO 226) for frequency weighting +- Critical band analysis matching human auditory filters +- Masking models to emphasize perceptually significant content + +**2. Musical Frequency Mapping** +- Logarithmic frequency scales matching musical octaves +- Mel-scale or bark-scale frequency distribution +- Adaptive binning based on musical key detection + +### Embedded System Optimization + +**1. Memory Management** +- Circular buffers for audio streaming (single-threaded) +- Stack-allocated processing with compile-time sizing +- Preallocated object pools for temporary calculations +- Memory alignment for efficient access patterns + +**2. Platform-Specific Optimization** +- ARM DSP instructions for FFT acceleration (Teensy 3.x) +- AVR assembly optimizations for 8-bit platforms +- ESP32 dual-core utilization for audio processing isolation +- Cache-friendly data structures for modern ARM cores + +## Specific Improvement Recommendations + +### Phase 1: Enhanced Beat Detection (High Impact, Low Risk) + +**1. Implement Spectral Flux Algorithm** +```cpp +class SpectralFluxDetector { +#ifdef SKETCH_HAS_LOTS_OF_MEMORY + fl::array mPreviousMagnitudes; + fl::array mFluxHistory; // For advanced smoothing +#else + fl::array mPreviousMagnitudes; + // No history on memory-constrained platforms +#endif + float mFluxThreshold; + + bool detectOnset(const FFTBins& current, const FFTBins& previous); + float calculateSpectralFlux(const FFTBins& current, const FFTBins& previous); +}; +``` + +**2. Multi-Band Beat Detection** +```cpp +struct BeatDetectors { +#ifdef SKETCH_HAS_LOTS_OF_MEMORY + SpectralFluxDetector bass; // 20-200 Hz + SpectralFluxDetector mid; // 200-2000 Hz + SpectralFluxDetector treble; // 2000-20000 Hz +#else + SpectralFluxDetector combined; // Single detector for memory-constrained +#endif +}; +``` + +**3. Adaptive Threshold Algorithm** +- Dynamic threshold based on recent audio history +- Separate thresholds for different frequency bands +- Tempo-aware cooldown periods + +### Phase 2: Perceptual Audio Enhancement (Medium Impact, Medium Risk) + +**1. A-Weighting Implementation** +```cpp +class PerceptualWeighting { + static constexpr fl::array A_WEIGHTING_COEFFS = { + // Frequency-dependent weighting factors + }; + +#ifdef SKETCH_HAS_LOTS_OF_MEMORY + fl::array mLoudnessHistory; // For dynamic compensation +#endif + + void applyAWeighting(AudioData& data) const; + void applyLoudnessCompensation(AudioData& data, float referenceLevel) const; +}; +``` + +**2. Musical Frequency Mapping** +- Replace linear frequency bins with logarithmic musical scale +- Implement note detection and harmonic analysis +- Add chord progression recognition for advanced effects + +**3. Dynamic Range Enhancement** +- Intelligent compression based on musical content +- Frequency-dependent AGC with musical awareness +- Adaptive noise gate with spectral subtraction + +### Phase 3: Embedded Performance Optimization (High Impact, Medium Risk) + +**1. SIMD Acceleration** +```cpp +#ifdef ARM_NEON +void processFFTBins_NEON(const FFTBins& input, AudioData& output); +#endif + +#ifdef __AVR__ +void processFFTBins_AVR_ASM(const FFTBins& input, AudioData& output); +#endif +``` + +**2. Circular Buffer for Audio Streaming** +```cpp +template +class CircularBuffer { + fl::size mWriteIndex{0}; + fl::size mReadIndex{0}; + fl::array mBuffer; + +#ifdef SKETCH_HAS_LOTS_OF_MEMORY + fl::array mBackupBuffer; // For advanced buffering strategies +#endif + + public: + void push(const T& item); + bool pop(T& item); + bool full() const; + bool empty() const; +}; +``` + +**3. Cache-Friendly Data Layout** +```cpp +// Structure of Arrays for better cache locality +struct AudioDataSoA { + alignas(32) fl::array frequencyBins; + alignas(32) fl::array previousBins; + +#ifdef SKETCH_HAS_LOTS_OF_MEMORY + alignas(32) fl::array spectralFlux; + alignas(32) fl::array tempoHistory; +#endif +}; +``` + +### Phase 4: Advanced Features (Variable Impact, High Risk) + +**1. Machine Learning Integration** (Only available with `SKETCH_HAS_LOTS_OF_MEMORY`) +- Lightweight neural network for beat classification +- Real-time genre detection for adaptive processing +- Learned tempo tracking with user feedback + +**2. Multi-Channel Analysis** +- Stereo phase analysis for spatial effects +- Mid/Side processing for enhanced separation +- Cross-correlation for echo and reverb detection + +**3. Musical Structure Analysis** +- Verse/chorus detection for macro-level effects +- Build-up and drop detection for electronic music +- Key and chord progression analysis + +## Implementation Priority Matrix + +| Feature | Impact | Embedded Feasibility | Implementation Risk | Priority | +|---------|--------|---------------------|-------------------|----------| +| Spectral Flux Beat Detection | High | High | Low | 1 | +| Multi-Band Onset Detection | High | High | Low | 2 | +| A-Weighting & Loudness | Medium | High | Low | 3 | +| SIMD Optimization | High | Medium | Medium | 4 | +| Musical Frequency Mapping | Medium | Medium | Medium | 5 | +| Lock-Free Buffers | High | Medium | High | 6 | +| Tempo Tracking | Medium | Medium | High | 7 | +| Machine Learning Features | Low | Low | High | 8 | + +## Technical Specifications + +### Memory Requirements +- **Current**: ~2KB for AudioReactive instance +- **Phase 1**: +1KB for enhanced beat detection +- **Phase 2**: +2KB for perceptual processing +- **Phase 3**: +4KB for optimization buffers + +### Performance Targets +- **Current**: ~200 MIPS on ARM Cortex-M4 +- **Optimized**: <150 MIPS with platform-specific optimizations +- **Latency**: <5ms end-to-end processing (single-threaded) +- **Beat Detection Accuracy**: >90% (vs ~60% current) + +### Platform Compatibility +- **Platforms with `SKETCH_HAS_LOTS_OF_MEMORY`**: Full feature support including ML and advanced buffering + - ESP32, Teensy 4.x, STM32F4+ with sufficient RAM +- **Memory-Constrained Platforms**: Basic feature set optimized for minimal memory usage + - Arduino UNO, ATtiny, basic STM32, older Teensy models +- **Feature Selection**: Automatic based on `SKETCH_HAS_LOTS_OF_MEMORY` define rather than manual platform detection + +## Conclusion + +The FastLED audio reactive component has solid foundations but significant room for improvement. The recommended phased approach balances feature enhancement with embedded system constraints, prioritizing high-impact, low-risk improvements first. Implementation of spectral flux-based beat detection alone would dramatically improve musical responsiveness while maintaining compatibility with existing hardware platforms. + +The proposed enhancements align with industry best practices for real-time audio processing while respecting the unique constraints of embedded LED controller applications. Each phase provides measurable improvements in audio analysis accuracy and visual synchronization quality. + +## C++ Design Audit Findings + +### Critical Design Issues Identified + +**1. Namespace and Container Usage Corrections** +- **Issue**: Original recommendations used `std::` containers instead of FastLED's `fl::` namespace +- **Fix**: All code examples now use `fl::array` instead of `std::array` +- **Impact**: Ensures compatibility with FastLED's embedded-optimized container implementations + +**2. Single-Threaded Architecture Recognition** +- **Issue**: Original document included lock-free and atomic programming recommendations inappropriate for single-threaded embedded systems +- **Fix**: Removed `std::atomic` and lock-free suggestions, replaced with single-threaded circular buffers +- **Impact**: Reduces complexity and memory overhead while matching actual deployment constraints + +**3. Platform-Specific Optimization Alignment** +- **Issue**: Recommended AVX2 optimizations not available on embedded platforms +- **Fix**: Focused on ARM DSP instructions (Teensy 3.x) and AVR assembly optimizations +- **Impact**: Provides realistic performance improvements for actual target hardware + +**4. Memory Management Best Practices** +- **Issue**: Initial recommendations didn't fully leverage FastLED's memory management patterns +- **Fix**: Emphasized `fl::array` for fixed-size allocations and proper alignment macros +- **Impact**: Better cache performance and reduced memory fragmentation + +### Recommended Design Patterns + +**1. Memory-Aware Container Strategy** +```cpp +// Preferred: Fixed-size arrays for predictable memory usage +fl::array mFrequencyBins; + +#ifdef SKETCH_HAS_LOTS_OF_MEMORY +// Advanced features with additional memory +fl::array mExtendedHistory; +#endif + +// Avoid: Dynamic vectors in real-time audio path +// fl::vector mFrequencyBins; // Don't use for real-time +``` + +**2. Memory-Based Feature Selection** +```cpp +#ifdef SKETCH_HAS_LOTS_OF_MEMORY + // Full feature set for high-memory platforms + #define AUDIO_ENABLE_SPECTRAL_FLUX 1 + #define AUDIO_ENABLE_MULTI_BAND 1 + #define AUDIO_ENABLE_TEMPO_TRACKING 1 +#else + // Basic features only for memory-constrained platforms + #define AUDIO_ENABLE_SPECTRAL_FLUX 0 + #define AUDIO_ENABLE_MULTI_BAND 0 + #define AUDIO_ENABLE_TEMPO_TRACKING 0 +#endif +``` + +**3. Memory-Aligned Data Structures** +```cpp +struct AudioProcessingData { + alignas(32) fl::array frequencyBins; + alignas(16) fl::array previousBins; + +#ifdef SKETCH_HAS_LOTS_OF_MEMORY + alignas(32) fl::array spectralFluxBuffer; + alignas(16) fl::array tempoTracker; +#endif + // Alignment ensures efficient memory access patterns +}; +``` + +### Implementation Priority Adjustments + +| Original Priority | Adjusted Priority | Reason | +|------------------|------------------|---------| +| Lock-Free Buffers (6) | Removed | Single-threaded architecture | +| SIMD Optimization (4) | Platform-Specific Opt (4) | Match actual hardware capabilities | +| Machine Learning (8) | Simplified ML (8) | Embedded memory constraints | + +### Final Recommendations + +**1. Use `SKETCH_HAS_LOTS_OF_MEMORY` for all feature gating instead of platform-specific defines** +**2. Start with Phase 1 implementations using FastLED containers** +**3. Leverage existing FastLED SIMD patterns from `lib8tion` for guidance** +**4. Test on memory-constrained platforms (Arduino UNO) first to ensure baseline functionality** +**5. Use ESP32 dual-core architecture for audio processing isolation when available** +**6. Always provide fallback implementations for memory-constrained platforms** + +The corrected design approach ensures all recommendations are implementable within FastLED's existing architecture while providing meaningful performance improvements for real-world embedded applications. diff --git a/v1.5/lib/FastLED/FEATURE_PACKAGE_JSON.md b/v1.5/lib/FastLED/FEATURE_PACKAGE_JSON.md new file mode 100644 index 0000000..ceb6abf --- /dev/null +++ b/v1.5/lib/FastLED/FEATURE_PACKAGE_JSON.md @@ -0,0 +1,494 @@ +# FastLED Native Platform Configuration + +## Overview + +This document describes the comprehensive `platform.json` configuration for FastLED's native build platform. The design leverages the Python `ziglang` package to provide a unified, cross-platform compilation environment that integrates seamlessly with the existing `build_unit.toml` structure. + +## Key Design Principles + +### 1. **Unified Toolchain Integration** +- **Ziglang Foundation**: Uses `python -m ziglang` commands matching the current `build_unit.toml` configuration +- **Cross-Platform Consistency**: Single toolchain across Linux, macOS, Windows with automatic target detection +- **Direct TOML Mapping**: Toolchain commands directly map between `platform.json` and `build_unit.toml` + +### 2. **Comprehensive Architecture Support** +- **x86_64 Targets**: Linux, macOS, Windows with appropriate system-specific flags +- **ARM64 Targets**: Linux (generic), macOS (Apple Silicon), Windows with optimized CPU settings +- **RISC-V 64**: Linux support for emerging architecture +- **Auto-Detection**: Intelligent target selection for simplified development workflow + +### 3. **Arduino Package Format Compliance** +- **PlatformIO Integration**: Full compatibility with PlatformIO package management +- **Framework Support**: Both FastLED-stub and Arduino-stub frameworks +- **Package Dependencies**: Proper toolchain, framework, and tool dependency management +- **Versioning Strategy**: Semantic versioning with appropriate version constraints + +### 4. **Build System Flexibility** +- **Flag Inheritance**: Base, debug, release, and test flag configurations +- **Platform-Specific Linking**: Automatic application of OS-specific link flags +- **Compiler Caching**: Optional ccache/sccache integration for performance +- **Build System Support**: CMake, Ninja, and direct compilation compatibility + +## Complete Platform.json Structure + +The full `platform.json` includes: + +1. **Board Definitions** (8 comprehensive targets): + - `native-x64-linux`, `native-x64-macos`, `native-x64-windows` + - `native-arm64-linux`, `native-arm64-macos`, `native-arm64-windows` + - `native-riscv64-linux`, `native-auto` (intelligent detection) + +2. **Toolchain Configuration**: + - Complete ziglang tool mapping (CC, CXX, AR, LD, etc.) + - Build flag hierarchies (base, debug, release, test) + - Platform-specific link flags for each OS + +3. **Package Management**: + - Toolchain dependencies with proper versioning + - Optional tool packages (cmake, ninja, caching) + - Framework packages for Arduino compatibility + +4. **Development Features**: + - Integrated debugger support (GDB for Linux/Windows, LLDB for macOS) + - Environment variable customization support + - Target compatibility specifications + +## Platform.json Configuration + +The complete platform.json follows Arduino package format with these key sections: + +### Core Metadata +```json +{ + "name": "FastLED Native Platform", + "title": "Native Host Platform with Zig Toolchain", + "description": "Cross-platform native compilation using Zig's bundled Clang", + "version": "1.0.0", + "packageType": "platform", + "spec": { + "owner": "fastled", + "id": "native-zig", + "name": "native-zig" + } +} +``` + +### Framework Definitions +```json +{ + "frameworks": { + "fastled-stub": { + "package": "framework-fastled-stub", + "script": "builder/frameworks/fastled-stub.py" + }, + "arduino-stub": { + "package": "framework-arduino-stub", + "script": "builder/frameworks/arduino-stub.py" + } + } +} +``` + +### Board Configurations +Each board includes complete configuration: +- **Hardware specs**: MCU, RAM, Flash, CPU frequency +- **Target triple**: Zig compilation target +- **Build flags**: Platform-specific preprocessor defines +- **Debug tools**: Appropriate debugger (GDB/LLDB) configuration +- **Framework support**: Compatible frameworks list + +Example board definition: +```json +{ + "native-x64-linux": { + "name": "Native x86_64 Linux", + "mcu": "x86_64", + "fcpu": "3000000000L", + "ram": 17179869184, + "flash": 1099511627776, + "vendor": "Generic", + "product": "Native x86_64", + "frameworks": ["fastled-stub", "arduino-stub"], + "build": { + "target": "x86_64-linux-gnu", + "toolchain": "ziglang", + "cpu": "x86_64", + "mcu": "native", + "f_cpu": "3000000000L", + "extra_flags": [ + "-DNATIVE_PLATFORM=1", + "-DSTUB_PLATFORM=1", + "-DFASTLED_NATIVE_X64=1", + "-DFASTLED_NATIVE_LINUX=1" + ] + }, + "upload": { "protocol": "native" }, + "debug": { + "tools": { + "gdb": { + "server": { + "executable": "gdb", + "arguments": ["$PROG_PATH"] + } + } + } + } + } +} +``` + +### Toolchain Mapping +Direct integration with ziglang commands: +```json +{ + "toolchain-ziglang": { + "CC": "python -m ziglang cc", + "CXX": "python -m ziglang c++", + "AR": "python -m ziglang ar", + "LD": "python -m ziglang c++", + "OBJCOPY": "python -m ziglang objcopy", + "NM": "python -m ziglang nm", + "STRIP": "python -m ziglang strip", + "RANLIB": "python -m ziglang ranlib" + } +} +``` + +### Build Flag Hierarchies +```json +{ + "build_flags": { + "base": [ + "-std=gnu++17", + "-fpermissive", + "-Wall", + "-Wextra", + "-Wno-deprecated-register", + "-Wno-backslash-newline-escape", + "-fno-exceptions", + "-fno-rtti", + "-DSTUB_PLATFORM=1", + "-DNATIVE_PLATFORM=1" + ], + "debug": [ + "-g3", + "-Og", + "-DDEBUG=1", + "-DFASTLED_DEBUG=1" + ], + "release": [ + "-O3", + "-DNDEBUG=1", + "-flto" + ], + "test": [ + "-DFASTLED_UNIT_TEST=1", + "-DFASTLED_FORCE_NAMESPACE=1", + "-DFASTLED_TESTING=1", + "-DFASTLED_USE_PROGMEM=0", + "-DARDUINO=10808", + "-DFASTLED_USE_STUB_ARDUINO=1", + "-DSKETCH_HAS_LOTS_OF_MEMORY=1", + "-DFASTLED_STUB_IMPL=1", + "-DFASTLED_USE_JSON_UI=1", + "-DFASTLED_NO_AUTO_NAMESPACE=1", + "-DFASTLED_NO_PINMAP=1", + "-DHAS_HARDWARE_PIN_SUPPORT=1", + "-DFASTLED_DEBUG_LEVEL=1", + "-DFASTLED_NO_ATEXIT=1", + "-DDOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS=1" + ] + } +} +``` + +### Platform-Specific Linking +```json +{ + "link_flags": { + "base": [], + "linux": [ + "-pthread", + "-lm", + "-ldl" + ], + "macos": [ + "-pthread", + "-lm" + ], + "windows": [ + "-mconsole", + "-nodefaultlibs", + "-unwindlib=libunwind", + "-nostdlib++", + "-lc++", + "-lkernel32", + "-luser32", + "-lgdi32", + "-ladvapi32" + ] + } +} +``` + +## Integration with build_unit.toml + +The `platform.json` is designed to seamlessly integrate with the existing `build_unit.toml` configuration: + +### Toolchain Mapping + +The `[tools]` section in `build_unit.toml` directly maps to the ziglang toolchain defined in `platform.json`: + +- `cpp_compiler = ["python", "-m", "ziglang", "c++"]` → `CXX: "python -m ziglang c++"` +- `linker = ["python", "-m", "ziglang", "c++"]` → `LD: "python -m ziglang c++"` +- `archiver = ["python", "-m", "ziglang", "ar"]` → `AR: "python -m ziglang ar"` + +### Build Flag Integration + +The platform flags complement the TOML configuration: + +- **Base flags**: Defined in both `platform.json` and `build_unit.toml` for consistency +- **Platform-specific flags**: Windows-specific flags from TOML are automatically applied +- **Test flags**: Unit test defines from TOML are included in the platform test configuration + +### Target Selection + +The platform supports automatic target detection or explicit target selection: + +```bash +# Auto-detect target +platformio run -e native-auto + +# Explicit target selection +platformio run -e native-x64-linux +platformio run -e native-arm64-macos +platformio run -e native-x64-windows +``` + +## Integration Benefits + +### Seamless TOML Integration +The design ensures perfect compatibility with existing `build_unit.toml`: +- Toolchain commands match exactly: `["python", "-m", "ziglang", "c++"]` +- Build flags complement rather than duplicate TOML configuration +- Platform-specific flags automatically applied based on detection + +### Enhanced Development Workflow +- **Single Command Compilation**: `platformio run -e native-auto` +- **Cross-Compilation Support**: Build for different targets from any host +- **Testing Integration**: Full unit test support with proper namespace isolation +- **Debug Builds**: Integrated debugging with platform-appropriate tools + +### Future-Proof Architecture +- **Extensible Design**: Easy addition of new architectures and targets +- **WebAssembly Ready**: Structure supports future WASM integration +- **Container Builds**: Foundation for Docker/Podman integration +- **Static Analysis**: Framework for clang-tidy and analyzer integration + +## Usage Examples + +### Basic Compilation + +```bash +# Compile for current platform (auto-detect) +platformio run -e native-auto + +# Compile for specific platform +platformio run -e native-x64-linux +``` + +### Testing + +```bash +# Run unit tests +platformio test -e native-auto + +# Run tests with specific build flags +platformio test -e native-x64-linux --test-port=native +``` + +### Cross-Compilation + +```bash +# Compile for ARM64 Linux from x86_64 +platformio run -e native-arm64-linux + +# Compile for Windows from Linux +platformio run -e native-x64-windows +``` + +### Debug Builds + +```bash +# Debug build with symbols +platformio run -e native-auto --build-type=debug + +# Release build with optimization +platformio run -e native-auto --build-type=release +``` + +## Advanced Configuration + +### Custom Board Definition + +```json +{ + "native-custom": { + "name": "Custom Native Target", + "mcu": "custom", + "build": { + "target": "x86_64-linux-musl", + "extra_flags": [ + "-DCUSTOM_DEFINE=1", + "-static" + ] + } + } +} +``` + +### Environment Variables + +The platform respects environment variables for customization: + +- `ZIG_TARGET`: Override target triple (e.g., `x86_64-linux-musl`) +- `ZIG_CPU`: Override CPU type (e.g., `x86_64_v3`) +- `FASTLED_NATIVE_FLAGS`: Additional compiler flags + +### Build System Integration + +The platform integrates with multiple build systems: + +- **PlatformIO**: Primary integration with comprehensive board support +- **CMake**: Compatible with existing CMake configurations +- **Direct Compilation**: Can be used directly with ziglang commands + +## Complete Board Definitions + +The platform.json includes 8 comprehensive board definitions: + +1. **native-x64-linux**: x86_64 Linux with GNU target +2. **native-x64-macos**: x86_64 macOS with Apple-specific configuration +3. **native-x64-windows**: x86_64 Windows with MinGW compatibility +4. **native-arm64-linux**: ARM64 Linux with Cortex-A76 optimization +5. **native-arm64-macos**: ARM64 macOS with Apple M1 optimization +6. **native-arm64-windows**: ARM64 Windows with appropriate runtime +7. **native-riscv64-linux**: RISC-V 64-bit Linux support +8. **native-auto**: Automatic target detection and selection + +## Tool Dependencies + +The platform includes comprehensive tool support: + +```json +{ + "tools": { + "python-ziglang": { + "type": "compiler", + "version": "^0.13.0", + "description": "Zig toolchain with bundled Clang/LLVM", + "install": { "pip": "ziglang" } + }, + "cmake": { + "type": "build-system", + "version": "^3.21.0", + "optional": true + }, + "ninja": { + "type": "build-system", + "version": "^1.11.0", + "optional": true + }, + "ccache": { + "type": "compiler-cache", + "version": "^4.0.0", + "optional": true + }, + "sccache": { + "type": "compiler-cache", + "version": "^0.7.0", + "optional": true + } + } +} +``` + +## Target Compatibility + +Platform compatibility specifications ensure proper deployment: + +```json +{ + "target_compatibility": { + "x86_64-linux-gnu": { + "glibc": ">=2.31", + "kernel": ">=5.4.0" + }, + "x86_64-macos-none": { + "macos": ">=11.0" + }, + "x86_64-windows-gnu": { + "windows": ">=10" + }, + "aarch64-linux-gnu": { + "glibc": ">=2.31", + "kernel": ">=5.4.0" + }, + "aarch64-macos-none": { + "macos": ">=11.0" + }, + "aarch64-windows-gnu": { + "windows": ">=10" + }, + "riscv64-linux-gnu": { + "glibc": ">=2.31", + "kernel": ">=5.10.0" + } + } +} +``` + +## Troubleshooting + +### Common Issues + +1. **Ziglang not found**: Ensure `ziglang` Python package is installed +2. **Target not supported**: Check `zig targets` for available targets +3. **Linking errors**: Verify platform-specific link flags in configuration + +### Debug Information + +```bash +# Check available targets +zig targets + +# Verify toolchain installation +python -m ziglang version + +# Test compilation +python -m ziglang c++ --version +``` + +## Implementation Status + +This design is **ready for implementation** and provides: +- ✅ **Complete board definitions** for all major native platforms +- ✅ **Full toolchain integration** with existing ziglang setup +- ✅ **Comprehensive build configurations** for all use cases +- ✅ **Arduino package format compliance** for PlatformIO compatibility +- ✅ **Debug and development tool integration** for professional development +- ✅ **Future-proof extensible architecture** for additional platforms + +The configuration can be deployed alongside the existing build system for gradual validation and testing before full migration. + +## Future Enhancements + +- **WebAssembly Support**: Integration with FastLED WASM compilation +- **Static Analysis**: Integration with clang-tidy and static analyzers +- **Performance Profiling**: Built-in profiling support for optimization +- **Container Builds**: Docker/Podman integration for reproducible builds +- **Package Registry**: Publication to Arduino package index for distribution + +## Conclusion + +This platform.json design provides a comprehensive, production-ready configuration for FastLED's native build platform. It seamlessly integrates with the existing `build_unit.toml` while providing the standardized Arduino package format required for broader ecosystem compatibility. The design is thoroughly tested conceptually and ready for implementation with proper validation and testing procedures. diff --git a/v1.5/lib/FastLED/FEATURE_RMT5_POOL.md b/v1.5/lib/FastLED/FEATURE_RMT5_POOL.md new file mode 100644 index 0000000..5e59643 --- /dev/null +++ b/v1.5/lib/FastLED/FEATURE_RMT5_POOL.md @@ -0,0 +1,1172 @@ +# RMT5 Worker Pool Implementation + +## Problem Statement + +ESP32 RMT5 has hardware limitations on the number of LED strips it can control simultaneously: +- **ESP32**: 8 RMT channels maximum +- **ESP32-S2/S3**: 4 RMT channels maximum +- **ESP32-C3/H2**: 2 RMT channels maximum + +Currently, FastLED's RMT5 implementation creates a one-to-one mapping between `RmtController5` instances and RMT hardware channels. This means you can only control K strips simultaneously, where K is the hardware limit. + +**Goal**: Implement a worker pool system to support N strips where N > K, allowing any reasonable number of LED strips to be controlled by recycling RMT workers. + +## Current RMT5 Architecture + +### Class Hierarchy +```cpp +ClocklessController + └── RmtController5 (mRMTController) + └── IRmtStrip* (mLedStrip) + └── RmtStrip (concrete implementation) + └── led_strip_handle_t (mStrip) // ESP-IDF handle + └── rmt_channel_handle_t // Hardware channel +``` + +### Current Flow +1. `RmtController5::loadPixelData()` creates `IRmtStrip` on first call +2. `IRmtStrip::Create()` calls `led_strip_new_rmt_device()` which allocates a hardware RMT channel +3. Channel remains allocated until `RmtStrip` destructor calls `led_strip_del()` +4. No sharing or pooling exists + +## RMT4 Worker Pool Reference + +RMT4 implements a sophisticated worker pool system: + +### Key Components +- **`gControllers[FASTLED_RMT_MAX_CONTROLLERS]`**: Array of all registered controllers +- **`gOnChannel[FASTLED_RMT_MAX_CHANNELS]`**: Currently active controllers per channel +- **Global counters**: `gNumStarted`, `gNumDone`, `gNext` for coordination +- **Semaphore coordination**: `gTX_sem` for synchronization + +### RMT4 Worker Pool Flow +1. All controllers register in `gControllers[]` during construction +2. `showPixels()` triggers batch processing when `gNumStarted == gNumControllers` +3. First K controllers start immediately on available channels +4. Remaining controllers queue until channels become available +5. `doneOnChannel()` callback releases channels and starts next queued controller +6. Process continues until all controllers complete + +## Proposed RMT5 Worker Pool Architecture + +### Core Design Principles + +1. **Backward Compatibility**: Existing `RmtController5` API remains unchanged +2. **Transparent Pooling**: Controllers don't know they're sharing workers +3. **Automatic Resource Management**: Workers handle setup/teardown automatically +4. **Thread Safety**: Pool operations are atomic and interrupt-safe + +### New Components + +#### 1. RmtWorkerPool (Singleton) +```cpp +class RmtWorkerPool { +public: + static RmtWorkerPool& getInstance(); + + // Worker management + RmtWorker* acquireWorker(const RmtWorkerConfig& config); + void releaseWorker(RmtWorker* worker); + + // Coordination + void registerController(RmtController5* controller); + void unregisterController(RmtController5* controller); + void executeDrawCycle(); + +private: + fl::vector mAvailableWorkers; + fl::vector mBusyWorkers; + fl::vector mRegisteredControllers; + + // Synchronization + SemaphoreHandle_t mPoolMutex; + SemaphoreHandle_t mDrawSemaphore; + + // State tracking + int mActiveDrawCount; + int mCompletedDrawCount; +}; +``` + +#### 2. RmtWorker (Replaceable RMT Resource) +```cpp +class RmtWorker { +public: + // Configuration for worker setup + struct Config { + int pin; + uint32_t ledCount; + bool isRgbw; + uint32_t t0h, t0l, t1h, t1l, reset; + IRmtStrip::DmaMode dmaMode; + }; + + // Worker lifecycle + bool configure(const Config& config); + void loadPixelData(fl::PixelIterator& pixels); + void startTransmission(); + void waitForCompletion(); + + // State management + bool isAvailable() const; + bool isConfiguredFor(const Config& config) const; + void reset(); + + // Callbacks + void onTransmissionComplete(); + +private: + IRmtStrip* mCurrentStrip; + Config mCurrentConfig; + bool mIsAvailable; + bool mTransmissionActive; + RmtWorkerPool* mPool; // Back reference for release +}; +``` + +#### 3. Modified RmtController5 +```cpp +class RmtController5 { +public: + // Existing API unchanged + RmtController5(int DATA_PIN, int T1, int T2, int T3, DmaMode dma_mode); + void loadPixelData(PixelIterator &pixels); + void showPixels(); + +private: + // New pooled implementation + RmtWorkerConfig mWorkerConfig; + fl::vector mPixelBuffer; // Persistent buffer + bool mRegisteredWithPool; + + // Remove direct IRmtStrip ownership + // IRmtStrip *mLedStrip = nullptr; // REMOVED +}; +``` + +### Worker Pool Operation Flow + +#### Registration Phase (Constructor) +```cpp +RmtController5::RmtController5(int DATA_PIN, int T1, int T2, int T3, DmaMode dma_mode) + : mPin(DATA_PIN), mT1(T1), mT2(T2), mT3(T3), mDmaMode(dma_mode) { + + // Configure worker requirements + mWorkerConfig = {DATA_PIN, 0, false, t0h, t0l, t1h, t1l, 280, convertDmaMode(dma_mode)}; + + // Register with pool + RmtWorkerPool::getInstance().registerController(this); + mRegisteredWithPool = true; +} +``` + +#### Data Loading Phase +```cpp +void RmtController5::loadPixelData(PixelIterator &pixels) { + // Update worker config with actual pixel count + mWorkerConfig.ledCount = pixels.size(); + mWorkerConfig.isRgbw = pixels.get_rgbw().active(); + + // Store pixel data in persistent buffer + storePixelData(pixels); +} + +void RmtController5::storePixelData(PixelIterator &pixels) { + const int bytesPerPixel = mWorkerConfig.isRgbw ? 4 : 3; + const int bufferSize = mWorkerConfig.ledCount * bytesPerPixel; + + mPixelBuffer.resize(bufferSize); + + // Copy pixel data to persistent buffer + uint8_t* bufPtr = mPixelBuffer.data(); + if (mWorkerConfig.isRgbw) { + while (pixels.has(1)) { + uint8_t r, g, b, w; + pixels.loadAndScaleRGBW(&r, &g, &b, &w); + *bufPtr++ = r; *bufPtr++ = g; *bufPtr++ = b; *bufPtr++ = w; + pixels.advanceData(); + pixels.stepDithering(); + } + } else { + while (pixels.has(1)) { + uint8_t r, g, b; + pixels.loadAndScaleRGB(&r, &g, &b); + *bufPtr++ = r; *bufPtr++ = g; *bufPtr++ = b; + pixels.advanceData(); + pixels.stepDithering(); + } + } +} +``` + +#### Execution Phase (Coordinated Draw) +```cpp +void RmtController5::showPixels() { + // Trigger coordinated draw cycle + RmtWorkerPool::getInstance().executeDrawCycle(); +} + +void RmtWorkerPool::executeDrawCycle() { + // Similar to RMT4 coordination logic + mActiveDrawCount = 0; + mCompletedDrawCount = 0; + + // Take draw semaphore + xSemaphoreTake(mDrawSemaphore, portMAX_DELAY); + + // Start as many controllers as we have workers + int startedCount = 0; + for (auto* controller : mRegisteredControllers) { + if (startedCount < mAvailableWorkers.size()) { + startController(controller); + startedCount++; + } + } + + // Wait for all controllers to complete + while (mCompletedDrawCount < mRegisteredControllers.size()) { + xSemaphoreTake(mDrawSemaphore, portMAX_DELAY); + + // Start next queued controller if workers available + startNextQueuedController(); + + xSemaphoreGive(mDrawSemaphore); + } +} + +void RmtWorkerPool::startController(RmtController5* controller) { + // Acquire worker from pool + RmtWorker* worker = acquireWorker(controller->getWorkerConfig()); + if (!worker) { + // This should not happen if pool is sized correctly + return; + } + + // Configure worker for this controller + worker->configure(controller->getWorkerConfig()); + + // Load pixel data from controller's persistent buffer + loadPixelDataToWorker(worker, controller); + + // Start transmission + worker->startTransmission(); + + mActiveDrawCount++; +} + +void RmtWorkerPool::onWorkerComplete(RmtWorker* worker) { + // Called from worker's completion callback + mCompletedDrawCount++; + + // Release worker back to pool + releaseWorker(worker); + + // Signal main thread + xSemaphoreGive(mDrawSemaphore); +} +``` + +### Worker Reconfiguration Strategy + +#### Efficient Worker Reuse +```cpp +bool RmtWorker::configure(const Config& newConfig) { + // Check if reconfiguration is needed + if (isConfiguredFor(newConfig)) { + return true; // Already configured correctly + } + + // Tear down current configuration + if (mCurrentStrip) { + // Wait for any pending transmission + if (mTransmissionActive) { + mCurrentStrip->waitDone(); + } + + // Clean shutdown + delete mCurrentStrip; + mCurrentStrip = nullptr; + } + + // Create new strip with new configuration + mCurrentStrip = IRmtStrip::Create( + newConfig.pin, newConfig.ledCount, newConfig.isRgbw, + newConfig.t0h, newConfig.t0l, newConfig.t1h, newConfig.t1l, + newConfig.reset, newConfig.dmaMode + ); + + if (!mCurrentStrip) { + return false; + } + + mCurrentConfig = newConfig; + return true; +} +``` + +#### Pin State Management +```cpp +void RmtWorker::reset() { + if (mCurrentStrip) { + // Ensure transmission is complete + if (mTransmissionActive) { + mCurrentStrip->waitDone(); + } + + // Set pin to safe state before teardown + gpio_set_level((gpio_num_t)mCurrentConfig.pin, 0); + gpio_set_direction((gpio_num_t)mCurrentConfig.pin, GPIO_MODE_OUTPUT); + + // Clean up strip + delete mCurrentStrip; + mCurrentStrip = nullptr; + } + + mTransmissionActive = false; + mIsAvailable = true; +} +``` + +### Memory Management Strategy + +#### Persistent Pixel Buffers +Each `RmtController5` maintains its own pixel buffer to avoid data races: + +```cpp +class RmtController5 { +private: + fl::vector mPixelBuffer; // Persistent storage + RmtWorkerConfig mWorkerConfig; // Configuration cache + + void storePixelData(PixelIterator& pixels); + void restorePixelData(RmtWorker* worker); +}; +``` + +#### Worker Pool Sizing +```cpp +void RmtWorkerPool::initialize() { + // Determine hardware channel count + int maxChannels = getHardwareChannelCount(); + + // Create one worker per hardware channel + mAvailableWorkers.reserve(maxChannels); + for (int i = 0; i < maxChannels; i++) { + mAvailableWorkers.push_back(new RmtWorker()); + } +} + +int RmtWorkerPool::getHardwareChannelCount() { +#if CONFIG_IDF_TARGET_ESP32 + return 8; +#elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + return 4; +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + return 2; +#else + return 2; // Conservative default +#endif +} +``` + +## Key Implementation Insights + +### Critical Async Decision Point +The worker pool must make a **per-controller decision** at `showPixels()` time: + +```cpp +void RmtController5::showPixels() { + RmtWorkerPool& pool = RmtWorkerPool::getInstance(); + + // CRITICAL: This decision determines async vs blocking behavior + if (pool.hasAvailableWorker()) { + // ASYNC PATH: Start immediately and return + pool.startControllerImmediate(this); + return; // Returns immediately - preserves async! + } else { + // BLOCKING PATH: This specific controller must wait + pool.startControllerQueued(this); // May block with polling + } +} +``` + +### ESP-IDF RMT Channel Management +Direct integration with ESP-IDF RMT5 APIs instead of using led_strip wrapper: + +```cpp +class RmtWorker { +private: + rmt_channel_handle_t mChannel; + rmt_encoder_handle_t mEncoder; + +public: + // Direct RMT channel creation for maximum control + bool createChannel(int pin) { + rmt_tx_channel_config_t config = { + .gpio_num = pin, + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10000000, + .mem_block_symbols = 64, + .trans_queue_depth = 1, // Single transmission per worker + }; + + ESP_ERROR_CHECK(rmt_new_tx_channel(&config, &mChannel)); + + // Register callback for async completion + rmt_tx_event_callbacks_t callbacks = { + .on_trans_done = onTransComplete, + }; + ESP_ERROR_CHECK(rmt_tx_register_event_callbacks(mChannel, &callbacks, this)); + + return true; + } + + void transmitAsync(uint8_t* pixelData, size_t dataSize) { + // Direct transmission - bypasses led_strip wrapper + ESP_ERROR_CHECK(rmt_enable(mChannel)); + ESP_ERROR_CHECK(rmt_transmit(mChannel, mEncoder, pixelData, dataSize, &mTxConfig)); + // Returns immediately - async transmission started + } +}; +``` + +### Polling Strategy Implementation +Use `delayMicroseconds(100)` only for queued controllers: + +```cpp +void RmtWorkerPool::startControllerQueued(RmtController5* controller) { + // Add to queue + mQueuedControllers.push_back(controller); + + // Poll until this controller gets a worker + while (true) { + if (RmtWorker* worker = tryAcquireWorker()) { + // Remove from queue and start + mQueuedControllers.remove(controller); + startControllerImmediate(controller, worker); + break; + } + + // Brief delay to prevent busy-wait + delayMicroseconds(100); + + // Yield periodically for FreeRTOS + static uint32_t pollCount = 0; + if (++pollCount % 50 == 0) { + yield(); + } + } +} +``` + +## Implementation Plan + +### Phase 1: Core Infrastructure +1. **Create `RmtWorkerPool` singleton** + - Basic worker management (acquire/release) + - Controller registration system + - Thread-safe operations with mutexes + +2. **Implement `RmtWorker` class** + - Worker lifecycle management + - Configuration and reconfiguration logic + - Completion callbacks + +3. **Modify `RmtController5`** + - Add persistent pixel buffer + - Integrate with worker pool + - Maintain backward-compatible API + +### Phase 2: Coordination Logic +1. **Implement coordinated draw cycle** + - Batch processing similar to RMT4 + - Semaphore-based synchronization + - Queue management for excess controllers + +2. **Add worker completion handling** + - Async completion callbacks + - Automatic worker recycling + - Next controller startup + +### Phase 3: Optimization & Safety +1. **Optimize worker reconfiguration** + - Minimize teardown/setup when possible + - Cache compatible configurations + - Efficient pin state management + +2. **Add error handling** + - Worker allocation failures + - Transmission errors + - Recovery mechanisms + +3. **Memory optimization** + - Minimize buffer copying + - Efficient pixel data transfer + - Memory pool for workers + +### Phase 4: Testing & Integration +1. **Unit tests** + - Worker pool operations + - Controller coordination + - Error scenarios + +2. **Integration testing** + - Multiple strip configurations + - High load scenarios + - Hardware limit validation + +3. **Performance benchmarking** + - Throughput comparison with RMT4 + - Memory usage analysis + - Latency measurements + +## CRITICAL: Async Behavior Preservation + +**Key Requirement**: RMT5 currently provides async drawing where `endShowLeds()` returns immediately without waiting. This must be preserved when N ≤ K, and only use polling/waiting when N > K. + +### Current RMT5 Async Flow +```cpp +// Current behavior - MUST PRESERVE when N ≤ K +void ClocklessController::endShowLeds(void *data) { + CPixelLEDController::endShowLeds(data); + mRMTController.showPixels(); // Calls drawAsync() - returns immediately! +} +``` + +### Async Strategy for Worker Pool + +#### When N ≤ K (Preserve Full Async) +- **Direct Assignment**: Each controller gets dedicated worker immediately +- **No Waiting**: `endShowLeds()` returns immediately after starting transmission +- **Callback-Driven**: Use ESP-IDF `rmt_tx_event_callbacks_t::on_trans_done` for completion +- **Zero Overhead**: Maintain current performance characteristics + +#### When N > K (Controlled Polling) +- **Immediate Start**: First K controllers start immediately (async) +- **Queue Remaining**: Controllers K+1 through N queue for workers +- **Polling Strategy**: Use `delayMicroseconds(100)` polling for queued controllers +- **Callback Coordination**: Workers signal completion via callbacks to start next queued controller + +## ESP-IDF RMT5 Callback Integration + +### Callback Registration Pattern +```cpp +class RmtWorker { +private: + rmt_channel_handle_t mRmtChannel; + RmtWorkerPool* mPool; + + static bool IRAM_ATTR onTransmissionComplete( + rmt_channel_handle_t channel, + const rmt_tx_done_event_data_t *edata, + void *user_data) { + + RmtWorker* worker = static_cast(user_data); + worker->handleTransmissionComplete(); + return false; // No high-priority task woken + } + +public: + bool initialize() { + // Create RMT channel + rmt_tx_channel_config_t tx_config = { + .gpio_num = mPin, + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10000000, // 10MHz + .mem_block_symbols = 64, + .trans_queue_depth = 4, + }; + + if (rmt_new_tx_channel(&tx_config, &mRmtChannel) != ESP_OK) { + return false; + } + + // Register completion callback + rmt_tx_event_callbacks_t callbacks = { + .on_trans_done = onTransmissionComplete, + }; + + return rmt_tx_register_event_callbacks(mRmtChannel, &callbacks, this) == ESP_OK; + } + + void handleTransmissionComplete() { + // Signal pool that this worker is available + mPool->onWorkerComplete(this); + } +}; +``` + +## Revised Worker Pool Architecture + +### Async-Aware Worker Pool +```cpp +class RmtWorkerPool { +public: + enum class DrawMode { + ASYNC_ONLY, // N ≤ K: All controllers async + MIXED_MODE // N > K: Some async, some polled + }; + + void executeDrawCycle() { + const int numControllers = mRegisteredControllers.size(); + const int numWorkers = mAvailableWorkers.size(); + + if (numControllers <= numWorkers) { + // ASYNC_ONLY mode - preserve full async behavior + executeAsyncOnlyMode(); + } else { + // MIXED_MODE - async for first K, polling for rest + executeMixedMode(); + } + } + +private: + void executeAsyncOnlyMode() { + // Start all controllers immediately - full async behavior preserved + for (auto* controller : mRegisteredControllers) { + RmtWorker* worker = acquireWorker(controller->getWorkerConfig()); + startControllerAsync(controller, worker); + } + // Return immediately - no waiting! + } + + void executeMixedMode() { + // Start first K controllers immediately (async) + int startedCount = 0; + for (auto* controller : mRegisteredControllers) { + if (startedCount < mAvailableWorkers.size()) { + RmtWorker* worker = acquireWorker(controller->getWorkerConfig()); + startControllerAsync(controller, worker); + startedCount++; + } else { + // Queue remaining controllers + mQueuedControllers.push_back(controller); + } + } + + // Poll for completion of queued controllers + while (!mQueuedControllers.empty()) { + delayMicroseconds(100); // Non-blocking poll interval + // Callback-driven worker completion will process queue + } + } + + void onWorkerComplete(RmtWorker* worker) { + // Called from ISR context via callback + releaseWorker(worker); + + // Start next queued controller if available + if (!mQueuedControllers.empty()) { + RmtController5* nextController = mQueuedControllers.front(); + mQueuedControllers.pop_front(); + + // Reconfigure worker and start transmission + startControllerAsync(nextController, worker); + } + } +}; +``` + +### Modified RmtController5 for Async Preservation +```cpp +class RmtController5 { +public: + void showPixels() { + // This method MUST return immediately when N ≤ K + // Only block when this specific controller is queued (N > K) + + RmtWorkerPool& pool = RmtWorkerPool::getInstance(); + + if (pool.canStartImmediately(this)) { + // Async path - return immediately + pool.startControllerImmediate(this); + } else { + // This controller is queued - must wait for worker + pool.startControllerQueued(this); + } + } +}; +``` + +## Polling Strategy Details + +### Microsecond Polling Pattern +```cpp +void RmtWorkerPool::waitForQueuedControllers() { + while (!mQueuedControllers.empty()) { + // Non-blocking check for available workers + if (hasAvailableWorker()) { + processNextQueuedController(); + } else { + // Short delay to prevent busy-waiting + delayMicroseconds(100); // 100μs polling interval + } + + // Yield to other tasks periodically + static uint32_t pollCount = 0; + if (++pollCount % 50 == 0) { // Every 5ms (50 * 100μs) + yield(); + } + } +} +``` + +### Callback-Driven Queue Processing +```cpp +void RmtWorker::handleTransmissionComplete() { + // Called from ISR context - keep minimal + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + // Signal completion to pool + xSemaphoreGiveFromISR(mPool->getCompletionSemaphore(), &xHigherPriorityTaskWoken); + + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); +} + +void RmtWorkerPool::processCompletionEvents() { + // Called from main task context + while (xSemaphoreTake(mCompletionSemaphore, 0) == pdTRUE) { + // Process one completion event + if (!mQueuedControllers.empty()) { + RmtController5* nextController = mQueuedControllers.front(); + mQueuedControllers.pop_front(); + + // Find available worker and start next transmission + RmtWorker* worker = findAvailableWorker(); + if (worker) { + startControllerAsync(nextController, worker); + } + } + } +} +``` + +## Benefits + +1. **Async Preservation**: Full async behavior maintained when N ≤ K +2. **Scalability**: Support unlimited LED strips (within memory constraints) +3. **Backward Compatibility**: Existing code works unchanged +4. **Resource Efficiency**: Optimal use of limited RMT hardware +5. **Controlled Blocking**: Only blocks when specific controller is queued +6. **Callback Efficiency**: ISR-driven completion for minimal latency +7. **Polling Optimization**: 100μs intervals prevent busy-waiting + +## Considerations + +1. **Memory Usage**: Each controller needs persistent pixel buffer +2. **Latency**: Worker switching adds small overhead +3. **Complexity**: More complex than direct mapping +4. **Debugging**: Pool coordination harder to debug than direct control + +## Migration Path + +The implementation maintains full backward compatibility. Existing code using `RmtController5` will automatically benefit from the worker pool without any changes required. + +## CRITICAL: LED Buffer Transfer Analysis - CORRECTED FINDINGS + +### ESP-IDF LED Buffer Management + +**Key Finding**: The ESP-IDF led_strip driver **supports external pixel buffers** and **buffer transfer IS possible through RMT channel recreation**. + +#### Current Buffer Architecture +```cpp +typedef struct { + led_strip_t base; + rmt_channel_handle_t rmt_chan; + rmt_encoder_handle_t strip_encoder; + uint8_t *pixel_buf; // ← Buffer pointer (fixed at creation) + bool pixel_buf_allocated_internally; // ← Ownership flag + // ... other fields +} led_strip_rmt_obj; +``` + +#### Buffer Creation Options +```cpp +led_strip_config_t strip_config = { + .strip_gpio_num = pin, + .max_leds = led_count, + .external_pixel_buf = external_buffer, // ← Can provide external buffer + // ... other config +}; +``` + +### Buffer Transfer Solution - RMT Channel Recreation + +**✅ POSSIBLE**: Buffer transfer through worker reconfiguration +- **Destroy existing RMT channel** when switching controllers +- **Create new RMT channel** with different external buffer +- **Worker pool manages appropriately-sized buffers** for each controller's requirements +- **RMT channels always use external buffers** owned by the worker pool + +### Critical Insight: Buffer Size Requirements + +**The Core Issue**: Different controllers need different buffer sizes: +- **Controller A**: 100 RGB LEDs → 300 bytes buffer +- **Controller B**: 200 RGBW LEDs → 800 bytes buffer +- **RMT Channel**: Must be recreated with appropriate buffer size for each controller + +**ESP-IDF led_strip_rmt_obj Structure:** +```cpp +typedef struct { + led_strip_t base; + rmt_channel_handle_t rmt_chan; + rmt_encoder_handle_t strip_encoder; + uint32_t strip_len; + uint8_t bytes_per_pixel; + led_color_component_format_t component_fmt; + uint8_t *pixel_buf; // ← MUST be external and pool-managed + bool pixel_buf_allocated_internally; // ← Always false for worker pool +} led_strip_rmt_obj; +``` + +**Available Buffer APIs (Complete List):** +- `led_strip_set_pixel()` - Writes to existing buffer +- `led_strip_set_pixel_rgbw()` - Writes to existing buffer +- `led_strip_clear()` - Zeros existing buffer +- `led_strip_refresh_async()` - Transmits from existing buffer +- **KEY**: `led_strip_del()` does NOT free external buffers (pixel_buf_allocated_internally = false) + +**Confirmed Solution**: Buffer transfer achieved through: +1. **Worker pool owns all RMT buffers** +2. **RMT channels destroyed/recreated** with appropriate buffer sizes +3. **External buffer management** prevents buffer deallocation during RMT destruction + +### Buffer Transfer Solution: Worker Pool Buffer Management + +**CORRECTED APPROACH**: Worker pool manages all RMT buffers and handles buffer sizing for different controllers. + +#### Worker Pool Buffer Management Strategy +```cpp +class RmtWorkerPool { +private: + struct WorkerState { + IRmtStrip* strip; + uint8_t* worker_buffer; // Pool-owned buffer for this worker + size_t buffer_capacity; // Current buffer size + WorkerConfig current_config; + bool is_available; + bool transmission_active; + }; + + fl::vector mWorkers; + + // Buffer pool for different sizes + fl::map> mBuffersBySize; + +public: + bool assignWorkerToController(RmtController5* controller) { + const WorkerConfig& config = controller->getWorkerConfig(); + const size_t requiredBufferSize = config.led_count * (config.is_rgbw ? 4 : 3); + + // Find available worker + WorkerState* worker = findAvailableWorker(); + if (!worker) return false; + + // Get appropriately sized buffer from pool + uint8_t* workerBuffer = acquireBuffer(requiredBufferSize); + if (!workerBuffer) return false; + + // CRITICAL: Wait for any active transmission to complete + if (worker->strip && worker->transmission_active) { + worker->strip->waitDone(); + worker->transmission_active = false; + } + + // Destroy existing RMT channel if configuration changed + if (worker->strip && (!configCompatible(worker->current_config, config) || + worker->buffer_capacity < requiredBufferSize)) { + delete worker->strip; // Destroy old RMT channel + worker->strip = nullptr; + + // Release old buffer + releaseBuffer(worker->worker_buffer, worker->buffer_capacity); + } + + // Copy controller's pixel data to worker's buffer + memcpy(workerBuffer, controller->getPixelBuffer(), controller->getBufferSize()); + + // Create new RMT channel with worker's external buffer + if (!worker->strip) { + worker->strip = IRmtStrip::CreateWithExternalBuffer( + config.pin, config.led_count, config.is_rgbw, + config.t0h, config.t0l, config.t1h, config.t1l, config.reset, + workerBuffer, // Worker's buffer, not controller's buffer + config.dma_mode + ); + + if (!worker->strip) { + releaseBuffer(workerBuffer, requiredBufferSize); + return false; + } + } + + worker->worker_buffer = workerBuffer; + worker->buffer_capacity = requiredBufferSize; + worker->current_config = config; + worker->is_available = false; + + // Start transmission + worker->strip->drawAsync(); + worker->transmission_active = true; + return true; + } + + void onWorkerComplete(WorkerState* worker) { + // Transmission complete - RMT hardware done with buffer + worker->transmission_active = false; + + if (!mQueuedControllers.empty()) { + // Assign to next waiting controller + RmtController5* nextController = mQueuedControllers.front(); + mQueuedControllers.pop_front(); + + const WorkerConfig& nextConfig = nextController->getWorkerConfig(); + const size_t nextBufferSize = nextConfig.led_count * (nextConfig.is_rgbw ? 4 : 3); + + if (worker->buffer_capacity >= nextBufferSize && + configCompatible(worker->current_config, nextConfig)) { + + // OPTIMIZATION: Reuse existing buffer and RMT channel + memcpy(worker->worker_buffer, nextController->getPixelBuffer(), nextController->getBufferSize()); + worker->strip->drawAsync(); + worker->transmission_active = true; + + } else { + // RECONFIGURE: Need different buffer size or RMT configuration + + // Release current buffer + releaseBuffer(worker->worker_buffer, worker->buffer_capacity); + + // Destroy RMT channel + delete worker->strip; + worker->strip = nullptr; + + // Get new appropriately-sized buffer + worker->worker_buffer = acquireBuffer(nextBufferSize); + if (!worker->worker_buffer) return; // Failed to get buffer + + // Copy next controller's data + memcpy(worker->worker_buffer, nextController->getPixelBuffer(), nextController->getBufferSize()); + + // Create new RMT channel with new buffer + worker->strip = IRmtStrip::CreateWithExternalBuffer( + nextConfig.pin, nextConfig.led_count, nextConfig.is_rgbw, + nextConfig.t0h, nextConfig.t0l, nextConfig.t1h, nextConfig.t1l, nextConfig.reset, + worker->worker_buffer, // New worker buffer + nextConfig.dma_mode + ); + + worker->buffer_capacity = nextBufferSize; + worker->current_config = nextConfig; + + // Start transmission + worker->strip->drawAsync(); + worker->transmission_active = true; + } + } else { + // No waiting controllers - worker becomes available + worker->is_available = true; + // Keep buffer and RMT channel configured for potential reuse + } + } + +private: + uint8_t* acquireBuffer(size_t size) { + // Round up to nearest power of 2 for efficient pooling + size_t poolSize = nextPowerOf2(size); + + auto& buffers = mBuffersBySize[poolSize]; + if (!buffers.empty()) { + uint8_t* buffer = buffers.back(); + buffers.pop_back(); + return buffer; + } + + // Allocate new buffer + return (uint8_t*)malloc(poolSize); + } + + void releaseBuffer(uint8_t* buffer, size_t size) { + size_t poolSize = nextPowerOf2(size); + mBuffersBySize[poolSize].push_back(buffer); + } +}; +``` + +#### Simplified Controller Implementation +```cpp +class RmtController5 { +private: + fl::vector mPixelBuffer; // Controller maintains its own data + WorkerConfig mWorkerConfig; + +public: + void loadPixelData(PixelIterator& pixels) { + // Store pixel data in persistent buffer (unchanged) + const int bytesPerPixel = mWorkerConfig.is_rgbw ? 4 : 3; + const int bufferSize = pixels.size() * bytesPerPixel; + + mPixelBuffer.resize(bufferSize); + + // Load pixel data into our persistent buffer + uint8_t* bufPtr = mPixelBuffer.data(); + if (mWorkerConfig.is_rgbw) { + while (pixels.has(1)) { + uint8_t r, g, b, w; + pixels.loadAndScaleRGBW(&r, &g, &b, &w); + *bufPtr++ = r; *bufPtr++ = g; *bufPtr++ = b; *bufPtr++ = w; + pixels.advanceData(); + pixels.stepDithering(); + } + } else { + while (pixels.has(1)) { + uint8_t r, g, b; + pixels.loadAndScaleRGB(&r, &g, &b); + *bufPtr++ = r; *bufPtr++ = g; *bufPtr++ = b; + pixels.advanceData(); + pixels.stepDithering(); + } + } + } + + void showPixels() { + // Pool handles all buffer management internally + RmtWorkerPool::getInstance().assignWorkerToController(this); + } + + // Provide buffer access for pool management + uint8_t* getPixelBuffer() { return mPixelBuffer.data(); } + size_t getBufferSize() const { return mPixelBuffer.size(); } +}; +``` + +### Optimized Teardown Strategy + +**Key Insight**: RMT strip teardown should be **conditional** based on worker pool demand: + +#### When N ≤ K (No Queued Controllers) +```cpp +void onWorkerComplete(RmtWorker* worker) { + if (mQueuedControllers.empty()) { + // NO TEARDOWN - keep worker configured and ready + // Worker maintains its led_strip configuration + // Optimizes for next frame if same controller used again + releaseWorker(worker); + } +} +``` + +#### When N > K (Queued Controllers Waiting) +```cpp +void onWorkerComplete(RmtWorker* worker) { + if (!mQueuedControllers.empty()) { + // IMMEDIATE TEARDOWN AND RECONFIGURATION + // Next controller is waiting - reconfigure immediately + RmtController5* nextController = mQueuedControllers.front(); + mQueuedControllers.pop_front(); + + // This triggers teardown in reconfigure() + startController(nextController, worker); + } +} +``` + +#### Buffer Change Requirement +**CRITICAL**: Even with identical pin/LED count/timing configuration, **teardown is always required** when switching between different controller buffers: + +```cpp +// Controller A has buffer at 0x12345678 +// Controller B has buffer at 0x87654321 +// Even if both have same pin/count/timing: +// - led_strip object MUST be recreated to use new buffer pointer +// - ESP-IDF has no API to change pixel_buf after creation +``` + +### Buffer Transfer Implementation Details + +#### Memory Safety +- **Controller Ownership**: Each `RmtController5` owns its persistent buffer +- **External Buffer Contract**: ESP-IDF won't free external buffers +- **Worker Lifecycle**: Workers destroy/recreate led_strip objects as needed +- **Buffer Validity**: Controllers must keep buffers valid during transmission + +#### Performance Considerations +- **Reconfiguration Cost**: Creating new led_strip objects has overhead +- **Buffer Copying**: No copying needed - workers use external buffers directly +- **Memory Efficiency**: Only one buffer per controller (no duplication) + +#### Error Handling +- **Reconfiguration Failures**: Handle led_strip creation failures gracefully +- **Buffer Size Mismatches**: Validate buffer sizes during reconfiguration +- **Transmission Errors**: Proper cleanup on transmission failures + +### Buffer Transfer Summary - CORRECTED + +**✅ SOLUTION**: Worker pool buffer management with RMT channel recreation +1. **Worker pool owns all RMT buffers** sized appropriately for each controller +2. **Controllers maintain their own pixel data** in persistent buffers +3. **Buffer copying required** from controller buffer to worker buffer +4. **RMT channels destroyed/recreated** with appropriately-sized external buffers +5. **ESP-IDF transmits from worker's external buffer** (not controller's buffer) + +**✅ POSSIBLE**: Buffer transfer through worker reconfiguration +- RMT channels can be destroyed and recreated with different external buffers +- Worker pool manages buffer allocation and sizing +- External buffers are not freed by ESP-IDF when RMT channels are destroyed + +**🔧 IMPLEMENTATION REQUIREMENTS**: +- Worker pool must manage buffers of different sizes +- RMT channel recreation required for buffer size changes +- Transmission completion synchronization before reconfiguration +- Buffer copying from controller to worker buffers + +## CORRECTED CONCLUSIONS AND RECOMMENDATIONS + +### Key Corrections to Original Analysis + +1. **Buffer Transfer IS Possible**: The original "NOT POSSIBLE" assessment was incorrect. Buffer transfer can be achieved through RMT channel recreation with appropriately-sized external buffers. + +2. **Worker Pool Must Manage Buffers**: The critical insight is that different controllers need different buffer sizes (RGB vs RGBW, different LED counts), so the worker pool must own and manage all RMT buffers. + +3. **Buffer Copying Required**: Unlike the original zero-copy approach, buffer copying from controller to worker is necessary to handle different buffer size requirements. + +4. **RMT Channel Recreation**: Workers must destroy and recreate RMT channels when switching between controllers with different requirements. + +### Recommended Implementation Strategy + +**✅ RECOMMENDED**: Worker Pool Buffer Management +- **Worker pool owns all RMT buffers** sized for different controller requirements +- **RMT channels use external buffers** managed by the worker pool +- **Buffer copying** from controller persistent buffers to worker buffers +- **RMT channel recreation** when buffer size or configuration changes +- **Transmission synchronization** to ensure safe reconfiguration + +### Benefits of Corrected Approach + +- ✅ **Supports Variable Buffer Sizes**: Handles different LED counts and RGB/RGBW modes +- ✅ **Proper Resource Management**: Worker pool manages buffer allocation/deallocation +- ✅ **Clean Separation**: Controllers focus on pixel data, workers handle hardware +- ✅ **Scalable Design**: Pool can optimize buffer reuse and minimize allocations +- ✅ **Thread Safe**: Proper synchronization prevents buffer access conflicts + +### Performance Considerations + +- ❌ **Buffer Copying Overhead**: Required due to different controller buffer sizes +- ✅ **Buffer Reuse Optimization**: Pool can reuse buffers for compatible configurations +- ✅ **Minimal RMT Recreation**: Only when buffer size or configuration changes +- ✅ **Efficient Memory Usage**: Pool manages buffer sizes optimally + +### Implementation Priority + +1. **Phase 1**: Implement basic worker pool with buffer management +2. **Phase 2**: Add buffer size optimization and reuse logic +3. **Phase 3**: Implement transmission synchronization and callbacks +4. **Phase 4**: Add performance optimizations and error handling + +## Future Enhancements + +1. **Priority System**: Allow high-priority strips to get workers first +2. **Smart Batching**: Group compatible strips to minimize reconfiguration +3. **Dynamic Scaling**: Adjust worker count based on usage patterns +4. **Metrics**: Add performance monitoring and statistics +5. **Buffer Pool Optimization**: Advanced buffer size prediction and caching \ No newline at end of file diff --git a/v1.5/lib/FastLED/GEMINI.md b/v1.5/lib/FastLED/GEMINI.md new file mode 100644 index 0000000..948d0e1 --- /dev/null +++ b/v1.5/lib/FastLED/GEMINI.md @@ -0,0 +1,1739 @@ +# FastLED Project Rules for Cursor + +## Cursor Configuration + +### Post-Change Hooks +Run linting after every code change: +```yaml +post_change_hooks: + - command: "bash lint" + description: "Run code formatting and linting" + working_directory: "." +``` + +## MCP Server Configuration +This project includes a custom MCP server (`mcp_server.py`) that provides tools for: +- Running tests with various options +- Compiling examples for different platforms +- Code fingerprinting and change detection +- Linting and formatting +- Project information and status +- **Build info analysis for platform-specific defines, compiler flags, and toolchain information** +- **Symbol analysis for binary optimization (all platforms)** +- Stack trace setup for enhanced debugging +- **🌐 FastLED Web Compiler with Playwright (FOREGROUND AGENTS ONLY)** +- **🚨 CRITICAL: `validate_completion` tool for background agents** + +To use the MCP server, run: `uv run mcp_server.py` + +**BACKGROUND AGENTS:** The MCP server includes a mandatory `validate_completion` tool that MUST be used before indicating task completion. This tool runs `bash test` and ensures all tests pass. + +### FastLED Web Compiler (FOREGROUND AGENTS ONLY) + +**🌐 FOR INTERACTIVE DEVELOPMENT:** The MCP server includes a `run_fastled_web_compiler` tool that: + +**Note:** For direct command-line WASM compilation, see the **WASM Sketch Compilation** section below. + +- **Compiles Arduino sketches to WASM** for browser execution +- **Captures console.log output** with playwright automation +- **Takes screenshots** of running visualizations +- **Monitors FastLED_onFrame calls** to verify proper initialization +- **Provides detailed analysis** of errors and performance + +**PREREQUISITES:** +- `pip install fastled` - FastLED web compiler +- `pip install playwright` - Browser automation (included in pyproject.toml) +- Docker (optional, for faster compilation) + +**USAGE EXAMPLES:** +```python +# Via MCP Server - Basic usage +use run_fastled_web_compiler tool with: +- example_path: "examples/Audio" +- capture_duration: 30 +- headless: false +- save_screenshot: true + +# Via MCP Server - Different examples +use run_fastled_web_compiler tool with: +- example_path: "examples/Blink" +- capture_duration: 15 +- headless: true + +# Via MCP Server - Quick test +use run_fastled_web_compiler tool with: +- example_path: "examples/wasm" +- capture_duration: 10 +``` + +**KEY FEATURES:** +- **Automatic browser installation:** Installs Chromium via playwright +- **Console.log capture:** Records all browser console output with timestamps +- **Error detection:** Identifies compilation failures and runtime errors +- **FastLED monitoring:** Tracks `FastLED_onFrame` calls to verify functionality +- **Screenshot capture:** Saves visualization images with timestamps +- **Docker detection:** Checks for Docker availability for faster builds +- **Background agent protection:** Automatically disabled for CI/background environments + +**🚫 BACKGROUND AGENT RESTRICTION:** +This tool is **COMPLETELY DISABLED** for background agents and CI environments. Background agents attempting to use this tool will receive an error message. This is intentional to prevent: +- Hanging processes in automated environments +- Resource conflicts in CI/CD pipelines +- Interactive browser windows in headless environments + +**CONSOLE.LOG CAPTURE PATTERN:** +The tool follows the pattern established in `ci/wasm_test.py` and `ci/ci/scrapers/`: +```javascript +// Example captured console.log patterns: +[14:25:30] log: FastLED_onFrame called: {"frame":1,"leds":100} +[14:25:30] log: Audio worklet initialized +[14:25:31] error: Missing audio_worklet_processor.js +[14:25:31] warning: WebGL context lost +``` + +**INTEGRATION WITH EXISTING CI:** +- Complements existing `ci/wasm_test.py` functionality +- Uses same playwright patterns as `ci/ci/scrapers/` +- Leverages existing pyproject.toml dependencies +- Compatible with existing Docker-based compilation workflow + +## Project Structure +- `src/` - Main FastLED library source code +- `examples/` - Arduino examples demonstrating FastLED usage +- `tests/` - Test files and infrastructure +- `ci/` - Continuous integration scripts +- `docs/` - Documentation + +## Key Commands +- `uv run test.py` - Run all tests +- `uv run test.py --cpp` - Run C++ tests only +- `uv run test.py TestName` - Run specific C++ test + - For example: running test_xypath.cpp would be uv run test.py xypath +- `./lint` - Run code formatting/linting +- `uv run ci/ci-compile.py uno --examples Blink` - Compile examples for specific platform + - For example (uno): `uv run ci/ci-compile.py uno --examples Blink` + - For example (esp32dev): `uv run ci/ci-compile.py esp32dev --examples Blink` + - For example (esp8266): `uv run ci/ci-compile.py esp01 --examples Blink` + - For example (teensy31): `uv run ci/ci-compile.py teensy31 --examples Blink` +- **WASM Compilation** - Compile Arduino sketches to run in web browsers: + - `uv run ci/wasm_compile.py examples/Blink -b --open` - Compile Blink to WASM and open browser + - `uv run ci/wasm_compile.py path/to/your/sketch -b --open` - Compile any sketch to WASM +- **Symbol Analysis** - Analyze binary size and optimization opportunities: + - `uv run ci/ci/symbol_analysis.py --board uno` - Analyze UNO platform + - `uv run ci/ci/symbol_analysis.py --board esp32dev` - Analyze ESP32 platform + - `uv run ci/demo_symbol_analysis.py` - Analyze all available platforms + +## 🤖 AI AGENT LINTING GUIDELINES + +### FOREGROUND AGENTS (Interactive Development) +**🚨 ALWAYS USE `bash lint` - DO NOT RUN INDIVIDUAL LINTING SCRIPTS** + +- **✅ CORRECT:** `bash lint` +- **❌ FORBIDDEN:** `./lint-js`, `./check-js`, `python3 scripts/enhance-js-typing.py` +- **❌ FORBIDDEN:** `uv run ruff check`, `uv run black`, individual tools + +**WHY:** `bash lint` provides: +- **Comprehensive coverage** - Python, C++, JavaScript, and enhancement analysis +- **Consistent workflow** - Single command for all linting needs +- **Proper sequencing** - Runs tools in the correct order with dependencies +- **Clear output** - Organized sections showing what's being checked +- **Agent guidance** - Shows proper usage for AI agents + +### BACKGROUND AGENTS (Automated/CI Environments) +**CAN USE FINE-GRAINED LINTING FOR SPECIFIC NEEDS** + +Background agents may use individual linting scripts when needed: +- `./lint-js` - JavaScript-only linting +- `./check-js` - JavaScript type checking +- `python3 scripts/enhance-js-typing.py` - JavaScript enhancement analysis +- `uv run ruff check` - Python linting only +- MCP server `lint_code` tool - Programmatic access + +**BUT STILL PREFER `bash lint` FOR COMPREHENSIVE CHECKING** + +### Linting Script Integration + +The `bash lint` command now includes: +1. **📝 Python Linting** - ruff, black, isort, pyright +2. **🔧 C++ Linting** - clang-format (when enabled) +3. **🌐 JavaScript Linting** - Deno lint, format check, type checking +4. **🔍 JavaScript Enhancement** - Analysis and recommendations +5. **💡 AI Agent Guidance** - Clear instructions for proper usage + +### When to Use Individual Scripts + +**FOREGROUND AGENTS:** Never. Always use `bash lint`. + +**BACKGROUND AGENTS:** Only when: +- **Debugging specific issues** with one language/tool +- **Testing incremental changes** to linting configuration +- **Running targeted analysis** for specific files +- **Integrating with automated workflows** via MCP server + +## Development Guidelines +- Follow existing code style and patterns +- Run tests before committing changes +- Use the MCP server tools for common tasks +- Check examples when making API changes + +## 🚨 CRITICAL: .INO FILE CREATION RULES 🚨 + +### ⚠️ THINK BEFORE CREATING .INO FILES ⚠️ + +**.ino files should be created SPARINGLY and ONLY when truly justified.** + +### 🚫 WHEN NOT TO CREATE .INO FILES: +- **Testing minor code changes** - Use existing test files or unit tests +- **Quick API validation** - Use unit tests or modify existing examples +- **Debugging specific functions** - Use test files, not new sketches +- **One-off experiments** - Create temporary test files instead +- **Small feature tests** - Extend existing relevant examples + +### ✅ WHEN TO CREATE .INO FILES: + +#### 1. **Temporary Testing (.ino)** +**Use Pattern:** `temp_.ino` or `test_.ino` +```cpp +// temp_json_api.ino - Testing new JSON fetch functionality +// test_networking.ino - Validating network stack changes +``` +- ✅ **FOR:** Testing new APIs during development +- ✅ **FOR:** Quick prototyping and validation +- ✅ **DELETE AFTER USE** - These are temporary by design + +#### 2. **Significant New Feature Examples** +**Use Pattern:** `examples//.ino` +```cpp +// examples/JsonFetchApi/JsonFetchApi.ino - Comprehensive JSON API example +// examples/NetworkStack/NetworkStack.ino - Major networking features +``` +- ✅ **FOR:** Large, comprehensive new features +- ✅ **FOR:** APIs that warrant dedicated examples +- ✅ **FOR:** Features that users will commonly implement +- ✅ **PERMANENT** - These become part of the example library + +### 📋 CREATION CHECKLIST: + +**Before creating ANY .ino file, ask:** + +1. **🤔 Is this testing a new API?** + - YES → Create `temp_.ino`, delete after testing + - NO → Consider alternatives + +2. **🤔 Is this a significant new feature that users will commonly use?** + - YES → Create `examples//.ino` + - NO → Use existing examples or test files + +3. **🤔 Can I modify an existing example instead?** + - YES → Extend existing example rather than creating new + - NO → Proceed with creation + +4. **🤔 Is this just for debugging/validation?** + - YES → Use unit tests or temporary test files + - NO → Consider if it meets the "significant feature" criteria + +### 🔍 REVIEW CRITERIA: + +**For Feature Examples (.ino files that stay):** +- ✅ **Demonstrates complete, real-world usage patterns** +- ✅ **Covers multiple aspects of the feature comprehensively** +- ✅ **Provides educational value for users** +- ✅ **Shows best practices and common use cases** +- ✅ **Is likely to be referenced by multiple users** + +**For Temporary Testing (.ino files that get deleted):** +- ✅ **Clearly named as temporary (temp_*, test_*)** +- ✅ **Focused on specific API validation** +- ✅ **Will be deleted after development cycle** +- ✅ **Too complex for unit test framework** + +### ❌ EXAMPLES OF WHAT NOT TO CREATE: +- `test_basic_led.ino` - Use existing Blink example +- `debug_colors.ino` - Use existing ColorPalette example +- `quick_brightness.ino` - Use unit tests or modify existing example +- `validate_pins.ino` - Use PinTest example or unit tests + +### ✅ EXAMPLES OF JUSTIFIED CREATIONS: +- `temp_new_wifi_api.ino` - Testing major new WiFi functionality (temporary) +- `examples/MachineLearning/MachineLearning.ino` - New ML integration feature (permanent) +- `temp_performance_test.ino` - Validating optimization changes (temporary) + +### 🧹 CLEANUP RESPONSIBILITY: +- **Temporary files:** Creator must delete when testing is complete +- **Feature examples:** Must be maintained and updated as API evolves +- **Abandoned files:** Regular cleanup reviews to remove unused examples + +**Remember: The examples directory is user-facing documentation. Every .ino file should provide clear value to FastLED users.** + +### Memory Management with Smart Pointers and Moveable Objects +**🚨 CRITICAL: Always use proper RAII patterns - smart pointers, moveable objects, or wrapper classes instead of raw pointers for resource management.** + +**Resource Management Options:** +- ✅ **PREFERRED**: `fl::shared_ptr` for shared ownership (multiple references to same object) +- ✅ **PREFERRED**: `fl::unique_ptr` for exclusive ownership (single owner, automatic cleanup) +- ✅ **PREFERRED**: Moveable wrapper objects (like `fl::promise`) for safe copying and transferring of unique resources +- ✅ **ACCEPTABLE**: `fl::vector` storing objects by value when objects support move/copy semantics +- ❌ **AVOID**: `fl::vector` storing raw pointers - use `fl::vector>` or `fl::vector>` +- ❌ **AVOID**: Manual `new`/`delete` - use `fl::make_shared()` or `fl::make_unique()` + +**Moveable Wrapper Pattern:** +When you have a unique resource (like a future, file handle, or network connection) that needs to be passed around easily, create a moveable wrapper class that: +- Internally manages the unique resource (often with `fl::unique_ptr` or similar) +- Provides copy semantics through shared implementation details +- Maintains clear ownership semantics +- Allows safe transfer between contexts + +**Examples:** +```cpp +// ✅ GOOD - Using smart pointers +fl::vector> mActiveClients; +auto client = fl::make_shared(); +mActiveClients.push_back(client); + +// ✅ GOOD - Using unique_ptr for exclusive ownership +fl::unique_ptr client = fl::make_unique(); + +// ✅ GOOD - Moveable wrapper objects (fl::promise example) +fl::vector> mActivePromises; // Copyable wrapper around unique future +fl::promise promise = fetch.get(url).execute(); +mActivePromises.push_back(promise); // Safe copy, shared internal state + +// ✅ GOOD - Objects stored by value (if copyable/moveable) +fl::vector mRequests; // When Request supports copy/move + +// ❌ BAD - Raw pointers require manual memory management +fl::vector mActivePromises; // Memory leaks possible +Promise* promise = new Promise(); // Who calls delete? +``` + +**fl::promise as Moveable Wrapper Example:** +```cpp +// fl::promise wraps a unique fl::future but provides copyable semantics +class promise { + fl::shared_ptr> mImpl; // Shared wrapper around unique resource +public: + promise(const promise& other) : mImpl(other.mImpl) {} // Safe copying + promise(promise&& other) : mImpl(fl::move(other.mImpl)) {} // Move support + // ... wrapper delegates to internal future +}; + +// Usage - can be copied and passed around safely +fl::promise promise = http_get_promise("https://api.example.com"); +someContainer.push_back(promise); // Copy is safe +processAsync(promise); // Can pass to multiple places +``` + +**Why This Pattern:** +- **Automatic cleanup** - No memory leaks from forgotten `delete` calls +- **Exception safety** - Resources automatically freed even if exceptions occur +- **Clear ownership** - Code clearly shows who owns what objects +- **Thread safety** - Smart pointers provide atomic reference counting +- **Easy sharing** - Moveable wrappers allow safe copying of unique resources +- **API flexibility** - Can pass resources between different contexts safely + +## 🔧 CMAKE BUILD SYSTEM ARCHITECTURE (DEPRECATED - NO LONGER USED) + +**⚠️ IMPORTANT: CMake build system is no longer used. FastLED now uses a Python-based build system.** + +### Build System Overview (Historical Reference) +FastLED previously used a sophisticated CMake build system located in `tests/cmake/` with modular configuration: + +**Core Build Files:** +- `tests/CMakeLists.txt` - Main CMake entry point +- `tests/cmake/` - Modular CMake configuration directory + +**Key CMake Modules:** +- `LinkerCompatibility.cmake` - **🚨 CRITICAL for linker issues** - GNU↔MSVC flag translation, lld-link compatibility, warning suppression +- `CompilerDetection.cmake` - Compiler identification and toolchain setup +- `CompilerFlags.cmake` - Compiler-specific flag configuration +- `DebugSettings.cmake` - Debug symbol and optimization configuration +- `OptimizationSettings.cmake` - LTO and performance optimization settings +- `ParallelBuild.cmake` - Parallel compilation and linker selection (mold, lld) +- `TestConfiguration.cmake` - Test target setup and configuration +- `BuildOptions.cmake` - Build option definitions and defaults + +### Linker Configuration (Most Important for Build Issues) + +**🎯 PRIMARY LOCATION for linker problems: `tests/cmake/LinkerCompatibility.cmake`** + +**Key Functions:** +- `apply_linker_compatibility()` - **Main entry point** - auto-detects lld-link and applies compatibility +- `translate_gnu_to_msvc_linker_flags()` - Converts GNU-style flags to MSVC-style for lld-link +- `get_dead_code_elimination_flags()` - Platform-specific dead code elimination +- `get_debug_flags()` - Debug information configuration +- `get_optimization_flags()` - Performance optimization flags + +**Linker Detection Logic:** +```cmake +if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + find_program(LLDLINK_EXECUTABLE lld-link) + if(LLDLINK_EXECUTABLE) + # Apply lld-link compatibility automatically + endif() +endif() +``` + +**Common Linker Issues & Solutions:** +- **lld-link warnings**: Suppressed via `/ignore:4099` in `apply_linker_compatibility()` +- **GNU→MSVC flag translation**: Automatic in `translate_gnu_to_msvc_linker_flags()` +- **Dead code elimination**: Platform-specific via `get_dead_code_elimination_flags()` +- **Debug symbol conflicts**: Handled in `get_debug_flags()` + +### Compiler Configuration + +**Compiler Detection**: `tests/cmake/CompilerDetection.cmake` +- Auto-detects Clang, GCC, MSVC +- Sets up toolchain-specific configurations +- Handles cross-compilation scenarios + +**Compiler Flags**: `tests/cmake/CompilerFlags.cmake` +- Warning configurations per compiler +- Optimization level management +- Platform-specific adjustments + +### Build Options & Configuration + +**Available Build Options** (defined in `BuildOptions.cmake`): +- `FASTLED_DEBUG_LEVEL` - Debug information level (NONE, MINIMAL, STANDARD, FULL) +- `FASTLED_OPTIMIZATION_LEVEL` - Optimization (O0, O1, O2, O3, Os, Ofast) +- `FASTLED_ENABLE_LTO` - Link-time optimization +- `FASTLED_ENABLE_PARALLEL_BUILD` - Parallel compilation +- `FASTLED_STATIC_RUNTIME` - Static runtime linking + +### Testing Infrastructure + +**Test Configuration**: `tests/cmake/TestConfiguration.cmake` +- Defines test targets and dependencies +- Configures test execution parameters +- Sets up coverage and profiling + +**Test Execution**: +- `bash test` - Comprehensive test runner (preferred) +- `uv run test.py` - Python test interface +- Individual test executables in `.build/bin/` + +### Build Troubleshooting Guide + +**For Linker Issues:** +1. **Check `tests/cmake/LinkerCompatibility.cmake` first** +2. Look for lld-link detection and compatibility functions +3. Check GNU→MSVC flag translation logic +4. Verify warning suppression settings + +**For Compiler Issues:** +1. Check `tests/cmake/CompilerDetection.cmake` for detection logic +2. Review `tests/cmake/CompilerFlags.cmake` for flag conflicts +3. Verify optimization settings in `OptimizationSettings.cmake` + +**For Build Performance:** +1. Check `tests/cmake/ParallelBuild.cmake` for parallel settings +2. Review LTO configuration in `OptimizationSettings.cmake` +3. Verify linker selection (mold, lld, default) + + + + +## 🚨 CRITICAL REQUIREMENTS FOR ALL AGENTS (FOREGROUND & BACKGROUND) 🚨 + +## 🚨 MANDATORY COMMAND EXECUTION RULES 🚨 + +### FOREGROUND AGENTS (Interactive Development) + +**FOREGROUND AGENTS MUST FOLLOW THESE COMMAND EXECUTION PATTERNS:** + +#### Python Code Execution: +- ❌ **NEVER run Python code directly** +- ✅ **ALWAYS create/modify tmp.py** with your code +- ✅ **ALWAYS run: `uv run tmp.py`** + +#### Shell Command Execution: +- ❌ **NEVER run shell commands directly** +- ✅ **ALWAYS create/modify tmp.sh** with your commands +- ✅ **ALWAYS run: `bash tmp.sh`** + +#### Command Execution Examples: + +**Python Code Pattern:** +```python +# tmp.py +import subprocess +result = subprocess.run(['git', 'status'], capture_output=True, text=True) +print(result.stdout) +``` +Then run: `uv run tmp.py` + +**Shell Commands Pattern:** +```bash +# tmp.sh +#!/bin/bash +git status +ls -la +uv run ci/ci-compile.py uno --examples Blink +``` +Then run: `bash tmp.sh` + +### BACKGROUND AGENTS (Automated/CI Environments) + +**BACKGROUND AGENTS MUST FOLLOW THESE RESTRICTED COMMAND EXECUTION PATTERNS:** + +#### Python Code Execution: +- ❌ **NEVER run Python code directly** +- ❌ **NEVER create/use tmp.py files** (forbidden for background agents) +- ✅ **ALWAYS use `uv run` with existing scripts** (e.g., `uv run test.py`, `uv run ci/ci-compile.py`) +- ✅ **ALWAYS use MCP server tools** for programmatic operations when available + +#### Shell Command Execution: +- ❌ **NEVER run shell commands directly** +- ❌ **NEVER create/use tmp.sh files** (forbidden for background agents) +- ✅ **ALWAYS use existing bash scripts** (e.g., `bash test`, `bash lint`) +- ✅ **ALWAYS use `uv run` for Python scripts** with proper arguments +- ✅ **ALWAYS use MCP server tools** for complex operations when available + +#### Background Agent Command Examples: + +**✅ ALLOWED - Using existing scripts:** +```bash +bash test +bash lint +uv run test.py audio_json_parsing +uv run ci/ci-compile.py uno --examples Blink +``` + +**❌ FORBIDDEN - Creating temporary files:** +```bash +# DON'T DO THIS - tmp.sh is forbidden for background agents +echo "git status" > tmp.sh +bash tmp.sh +``` + +**✅ PREFERRED - Using MCP server tools:** +```bash +uv run mcp_server.py +# Then use appropriate MCP tools like: validate_completion, symbol_analysis, etc. +``` + +### DELETE Operations - DANGER ZONE (ALL AGENTS): +- 🚨 **STOP and ask for permission** before ANY delete operations +- ✅ **EXCEPTION:** Single files that you just created are OK to delete +- ❌ **NEVER delete multiple files** without explicit permission +- ❌ **NEVER delete directories** without explicit permission +- ❌ **NEVER delete system files or project files** without permission + +### Git-Bash Terminal Truncation Issue +**🚨 IMPORTANT: Git-Bash terminal may truncate the first character of commands** + +**Problem:** The git-bash terminal on Windows sometimes truncates the first character of commands, causing them to fail or execute incorrectly. + +**Solution:** Pre-pend commands with an additional space when using git-bash: +- ✅ **CORRECT**: ` bash test` (note the leading space) +- ❌ **INCORRECT**: `bash test` (may get truncated to `ash test`) + +**Examples:** +```bash +# Good - with leading space for git-bash compatibility + bash test + uv run test.py + bash lint + +# Bad - may get truncated in git-bash +bash test +uv run test.py +bash lint +``` + +**Why:** This ensures commands work reliably across all terminal environments, especially git-bash on Windows systems. + +### Why These Rules Exist: +- **Ensures all operations are reviewable and traceable** +- **Prevents accidental destructive operations in automated environments** +- **Allows for better debugging and error handling** +- **Maintains consistency across different agent types** +- **Provides audit trail for all system modifications** +- **Prevents background agents from creating unnecessary temporary files** + +**These command execution rules apply to ALL operations including but not limited to:** +- File system operations (creating, modifying, deleting files) +- Git operations (commits, pushes, branch changes) +- Package installations and updates +- Build and compilation commands +- Test execution and validation +- System configuration changes + + +### MANDATORY MEMORY REFRESH BEFORE COMPLETION +**🚨 ALL AGENTS MUST REFRESH THEIR MEMORY BEFORE CONCLUDING WORK:** + +- **The AI shall read the cursor rules again to find out what to do before task is considered complete** + - This is MANDATORY for both foreground and background agents + - Reading the cursor rules refreshes your memory about the latest project rules, coding standards, and requirements + - This ensures you have current information about testing procedures, validation tools, and completion requirements + - Do NOT indicate your work is "done" until you have refreshed your memory by reading the cursor rules + +## 🚨 CRITICAL REQUIREMENTS FOR BACKGROUND AGENTS 🚨 + +### MANDATORY PRE-COMPLETION VALIDATION +**ALL BACKGROUND AGENTS MUST FOLLOW THESE REQUIREMENTS BEFORE INDICATING COMPLETION:** + +1. **🚨 ALWAYS RUN `bash test` BEFORE INDICATING COMPLETION** + - This is MANDATORY and NON-NEGOTIABLE for all background agents + - The command `bash test` runs the full test suite including unit tests and compilation checks + - Background agents MUST NOT indicate they are "done" until ALL tests pass + +2. **🚨 USE MCP SERVER VALIDATION TOOL** + - Use the `validate_completion` tool from the MCP server: `uv run mcp_server.py` + - This tool runs `bash test` and validates that all tests pass + - Only indicate completion if this validation tool returns success + +3. **🚨 ZERO TOLERANCE FOR TEST FAILURES** + - If ANY test fails, the background agent MUST fix the issues before completion + - Do NOT indicate completion with failing tests + - Do NOT ignore test errors or warnings + +### Background Agent Completion Checklist: +- [ ] All code changes have been made +- [ ] `bash test` has been run and ALL tests pass +- [ ] No compilation errors or warnings +- [ ] MCP server `validate_completion` tool shows success +- [ ] Only then indicate task completion + +**FAILURE TO FOLLOW THESE REQUIREMENTS WILL RESULT IN BROKEN CODE SUBMISSIONS.** + +### C++ Design Patterns +**Shared Implementation Pattern:** When writing a lot of code that applies the same operation on a bunch of C++ objects, try and determine if those objects share a common base class or internal object. If so consider whether it's appropriate to move the implementation into a shared space. + +## Code Standards + +### Avoid std:: Prefixed Functions +**DO NOT use `std::` prefixed functions or headers in the codebase.** This project provides its own STL-equivalent implementations under the `fl::` namespace. + +**Examples of what to avoid and use instead:** + +**Headers:** + +**Core Language Support:** +- ❌ `#include ` → ✅ `#include "fl/type_traits.h"` +- ❌ `#include ` → ✅ `#include "fl/algorithm.h"` +- ❌ `#include ` → ✅ `#include "fl/functional.h"` +- ❌ `#include ` → ✅ `#include "fl/initializer_list.h"` + +**Containers:** +- ❌ `#include ` → ✅ `#include "fl/vector.h"` +- ❌ `#include ` → ✅ `#include "fl/map.h"` +- ❌ `#include ` → ✅ `#include "fl/hash_map.h"` +- ❌ `#include ` → ✅ `#include "fl/hash_set.h"` +- ❌ `#include ` → ✅ `#include "fl/set.h"` +- ❌ `#include ` → ✅ `#include "fl/slice.h"` + +**Utilities & Smart Types:** +- ❌ `#include ` → ✅ `#include "fl/optional.h"` +- ❌ `#include ` → ✅ `#include "fl/variant.h"` +- ❌ `#include ` → ✅ `#include "fl/pair.h"` (for std::pair) +- ❌ `#include ` → ✅ `#include "fl/string.h"` +- ❌ `#include ` → ✅ `#include "fl/scoped_ptr.h"` or `#include "fl/ptr.h"` + +**Stream/IO:** +- ❌ `#include ` → ✅ `#include "fl/sstream.h"` + +**Threading:** +- ❌ `#include ` → ✅ `#include "fl/thread.h"` + +**Math & System:** +- ❌ `#include ` → ✅ `#include "fl/math.h"` +- ❌ `#include ` → ✅ `#include "fl/stdint.h"` + +**Functions and classes:** +- ❌ `std::move()` → ✅ `fl::move()` +- ❌ `std::forward()` → ✅ `fl::forward()` +- ❌ `std::vector` → ✅ `fl::vector` +- ❌ `std::enable_if` → ✅ `fl::enable_if` + +**Why:** The project maintains its own implementations to ensure compatibility across all supported platforms and to avoid bloating the library with unnecessary STL dependencies. + +**Before using any standard library functionality, check if there's a `fl::` equivalent in the `src/fl/` directory first.** + +### Compiler Warning Suppression +**ALWAYS use the FastLED compiler control macros from `fl/compiler_control.h` for warning suppression.** This ensures consistent cross-compiler support and proper handling of platform differences. + +**Correct Warning Suppression Pattern:** +```cpp +#include "fl/compiler_control.h" + +// Suppress specific warning around problematic code +FL_DISABLE_WARNING_PUSH +FL_DISABLE_FORMAT_TRUNCATION // Use specific warning macros +// ... code that triggers warnings ... +FL_DISABLE_WARNING_POP +``` + +**Available Warning Suppression Macros:** +- ✅ `FL_DISABLE_WARNING_PUSH` / `FL_DISABLE_WARNING_POP` - Standard push/pop pattern +- ✅ `FL_DISABLE_WARNING(warning_name)` - Generic warning suppression (use sparingly) +- ✅ `FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS` - Clang global constructor warnings +- ✅ `FL_DISABLE_WARNING_SELF_ASSIGN_OVERLOADED` - Clang self-assignment warnings +- ✅ `FL_DISABLE_FORMAT_TRUNCATION` - GCC format truncation warnings + +**What NOT to do:** +- ❌ **NEVER use raw `#pragma` directives** - they don't handle compiler differences +- ❌ **NEVER write manual `#ifdef __clang__` / `#ifdef __GNUC__` blocks** - use the macros +- ❌ **NEVER ignore warnings without suppression** - fix the issue or suppress appropriately + +**Examples:** +```cpp +// ✅ CORRECT - Using FastLED macros +#include "fl/compiler_control.h" + +FL_DISABLE_WARNING_PUSH +FL_DISABLE_FORMAT_TRUNCATION +// Code that triggers format truncation warnings +sprintf(small_buffer, "Very long format string %d", value); +FL_DISABLE_WARNING_POP + +// ❌ INCORRECT - Raw pragma directives +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" +sprintf(small_buffer, "Very long format string %d", value); +#pragma GCC diagnostic pop +``` + +**Why:** The FastLED compiler control system automatically handles: +- **Compiler detection** (GCC vs Clang vs MSVC) +- **Version compatibility** (warnings that don't exist on older compilers) +- **Platform specifics** (AVR, ESP32, ARM, etc.) +- **Consistent naming** across different compiler warning systems + +### Debug Printing +**Use `FL_WARN` for debug printing throughout the codebase.** This ensures consistent debug output that works in both unit tests and live application testing. + +**Usage:** +- ✅ `FL_WARN("Debug message: " << message);` +- ❌ `FL_WARN("Value: %d", value);` + +**Why:** `FL_WARN` provides a unified logging interface that works across all platforms and testing environments, including unit tests and Arduino sketches. + +### Emoticon and Emoji Usage Policy +**NO emoticons or emoji characters are allowed in C++ source files.** This ensures professional, maintainable code that works correctly across all platforms and development environments. + +**Prohibited in C++ Files:** +- ❌ **C++ source files** (*.cpp, *.c) +- ❌ **C++ header files** (*.h, *.hpp) +- ❌ **Comments in C++ files** - use clear text instead +- ❌ **String literals in C++ code** - use descriptive text +- ❌ **Log messages in C++ code** - use text prefixes like "SUCCESS:", "ERROR:", "WARNING:" + +**Examples of what NOT to do in C++ files:** +```cpp +// ❌ BAD - Emoticons in comments +// 🎯 This function handles user input + +// ❌ BAD - Emoticons in log messages +FL_WARN("✅ Operation successful!"); +FL_WARN("❌ Error occurred: " << error_msg); + +// ❌ BAD - Emoticons in string literals +const char* status = "🔄 Processing..."; +``` + +**Examples of correct alternatives in C++ files:** +```cpp +// ✅ GOOD - Clear text in comments +// TUTORIAL: This function handles user input + +// ✅ GOOD - Text prefixes in log messages +FL_WARN("SUCCESS: Operation completed successfully!"); +FL_WARN("ERROR: Failed to process request: " << error_msg); + +// ✅ GOOD - Descriptive text in string literals +const char* status = "PROCESSING: Request in progress..."; +``` + +**Allowed in Python Files:** +- ✅ **Python source files** (*.py) - emoticons are acceptable for build scripts, tools, and utilities +- ✅ **Python comments and docstrings** - can use emoticons for clarity in development tools +- ✅ **Python log messages** - emoticons OK in build/test output for visual distinction + +**Why:** +- **Cross-platform compatibility** - Some compilers/platforms have issues with Unicode characters +- **Professional codebase** - Maintains serious, enterprise-grade appearance +- **Accessibility** - Screen readers and text-based tools work better with plain text +- **Consistency** - Ensures uniform code style across all C++ files +- **Debugging** - Text-based prefixes are easier to search and filter in logs + +**Before adding any visual indicators to C++ code, use text-based alternatives like "TODO:", "NOTE:", "WARNING:", "SUCCESS:", "ERROR:" prefixes.** + +### Naming Conventions +**Follow these naming conventions for consistency across the codebase:** + +**Simple Objects:** +- ✅ Use lowercase class names for simple objects (e.g., `fl::vec2f`, `fl::point`, `fl::rect`) +- ❌ Avoid: `fl::Vec2f`, `fl::Point`, `fl::Rect` + +**Complex Objects:** +- ✅ Use CamelCase with uppercase first character for complex objects (e.g., `Raster`, `Controller`, `Canvas`) +- ❌ Avoid: `raster`, `controller`, `canvas` + +**Pixel Types:** +- ✅ Use ALL CAPS for pixel types (e.g., `CRGB`, `CHSV`, `HSV16`, `RGB24`) +- ❌ Avoid: `crgb`, `Crgb`, `chsv`, `Chsv` + +**Member Variables and Functions:** + +**Complex Classes/Objects:** +- ✅ **Member variables:** Use `mVariableName` format (e.g., `mPixelCount`, `mBufferSize`, `mCurrentIndex`) +- ✅ **Member functions:** Use camelCase (e.g., `getValue()`, `setPixelColor()`, `updateBuffer()`) +- ❌ Avoid: `m_variable_name`, `variableName`, `GetValue()`, `set_pixel_color()` + +**Simple Classes/Structs:** +- ✅ **Member variables:** Use lowercase snake_case (e.g., `x`, `y`, `width`, `height`, `pixel_count`) +- ✅ **Member functions:** Use camelCase (e.g., `getValue()`, `setPosition()`, `normalize()`) +- ❌ Avoid: `mX`, `mY`, `get_value()`, `set_position()` + +**Examples:** + +```cpp +// Complex class - use mVariableName for members +class Controller { +private: + int mPixelCount; // ✅ Complex class member variable + uint8_t* mBuffer; // ✅ Complex class member variable + bool mIsInitialized; // ✅ Complex class member variable + +public: + void setPixelCount(int count); // ✅ Complex class member function + int getPixelCount() const; // ✅ Complex class member function + void updateBuffer(); // ✅ Complex class member function +}; + +// Simple struct - use snake_case for members +struct vec2 { + int x; // ✅ Simple struct member variable + int y; // ✅ Simple struct member variable + + float magnitude() const; // ✅ Simple struct member function + void normalize(); // ✅ Simple struct member function +}; + +struct point { + float x; // ✅ Simple struct member variable + float y; // ✅ Simple struct member variable + float z; // ✅ Simple struct member variable + + void setPosition(float x, float y, float z); // ✅ Simple struct member function + float distanceTo(const point& other) const; // ✅ Simple struct member function +}; +``` + +**Why:** These conventions help distinguish between different categories of objects and maintain consistency with existing FastLED patterns. Complex classes use Hungarian notation for member variables to clearly distinguish them from local variables, while simple structs use concise snake_case for lightweight data containers. + +### Container Parameter Types +**Prefer `fl::span` over `fl::vector` or arrays for function parameters.** `fl::span` provides a non-owning view that automatically converts from various container types, making APIs more flexible and efficient. + +**Examples:** +- ✅ `void processData(fl::span data)` - accepts arrays, vectors, and other containers +- ❌ `void processData(fl::vector& data)` - only accepts fl::Vector +- ❌ `void processData(uint8_t* data, size_t length)` - requires manual length tracking + +**Benefits:** +- **Automatic conversion:** `fl::span` can automatically convert from `fl::vector`, C-style arrays, and other container types +- **Type safety:** Maintains compile-time type checking while being more flexible than raw pointers +- **Performance:** Zero-cost abstraction that avoids unnecessary copying or allocation +- **Consistency:** Provides a uniform interface for working with contiguous data + +**When to use `fl::vector` instead:** +- When you need ownership and dynamic resizing capabilities +- When storing data as a class member that needs to persist + +**Why:** Using `fl::span` for parameters makes functions more reusable and avoids forcing callers to convert their data to specific container types. + +### Exception Handling +**DO NOT use try-catch blocks or C++ exception handling in the codebase.** FastLED is designed to work on embedded systems like Arduino where exception handling may not be available or desired due to memory and performance constraints. + +**Examples of what to avoid and use instead:** + +**Avoid Exception Handling:** +- ❌ `try { ... } catch (const std::exception& e) { ... }` - Exception handling not available on many embedded platforms +- ❌ `throw std::runtime_error("error message")` - Throwing exceptions not supported +- ❌ `#include ` or `#include ` - Exception headers not needed + +**Use Error Handling Alternatives:** +- ✅ **Return error codes:** `bool function() { return false; }` or custom error enums +- ✅ **Optional types:** `fl::optional` for functions that may not return a value +- ✅ **Assertions:** `FL_ASSERT(condition)` for debug-time validation +- ✅ **Early returns:** `if (!valid) return false;` for error conditions +- ✅ **Status objects:** Custom result types that combine success/failure with data + +**Examples of proper error handling:** +```cpp +// Good: Using return codes +bool initializeHardware() { + if (!setupPins()) { + FL_WARN("Failed to setup pins"); + return false; + } + return true; +} + +// Good: Using fl::optional +fl::optional calculateValue(int input) { + if (input < 0) { + return fl::nullopt; // No value, indicates error + } + return fl::make_optional(sqrt(input)); +} + +// Good: Using early returns +void processData(const uint8_t* data, size_t len) { + if (!data || len == 0) { + FL_WARN("Invalid input data"); + return; // Early return on error + } + // Process data... +} +``` + +**Why:** Many embedded platforms (especially Arduino-compatible boards) don't support C++ exceptions or have them disabled to save memory and improve performance. FastLED must work reliably across all supported platforms. + +### JSON Usage - Ideal API Patterns +**🎯 PREFERRED: Use the modern `fl::Json` class for all JSON operations.** FastLED provides an ideal JSON API that prioritizes type safety, ergonomics, and crash-proof operation. + +**✅ IDIOMATIC JSON USAGE:** +```cpp +// NEW: Clean, safe, idiomatic API +fl::Json json = fl::Json::parse(jsonStr); +int brightness = json["config"]["brightness"] | 128; // Gets value or 128 default +string name = json["device"]["name"] | string("default"); // Type-safe with default +bool enabled = json["features"]["networking"] | false; // Never crashes + +// Array operations +if (json["effects"].contains("rainbow")) { + // Safe array checking +} +``` + +**❌ DISCOURAGED: Verbose legacy API:** +```cpp +// OLD: Verbose, error-prone API (still works, but not recommended) +fl::JsonDocument doc; +fl::string error; +fl::parseJson(jsonStr, &doc, &error); +int brightness = doc["config"]["brightness"].as(); // Can crash if missing +``` + +**Key Benefits of Ideal API:** +- **🛡️ Type Safety** - No crashes on missing fields or type mismatches +- **🎯 Default Values** - Clean `json["key"] | default` syntax +- **📖 Readable Code** - 50% less boilerplate for common operations +- **🚀 Testing** - Easy test data construction with `JsonBuilder` + +**📚 Reference Example:** See `examples/Json/Json.ino` for comprehensive usage patterns and API comparison. + +**Testing with JsonBuilder:** +```cpp +// Easy test data construction +auto json = JsonBuilder() + .set("brightness", 128) + .set("enabled", true) + .set("name", "test_device") + .build(); + +// Type-safe testing +CHECK_EQ(json["brightness"] | 0, 128); +CHECK(json["enabled"] | false); +``` + +**🎯 GUIDELINE:** Always prefer the ideal `fl::Json` API for new code. The legacy `fl::parseJson` API remains available for backward compatibility but should be avoided in new implementations. + +## ⚠️ CRITICAL WARNING: C++ ↔ JavaScript Bindings + +**🚨 EXTREMELY IMPORTANT: DO NOT MODIFY FUNCTION SIGNATURES IN WEBASSEMBLY BINDINGS WITHOUT EXTREME CAUTION! 🚨** + +The FastLED project includes WebAssembly (WASM) bindings that bridge C++ and JavaScript code. **Changing function signatures in these bindings is a major source of runtime errors and build failures.** + +### Key Binding Files (⚠️ HIGH RISK ZONE ⚠️): +- `src/platforms/wasm/js_bindings.cpp` - Main JavaScript interface via EM_ASM +- `src/platforms/wasm/ui.cpp` - UI update bindings with extern "C" wrappers +- `src/platforms/wasm/active_strip_data.cpp` - Strip data bindings via EMSCRIPTEN_BINDINGS +- `src/platforms/wasm/fs_wasm.cpp` - File system bindings via EMSCRIPTEN_BINDINGS + +### Before Making ANY Changes to These Files: + +1. **🛑 STOP and consider if the change is absolutely necessary** +2. **📖 Read the warning comments at the top of each binding file** +3. **🧪 Test extensively on WASM target after any changes** +4. **🔗 Verify both C++ and JavaScript sides remain synchronized** +5. **📝 Update corresponding JavaScript code if function signatures change** + +### Common Binding Errors: +- **Parameter type mismatches** (e.g., `const char*` vs `std::string`) +- **Return type changes** that break JavaScript expectations +- **Function name changes** without updating JavaScript calls +- **Missing `extern "C"` wrappers** for EMSCRIPTEN_KEEPALIVE functions +- **EMSCRIPTEN_BINDINGS macro changes** without updating JS Module calls + +### If You Must Modify Bindings: +1. **Update BOTH sides simultaneously** (C++ and JavaScript) +2. **Maintain backward compatibility** when possible +3. **Add detailed comments** explaining the interface contract +4. **Test thoroughly** with real WASM builds, not just compilation +5. **Update documentation** and interface specs + +**Remember: The bindings are a CONTRACT between C++ and JavaScript. Breaking this contract causes silent failures and mysterious bugs that are extremely difficult to debug.** + +## 🚨 WASM PLATFORM SPECIFIC RULES 🚨 + +### WASM Unified Build Awareness + +**🚨 CRITICAL: WASM builds use unified compilation when `FASTLED_ALL_SRC=1` is enabled (automatic for Clang builds)** + +**Root Cause**: Multiple .cpp files are compiled together in a single compilation unit, causing: +- Duplicate function definitions +- Type signature conflicts +- Symbol redefinition errors + +**MANDATORY RULES:** +- **ALWAYS check for unified builds** when modifying WASM platform files +- **NEVER create duplicate function definitions** across WASM .cpp files +- **USE `EMSCRIPTEN_KEEPALIVE` functions as canonical implementations** +- **MATCH Emscripten header signatures exactly** for external C functions +- **REMOVE conflicting implementations** and add explanatory comments + +**Fix Pattern Example:** +```cpp +// In timer.cpp (CANONICAL) +extern "C" { +EMSCRIPTEN_KEEPALIVE uint32_t millis() { + // Implementation +} +} + +// In js.cpp (FIXED) +extern "C" { +// NOTE: millis() and micros() functions are defined in timer.cpp with EMSCRIPTEN_KEEPALIVE +// to avoid duplicate definitions in unified builds +} +``` + +### WASM Async Platform Pump Pattern + +**🚨 CRITICAL: Avoid long blocking sleeps that prevent responsive async processing** + +**MANDATORY RULES:** +- **AVOID long blocking sleeps** (e.g., `emscripten_sleep(100)`) in main loops +- **USE short sleep intervals** (1ms) for responsive yielding in main loops +- **ALLOW JavaScript to control timing** via extern functions rather than blocking C++ loops + +**Correct Implementation:** +```cpp +// ✅ GOOD - responsive yielding without blocking +while (true) { + // Keep pthread alive for extern function calls + // Use short sleep for responsiveness + emscripten_sleep(1); // 1ms yield for responsiveness +} +``` + +**Why This Matters:** +- Long blocking sleeps prevent responsive browser interaction +- JavaScript should control FastLED timing via requestAnimationFrame +- Short sleep intervals maintain responsiveness while allowing other threads to work + +### WASM Function Signature Matching + +**🚨 CRITICAL: External C function declarations must match Emscripten headers exactly** + +**Common Error Pattern:** +```cpp +// ❌ WRONG - causes compilation error +extern "C" void emscripten_sleep(int ms); + +// ✅ CORRECT - matches official Emscripten header +extern "C" void emscripten_sleep(unsigned int ms); +``` + +**MANDATORY RULES:** +- **ALWAYS verify against official Emscripten header signatures** +- **NEVER assume parameter types** - check the actual headers +- **UPDATE signatures immediately** when compilation errors occur + +### WASM Sketch Compilation + +**🎯 HOW TO COMPILE ANY ARDUINO SKETCH TO WASM:** + +**Basic Compilation Commands:** +```bash +# Compile any sketch directory to WASM +uv run ci/wasm_compile.py path/to/your/sketch + +# Compile with build and open browser automatically +uv run ci/wasm_compile.py path/to/your/sketch -b --open + +# Compile examples/Blink to WASM +uv run ci/wasm_compile.py examples/Blink -b --open + +# Compile examples/DemoReel100 to WASM +uv run ci/wasm_compile.py examples/DemoReel100 -b --open +``` + +**Output:** Creates `fastled_js/` folder with: +- `fastled.js` - JavaScript loader +- `fastled.wasm` - WebAssembly binary +- `index.html` - HTML page to run the sketch + +**Run in Browser:** +```bash +# Simple HTTP server to test +cd fastled_js +python -m http.server 8000 +# Open http://localhost:8000 +``` + +**🚨 REQUIREMENTS:** +- **Docker must be installed and running** +- **Internet connection** (for Docker image download on first run) +- **~1GB RAM** for Docker container during compilation + +### WASM Testing Requirements + +**🚨 MANDATORY: Always test WASM compilation after platform file changes** + +**Platform Testing Commands:** +```bash +# Test WASM platform changes (for platform developers) +uv run ci/wasm_compile.py -b examples/wasm + +# Quick test without full build +uv run ci/wasm_compile.py examples/wasm --quick +``` + +**Watch For These Error Patterns:** +- `error: conflicting types for 'function_name'` +- `error: redefinition of 'function_name'` +- `warning: attribute declaration must precede definition` +- `RuntimeError: unreachable` (often async-related) + +**MANDATORY RULES:** +- **ALWAYS test WASM compilation** after modifying any WASM platform files +- **USE `uv run ci/wasm_compile.py` for validation** +- **WATCH for unified build conflicts** in compilation output +- **VERIFY async operations work properly** in browser environment + +### WASM Platform File Organization + +**Best Practices for WASM platform files:** +- ✅ **Use `timer.cpp` for canonical timing functions** with `EMSCRIPTEN_KEEPALIVE` +- ✅ **Use `entry_point.cpp` for main() and setup/loop coordination** with async pumping +- ✅ **Use `js.cpp` for JavaScript utility functions** without duplicating timer functions +- ✅ **Include proper async infrastructure** (`fl/async.h`) in entry points +- ✅ **Comment when removing duplicate implementations** to explain unified build conflicts + +## Testing +The project uses a comprehensive test suite including: +- C++ unit tests +- Platform compilation tests +- Code quality checks (ruff, clang-format) +- Example compilation verification + +### Test Assertion Macros +**🚨 CRITICAL: Always use the proper assertion macros for better error messages and debugging:** + +**Equality Assertions:** +- ✅ **CORRECT**: `CHECK_EQ(A, B)` - for equality comparisons +- ❌ **INCORRECT**: `CHECK(A == B)` - provides poor error messages + +**Inequality Assertions:** +- ✅ **CORRECT**: `CHECK_LT(A, B)` - for less than comparisons +- ✅ **CORRECT**: `CHECK_LE(A, B)` - for less than or equal comparisons +- ✅ **CORRECT**: `CHECK_GT(A, B)` - for greater than comparisons +- ✅ **CORRECT**: `CHECK_GE(A, B)` - for greater than or equal comparisons +- ❌ **INCORRECT**: `CHECK(A < B)`, `CHECK(A <= B)`, `CHECK(A > B)`, `CHECK(A >= B)` + +**Boolean Assertions:** +- ✅ **CORRECT**: `CHECK_TRUE(condition)` - for true conditions +- ✅ **CORRECT**: `CHECK_FALSE(condition)` - for false conditions +- ❌ **INCORRECT**: `CHECK(condition)` - for boolean checks + +**String Assertions:** +- ✅ **CORRECT**: `CHECK_STREQ(str1, str2)` - for string equality +- ✅ **CORRECT**: `CHECK_STRNE(str1, str2)` - for string inequality +- ❌ **INCORRECT**: `CHECK(str1 == str2)` - for string comparisons + +**Floating Point Assertions:** +- ✅ **CORRECT**: `CHECK_DOUBLE_EQ(a, b)` - for floating point equality +- ✅ **CORRECT**: `CHECK_DOUBLE_NE(a, b)` - for floating point inequality +- ❌ **INCORRECT**: `CHECK(a == b)` - for floating point comparisons + +**Examples:** +```cpp +// Good assertion usage +CHECK_EQ(expected_value, actual_value); +CHECK_LT(current_index, max_index); +CHECK_GT(temperature, 0.0); +CHECK_TRUE(is_initialized); +CHECK_FALSE(has_error); +CHECK_STREQ("expected", actual_string); +CHECK_DOUBLE_EQ(3.14159, pi_value, 0.001); + +// Bad assertion usage +CHECK(expected_value == actual_value); // Poor error messages +CHECK(current_index < max_index); // Poor error messages +CHECK(is_initialized); // Unclear intent +CHECK("expected" == actual_string); // Wrong comparison type +``` + +**Why:** Using the proper assertion macros provides: +- **Better error messages** with actual vs expected values +- **Clearer intent** about what is being tested +- **Consistent debugging** across all test failures +- **Type safety** for different comparison types + +### Test File Creation Guidelines +**🚨 CRITICAL: Minimize test file proliferation - Consolidate tests whenever possible** + +**PREFERRED APPROACH:** +- ✅ **CONSOLIDATE:** Add new test cases to existing related test files +- ✅ **EXTEND:** Expand existing `TEST_CASE()` blocks with additional scenarios +- ✅ **REUSE:** Leverage existing test infrastructure and helper functions +- ✅ **COMPREHENSIVE:** Create single comprehensive test files that cover entire feature areas + +**CREATE NEW TEST FILES ONLY WHEN:** +- ✅ **Testing completely new subsystems** with no existing related tests +- ✅ **Isolated feature areas** that don't fit logically into existing test structure +- ✅ **Complex integration tests** that require dedicated setup/teardown + +**AVOID:** +- ❌ **Creating new test files for minor bug fixes** - add to existing tests +- ❌ **One test case per file** - consolidate related functionality +- ❌ **Duplicate test patterns** across multiple files +- ❌ **Scattered feature testing** - keep related tests together + +**EXAMPLES:** + +**✅ GOOD - Consolidation:** +```cpp +// Add to existing tests/test_json_comprehensive.cpp +TEST_CASE("JSON - New Feature Addition") { + // Add new functionality tests to existing comprehensive file +} +``` + +**❌ BAD - Proliferation:** +```cpp +// Don't create tests/test_json_new_feature.cpp for minor additions +// Instead add to existing comprehensive test file +``` + +**DEVELOPMENT WORKFLOW:** +1. **During Development/Bug Fixing:** Temporary test files are acceptable for rapid iteration +2. **Near Completion:** **MANDATORY** - Consolidate temporary tests into existing files +3. **Final Review:** Remove temporary test files and ensure comprehensive coverage in main test files + +**CONSOLIDATION CHECKLIST:** +- [ ] Can this test be added to an existing `TEST_CASE` in the same file? +- [ ] Does an existing test file cover the same functional area? +- [ ] Would this test fit better as a sub-section of a comprehensive test? +- [ ] Are there duplicate test patterns that can be eliminated? + +**Why:** Maintaining a clean, consolidated test suite: +- **Easier maintenance** - fewer files to manage and update +- **Better organization** - related functionality tested together +- **Faster builds** - fewer compilation units +- **Cleaner repository** - less file clutter +- **Improved discoverability** - easier to find existing test coverage + +### Test Execution Format +**🚨 CRITICAL: Always use the correct test execution format:** +- ✅ **CORRECT**: `bash test ` (e.g., `bash test audio_json_parsing`) +- ❌ **INCORRECT**: `./.build/bin/test_.exe` +- ❌ **INCORRECT**: Running executables directly + +**Examples:** +- `bash test` - Run all tests (includes debug symbols) +- `bash test audio_json_parsing` - Run specific test +- `bash test xypath` - Run test_xypath.cpp +- `bash compile uno --examples Blink` - Compile examples + +**Quick Build Options:** +- `bash test --quick --cpp` - Quick C++ tests only (when no *.py changes) +- `bash test --quick` - Quick tests including Python (when *.py changes) + +**Why:** The `bash test` wrapper handles platform differences, environment setup, and proper test execution across all supported systems. + +Use `bash test` as specified in user rules for running unit tests. For compilation tests, use `bash compile --examples ` (e.g., `bash compile uno --examples Blink`). + +**🚨 FOR BACKGROUND AGENTS:** Running `bash test` is MANDATORY before indicating completion. Use the MCP server `validate_completion` tool to ensure all tests pass before completing any task. + +## Debugging and Stack Traces + +### Stack Trace Setup +The FastLED project supports enhanced debugging through stack trace functionality for crash analysis and debugging. + +**For Background Agents**: Use the MCP server tool `setup_stack_traces` to automatically install and configure stack trace support: + +```bash +# Via MCP server (recommended for background agents) +uv run mcp_server.py +# Then use setup_stack_traces tool with method: "auto" +``` + +**Manual Installation**: + +**Ubuntu/Debian**: +```bash +sudo apt-get update +sudo apt-get install -y libunwind-dev build-essential +``` + +**CentOS/RHEL/Fedora**: +```bash +sudo yum install -y libunwind-devel gcc-c++ # CentOS/RHEL +sudo dnf install -y libunwind-devel gcc-c++ # Fedora +``` + +**macOS**: +```bash +brew install libunwind +``` + +### Available Stack Trace Methods +1. **LibUnwind** (Recommended) - Enhanced stack traces with symbol resolution +2. **Execinfo** (Fallback) - Basic stack traces using standard glibc +3. **Windows** (On Windows) - Windows-specific debugging APIs +4. **No-op** (Last resort) - Minimal crash handling + +The build system automatically detects and configures the best available option. + +### Testing Stack Traces +```bash +# Note: Stack trace testing now uses Python build system +# CMake commands are deprecated +cd tests +cmake . && make crash_test_standalone crash_test_execinfo # DEPRECATED + +# Test libunwind version +./.build/bin/crash_test_standalone manual # Manual stack trace +./.build/bin/crash_test_standalone nullptr # Crash test + +# Test execinfo version +./.build/bin/crash_test_execinfo manual # Manual stack trace +./.build/bin/crash_test_execinfo nullptr # Crash test +``` + +### Using in Code +```cpp +#include "tests/crash_handler.h" + +int main() { + setup_crash_handler(); // Enable crash handling + // Your code here... + return 0; +} +``` + +**For Background Agents**: Always run the `setup_stack_traces` MCP tool when setting up a new environment to ensure proper debugging capabilities are available. + +## Platform Build Information Analysis + +The FastLED project includes comprehensive tools for analyzing platform-specific build information, including preprocessor defines, compiler flags, and toolchain paths from `build_info.json` files generated during compilation. + +### Overview + +Platform build information is stored in `.build/{platform}/build_info.json` files that are automatically generated when compiling for any platform. These files contain: + +- **Platform Defines** - Preprocessor definitions (`#define` values) specific to each platform +- **Compiler Information** - Paths, flags, and types for C/C++ compilers +- **Toolchain Aliases** - Tool paths for gcc, g++, ar, objcopy, nm, etc. +- **Build Configuration** - Framework, build type, and other settings + +### Quick Start + +#### 1. Generate Build Information + +Before analyzing, ensure you have compiled the platform: + +```bash +# Compile a platform to generate build_info.json +uv run ci/ci-compile.py uno --examples Blink +uv run ci/ci-compile.py esp32dev --examples Blink +``` + +This creates `.build/{platform}/build_info.json` with all platform information. + +#### 2. Analyze Platform Defines + +**Get platform-specific preprocessor defines:** +```bash +# Command line tool +python3 ci/ci/build_info_analyzer.py --board uno --show-defines + +# Via MCP server +uv run mcp_server.py +# Use build_info_analysis tool with: board="uno", show_defines=true +``` + +**Example output for UNO:** +``` +📋 Platform Defines for UNO: + PLATFORMIO=60118 + ARDUINO_AVR_UNO + F_CPU=16000000L + ARDUINO_ARCH_AVR + ARDUINO=10808 + __AVR_ATmega328P__ +``` + +**Example output for ESP32:** +``` +📋 Platform Defines for ESP32DEV: + ESP32=ESP32 + ESP_PLATFORM + F_CPU=240000000L + ARDUINO_ARCH_ESP32 + IDF_VER="v5.3.2-174-g083aad99cf-dirty" + ARDUINO_ESP32_DEV + # ... and 23 more defines +``` + +#### 3. Compare Platforms + +**Compare defines between platforms:** +```bash +# Command line +python3 ci/ci/build_info_analyzer.py --compare uno esp32dev + +# Via MCP +# Use build_info_analysis tool with: board="uno", compare_with="esp32dev" +``` + +Shows differences and commonalities between platform defines. + +### Available Tools + +#### Command Line Tool + +**Basic Usage:** +```bash +# List available platforms +python3 ci/ci/build_info_analyzer.py --list-boards + +# Show platform defines +python3 ci/ci/build_info_analyzer.py --board uno --show-defines + +# Show compiler information +python3 ci/ci/build_info_analyzer.py --board esp32dev --show-compiler + +# Show toolchain aliases +python3 ci/ci/build_info_analyzer.py --board teensy31 --show-toolchain + +# Show everything +python3 ci/ci/build_info_analyzer.py --board digix --show-all + +# Compare platforms +python3 ci/ci/build_info_analyzer.py --compare uno esp32dev + +# JSON output for automation +python3 ci/ci/build_info_analyzer.py --board uno --show-defines --json +``` + +#### MCP Server Tool + +**For Background Agents**, use the MCP server `build_info_analysis` tool: + +```bash +# Start MCP server +uv run mcp_server.py + +# Use build_info_analysis tool with parameters: +# - board: "uno", "esp32dev", "teensy31", etc. or "list" to see available +# - show_defines: true/false (default: true) +# - show_compiler: true/false +# - show_toolchain: true/false +# - show_all: true/false +# - compare_with: "other_board_name" for comparison +# - output_json: true/false for programmatic use +``` + +### Supported Platforms + +The build info analysis works with **ANY platform** that generates a `build_info.json` file: + +**Embedded Platforms:** +- ✅ **UNO (AVR)** - 8-bit microcontroller (6 defines) +- ✅ **ESP32DEV** - WiFi-enabled 32-bit platform (29 defines) +- ✅ **ESP32S3, ESP32C3, ESP32C6** - All ESP32 variants +- ✅ **TEENSY31, TEENSYLC** - ARM Cortex-M platforms +- ✅ **DIGIX, BLUEPILL** - ARM Cortex-M3 platforms +- ✅ **STM32, NRF52** - Various ARM platforms +- ✅ **RPIPICO, RPIPICO2** - Raspberry Pi Pico platforms +- ✅ **ATTINY85, ATTINY1616** - Small AVR microcontrollers + +### Use Cases + +#### For Code Development + +**Understanding Platform Differences:** +```bash +# See what defines are available for conditional compilation +python3 ci/ci/build_info_analyzer.py --board esp32dev --show-defines + +# Compare two platforms to understand differences +python3 ci/ci/build_info_analyzer.py --compare uno esp32dev +``` + +**Compiler and Toolchain Information:** +```bash +# Get compiler paths and flags for debugging builds +python3 ci/ci/build_info_analyzer.py --board teensy31 --show-compiler + +# Get toolchain paths for symbol analysis +python3 ci/ci/build_info_analyzer.py --board digix --show-toolchain +``` + +#### For Automation + +**JSON output for scripts:** +```bash +# Get defines as JSON for processing +python3 ci/ci/build_info_analyzer.py --board uno --show-defines --json + +# Get all build info as JSON +python3 ci/ci/build_info_analyzer.py --board esp32dev --show-all --json +``` + +### Integration with Other Tools + +Build info analysis integrates with other FastLED tools: + +1. **Symbol Analysis** - Uses build_info.json to find toolchain paths +2. **Compilation** - Generated automatically during example compilation +3. **Testing** - Provides platform context for test results + +### Troubleshooting + +**Common Issues:** + +1. **"No boards with build_info.json found"** + - **Solution:** Compile a platform first: `uv run ci/ci-compile.py {board} --examples Blink` + +2. **"Board key not found in build_info.json"** + - **Solution:** Check available boards: `python3 ci/ci/build_info_analyzer.py --list-boards` + +3. **"Build info not found for platform"** + - **Solution:** Ensure the platform compiled successfully and check `.build/{board}/build_info.json` exists + +### Best Practices + +1. **Always compile first** before analyzing build information +2. **Use comparison feature** to understand platform differences +3. **Check defines** when adding platform-specific code +4. **Use JSON output** for automated processing and CI/CD +5. **Combine with symbol analysis** for complete platform understanding + +**For Background Agents:** Use the MCP server `build_info_analysis` tool for consistent access to platform build information and proper error handling. + +## Symbol Analysis for Binary Optimization + +The FastLED project includes comprehensive symbol analysis tools to identify optimization opportunities and understand binary size allocation across all supported platforms. + +### Overview + +Symbol analysis examines compiled ELF files to: +- **Identify large symbols** that may be optimization targets +- **Understand memory allocation** across different code sections +- **Find unused features** that can be eliminated to reduce binary size +- **Compare symbol sizes** between different builds or platforms +- **Provide optimization recommendations** based on actual usage patterns + +### Supported Platforms + +The symbol analysis tools work with **ANY platform** that generates a `build_info.json` file, including: + +**Embedded Platforms:** +- ✅ **UNO (AVR)** - Small 8-bit microcontroller (typically ~3-4KB symbols) +- ✅ **ESP32DEV (Xtensa)** - WiFi-enabled 32-bit platform (typically ~200-300KB symbols) +- ✅ **ESP32S3, ESP32C3, ESP32C6, etc.** - All ESP32 variants supported + +**ARM Platforms:** +- ✅ **TEENSY31 (ARM Cortex-M4)** - High-performance 32-bit (typically ~10-15KB symbols) +- ✅ **TEENSYLC (ARM Cortex-M0+)** - Low-power ARM platform (typically ~8-10KB symbols) +- ✅ **DIGIX (ARM Cortex-M3)** - Arduino Due compatible (typically ~15-20KB symbols) +- ✅ **STM32 (ARM Cortex-M3)** - STMicroelectronics platform (typically ~12-18KB symbols) + +**And many more!** Any platform with toolchain support and `build_info.json` generation. + +### Quick Start + +#### 1. Prerequisites + +Before running symbol analysis, ensure you have compiled the platform: + +```bash +# Compile platform first (required step) +uv run ci/ci-compile.py uno --examples Blink +uv run ci/ci-compile.py esp32dev --examples Blink +uv run ci/ci-compile.py teensy31 --examples Blink +``` + +This generates the required `.build/{platform}/build_info.json` file and ELF binary. + +#### 2. Run Symbol Analysis + +**Analyze specific platform:** +```bash +uv run ci/ci/symbol_analysis.py --board uno +uv run ci/ci/symbol_analysis.py --board esp32dev +uv run ci/ci/symbol_analysis.py --board teensy31 +``` + +**Analyze all available platforms at once:** +```bash +uv run ci/demo_symbol_analysis.py +``` + +**Using MCP Server (Recommended for Background Agents):** +```bash +# Use MCP server tools +uv run mcp_server.py +# Then use symbol_analysis tool with board: "uno" or "auto" +# Or use symbol_analysis tool with run_all_platforms: true +``` + +### Analysis Output + +Symbol analysis provides detailed reports including: + +**Summary Information:** +- **Total symbols count** and **total size** +- **Symbol type breakdown** (text, data, bss, etc.) +- **Platform-specific details** (toolchain, ELF file location) + +**Largest Symbols List:** +- **Top symbols by size** for optimization targeting +- **Demangled C++ names** for easy identification +- **Size in bytes** and **percentage of total** + +**Optimization Recommendations:** +- **Feature analysis** - unused features that can be disabled +- **Size impact estimates** - potential savings from removing features +- **Platform-specific suggestions** based on symbol patterns + +### Example Analysis Results + +**UNO Platform (Small embedded):** +``` +================================================================================ +UNO SYMBOL ANALYSIS REPORT +================================================================================ + +SUMMARY: + Total symbols: 51 + Total symbol size: 3767 bytes (3.7 KB) + +LARGEST SYMBOLS (sorted by size): + 1. 1204 bytes - ClocklessController<...>::showPixels(...) + 2. 572 bytes - CFastLED::show(unsigned char) + 3. 460 bytes - main + 4. 204 bytes - CPixelLEDController<...>::show(...) +``` + +**ESP32 Platform (Feature-rich):** +``` +================================================================================ +ESP32DEV SYMBOL ANALYSIS REPORT +================================================================================ + +SUMMARY: + Total symbols: 2503 + Total symbol size: 237092 bytes (231.5 KB) + +LARGEST SYMBOLS (sorted by size): + 1. 12009 bytes - _vfprintf_r + 2. 11813 bytes - _svfprintf_r + 3. 8010 bytes - _vfiprintf_r + 4. 4192 bytes - port_IntStack +``` + +### Advanced Features + +#### JSON Output +Save detailed analysis results for programmatic processing: +```bash +uv run symbol_analysis.py --board esp32dev --output-json +# Results saved to: .build/esp32dev_symbol_analysis.json +``` + +#### Batch Analysis +Analyze multiple platforms in sequence: +```bash +for board in uno esp32dev teensy31; do + uv run symbol_analysis.py --board $board +done +``` + +#### Integration with Build Systems +The symbol analysis can be integrated into automated build processes: +```python +# In your Python build script +import subprocess +result = subprocess.run(['uv', 'run', 'symbol_analysis.py', '--board', 'uno'], + capture_output=True, text=True) +``` + +### MCP Server Tools + +**For Background Agents**, use the MCP server tools for symbol analysis: + +1. **Generic Symbol Analysis** (`symbol_analysis` tool): + - Works with **any platform** (UNO, ESP32, Teensy, STM32, etc.) + - Auto-detects available platforms from `.build/` directory + - Can analyze single platform or all platforms simultaneously + - Provides comprehensive usage instructions + +2. **ESP32-Specific Analysis** (`esp32_symbol_analysis` tool): + - **ESP32 platforms only** with FastLED-focused filtering + - Includes feature analysis and optimization recommendations + - FastLED-specific symbol identification and grouping + +**Usage via MCP:** +```bash +# Start MCP server +uv run mcp_server.py + +# Use symbol_analysis tool with parameters: +# - board: "uno", "esp32dev", or "auto" +# - run_all_platforms: true/false +# - output_json: true/false +``` + +### Troubleshooting + +**Common Issues:** + +1. **"build_info.json not found"** + - **Solution:** Compile the platform first: `uv run ci/ci-compile.py {board} --examples Blink` + +2. **"ELF file not found"** + - **Solution:** Ensure compilation completed successfully and check `.build/{board}/` directory + +3. **"Tool not found"** (nm, c++filt, etc.) + - **Solution:** The platform toolchain isn't installed or configured properly + - **Check:** Verify PlatformIO platform installation: `uv run pio platform list` + +4. **"No symbols found"** + - **Solution:** The ELF file may be stripped or compilation failed + - **Check:** Verify ELF file exists and has debug symbols + +**Debug Mode:** +```bash +# Run with verbose Python output for debugging +uv run python -v ci/ci/symbol_analysis.py --board uno +``` + +### Best Practices + +1. **Always compile first** before running symbol analysis +2. **Use consistent examples** (like Blink) for size comparisons +3. **Run analysis on clean builds** to avoid cached/incremental build artifacts +4. **Compare results across platforms** to understand feature scaling +5. **Focus on the largest symbols first** for maximum optimization impact +6. **Use JSON output for automated processing** and trend analysis + +**For Background Agents:** Always use the MCP server `symbol_analysis` tool for consistent results and proper error handling. diff --git a/v1.5/lib/FastLED/LAYOUT.md b/v1.5/lib/FastLED/LAYOUT.md new file mode 100644 index 0000000..d891042 --- /dev/null +++ b/v1.5/lib/FastLED/LAYOUT.md @@ -0,0 +1,414 @@ +# FastLED UI Layout Management System Design + +## Overview + +This document outlines the design for a comprehensive, responsive UI layout management system for the FastLED WebAssembly compiler interface. The system provides dynamic layout calculation, state management, and container coordination to ensure optimal UI presentation across different screen sizes and orientations. + +## Architecture + +### Core Components + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ UILayoutPlacementManager │ +│ (Main Orchestrator) │ +├─────────────────────────────────────────────────────────────────┤ +│ • Coordinates all layout operations │ +│ • Applies CSS styling and grid layouts │ +│ • Handles layout mode transitions │ +│ • Manages container visibility │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ +│ LayoutStateManager │ │ ResizeCoordinator│ │ ColumnValidator │ +│ (State & Logic) │ │ (Event Handling) │ │ (Optimization) │ +├─────────────────────┤ ├─────────────────┤ ├─────────────────────┤ +│ • Viewport tracking │ │ • Resize events │ │ • Layout validation │ +│ • Layout calculation│ │ • Debouncing │ │ • Performance check │ +│ • State transitions │ │ • Event dispatch │ │ • Efficiency metrics│ +│ • Container states │ │ • Coordination │ │ • Optimization hints│ +└─────────────────────┘ └─────────────────┘ └─────────────────────┘ +``` + +## Component Specifications + +### 1. UILayoutPlacementManager + +**Purpose**: Main orchestrator for all layout operations +**Location**: `src/platforms/wasm/compiler/modules/ui_layout_placement_manager.js` + +**Key Responsibilities:** +- Apply layout styles to DOM elements +- Coordinate between state manager and UI components +- Handle layout mode transitions (mobile, tablet, desktop, ultrawide) +- Manage container visibility and grid configurations + +**Public API:** +```javascript +class UILayoutPlacementManager { + constructor() + applyLayout() + forceLayoutUpdate() + validateAndOptimizeLayout() + getLayoutInfo() + updateContentMetrics(totalGroups, totalElements) +} +``` + +### 2. LayoutStateManager + +**Purpose**: Unified state management for layout calculations +**Location**: `src/platforms/wasm/compiler/modules/layout_state_manager.js` + +**Key Features:** +- Centralized layout state storage +- Viewport and breakpoint detection +- Canvas and UI column calculations +- Container state tracking +- Atomic state updates with change events + +**State Schema:** +```javascript +{ + mode: 'mobile' | 'tablet' | 'desktop' | 'ultrawide', + viewportWidth: number, + availableWidth: number, + canvasSize: number, + uiColumns: number, + uiColumnWidth: number, + uiTotalWidth: number, + canExpand: boolean, + container2Visible: boolean, + totalGroups: number, + totalElements: number +} +``` + +### 3. ResizeCoordinator + +**Purpose**: Coordinated resize event handling +**Location**: `src/platforms/wasm/compiler/modules/resize_coordinator.js` + +**Features:** +- Debounced resize event handling +- Cross-component resize coordination +- Performance-optimized event dispatch +- Race condition prevention + +### 4. ColumnValidator + +**Purpose**: Layout optimization and validation +**Location**: `src/platforms/wasm/compiler/modules/column_validator.js` + +**Capabilities:** +- Layout efficiency analysis +- Column width optimization +- Content density validation +- Performance recommendations + +## Layout Modes + +### Mobile (≤ 768px) +- **Layout**: Single column (1×N grid) +- **Canvas**: Full width, centered +- **UI**: Stacked below canvas +- **Containers**: Only primary UI container visible + +### Tablet (769px - 1199px) +- **Layout**: Two columns (2×N grid) +- **Canvas**: Left side, fixed width +- **UI**: Right side, flexible width +- **Containers**: Primary UI container only + +### Desktop (1200px - 1599px) +- **Layout**: Two columns (2×N grid) +- **Canvas**: Left side, larger fixed width +- **UI**: Right side, flexible width +- **Containers**: Primary UI container only + +### Ultrawide (≥ 1600px) +- **Layout**: Three columns (3×N grid) +- **Canvas**: Center, fixed width +- **UI**: Left and right sides, flexible widths +- **Containers**: Both primary and secondary UI containers + +## Configuration + +### Layout Constants +```javascript +{ + minCanvasSize: 320, + maxCanvasSize: 800, + minUIColumnWidth: 280, + maxUIColumnWidth: 400, + horizontalGap: 40, + verticalGap: 20, + containerPadding: 40, + maxUIColumns: 3, + preferredUIColumnWidth: 320, + canvasExpansionRatio: 0.6, + minContentRatio: 0.4 +} +``` + +### Breakpoints +```javascript +{ + mobile: { max: 768 }, + tablet: { min: 769, max: 1199 }, + desktop: { min: 1200, max: 1599 }, + ultrawide: { min: 1600 } +} +``` + +## Implementation Todos + +### Phase 1: Core Infrastructure ✅ +- [x] Create LayoutStateManager with unified state handling +- [x] Implement ResizeCoordinator for event management +- [x] Build ColumnValidator for layout optimization +- [x] Refactor UILayoutPlacementManager to use new components + +### Phase 2: State Management ✅ +- [x] Implement atomic state updates with change events +- [x] Add container state tracking and visibility management +- [x] Create viewport detection and breakpoint handling +- [x] Add content metrics tracking (groups, elements) + +### Phase 3: Layout Calculation ✅ +- [x] Implement responsive canvas sizing algorithms +- [x] Add dynamic UI column width calculation +- [x] Create layout mode transition logic +- [x] Add grid template generation for CSS + +### Phase 4: Event Coordination ✅ +- [x] Implement debounced resize handling +- [x] Add cross-component event coordination +- [x] Create performance-optimized event dispatch +- [x] Add race condition prevention + +### Phase 5: Optimization & Validation ✅ +- [x] Create layout efficiency analysis +- [x] Implement column width optimization +- [x] Add content density validation +- [x] Create performance monitoring and recommendations + +### Phase 6: Integration & Polish ✅ +- [x] Integrate with existing UI manager +- [x] Add backward compatibility for legacy APIs +- [x] Implement proper error handling and fallbacks +- [x] Add comprehensive logging and debugging support + +### Phase 7: Testing & Documentation +- [ ] Create comprehensive unit tests for all components +- [ ] Add integration tests for layout scenarios +- [ ] Create visual regression tests for different screen sizes +- [ ] Add performance benchmarks and optimization tests +- [ ] Document API usage and configuration options + +### Phase 8: Advanced Features +- [ ] Add animation support for layout transitions +- [ ] Implement custom breakpoint configuration +- [ ] Add layout templates and presets +- [ ] Create advanced grid layout options +- [ ] Add accessibility features and ARIA support + +### Phase 9: Performance Optimization +- [ ] Implement layout caching for repeated calculations +- [ ] Add virtual scrolling for large UI element lists +- [ ] Optimize DOM manipulation and CSS application +- [ ] Add lazy loading for non-visible UI components +- [ ] Implement progressive enhancement for slower devices + +### Phase 10: Extensibility +- [ ] Create plugin system for custom layout algorithms +- [ ] Add theme and styling customization APIs +- [ ] Implement layout export/import functionality +- [ ] Create developer tools for layout debugging +- [ ] Add real-time layout editing capabilities + +## Benefits + +### Performance +- **Debounced Events**: Prevents excessive layout recalculations +- **Atomic Updates**: Eliminates partial state inconsistencies +- **Efficient DOM**: Minimizes CSS recalculations and reflows +- **Optimized Calculations**: Smart caching and memoization + +### Maintainability +- **Separation of Concerns**: Each component has a single responsibility +- **Unified State**: Single source of truth for layout information +- **Modular Design**: Components can be tested and modified independently +- **Clear APIs**: Well-defined interfaces between components + +### User Experience +- **Responsive Design**: Seamless adaptation to any screen size +- **Smooth Transitions**: Coordinated layout changes without flicker +- **Optimal Layouts**: Intelligent space utilization across devices +- **Fast Rendering**: Performance-optimized for real-time applications + +### Developer Experience +- **Type Safety**: Comprehensive JSDoc annotations for IDE support +- **Debugging Tools**: Built-in logging and state inspection +- **Extensible**: Easy to add new layout modes and features +- **Documentation**: Clear API documentation and usage examples + +## JSON UI Persistent State + +### State Management Architecture +The system now includes comprehensive JSON-based state persistence implemented in commit `86d9b7d5d`. This enables: + +- **Persistent Layout State**: All layout configurations stored as JSON with atomic updates +- **UI Element State Tracking**: Complete state management for UI elements with change events +- **Recording/Playback System**: Full UI interaction recording and replay capabilities +- **Container State Management**: Visibility and configuration state for all UI containers + +### Core State Components + +#### LayoutStateManager +**Location**: `src/platforms/wasm/compiler/modules/layout_state_manager.js:487` + +Provides centralized JSON state management with: +```javascript +{ + mode: 'mobile' | 'tablet' | 'desktop' | 'ultrawide', + viewportWidth: number, + availableWidth: number, + canvasSize: number, + uiColumns: number, + uiColumnWidth: number, + uiTotalWidth: number, + canExpand: boolean, + container2Visible: boolean, + totalGroups: number, + totalElements: number, + containers: { + 'ui-controls': { visible: boolean, columns: number, width: number }, + 'ui-controls-2': { visible: boolean, columns: number, width: number } + } +} +``` + +#### UIRecorder +**Location**: `src/platforms/wasm/compiler/modules/ui_recorder.js:517` + +Records UI state changes as JSON events: +```javascript +{ + recording: { + version: "1.0", + startTime: "ISO8601", + endTime: "ISO8601", + metadata: { recordingId: string, layoutMode: string, totalDuration: number } + }, + events: [ + { + timestamp: number, + type: 'add' | 'update' | 'remove', + elementId: string, + data: { elementType?: string, value?: any, previousValue?: any, elementConfig?: Object } + } + ] +} +``` + +#### UIPlayback +**Location**: `src/platforms/wasm/compiler/modules/ui_playback.js:596` + +Reconstructs UI from JSON state with: +- Event-by-event playback with timing control +- Speed control and pause/resume functionality +- Element state validation and restoration +- Progress tracking and timeline navigation + +### UI Reconstruction Benefits + +Instead of manipulating DOM objects directly, the JSON state system enables: + +1. **State-Driven Reconstruction**: Rebuild entire UI from current JSON state +2. **Atomic Updates**: All changes applied atomically to prevent inconsistencies +3. **Time Travel**: Navigate to any point in UI history via recorded states +4. **State Validation**: Verify UI consistency against recorded state +5. **Reproducible Layouts**: Exact recreation of UI configurations + +### Usage Pattern for Reconstruction + +```javascript +// Get current UI state +const currentState = layoutManager.stateManager.getState(); + +// Reconstruct UI from state instead of DOM manipulation +function reconstructUI(state) { + // Clear existing UI + clearAllContainers(); + + // Rebuild from JSON state + applyLayoutFromState(state); + recreateElementsFromState(state); + restoreContainerVisibility(state); +} + +// Apply state changes +layoutManager.stateManager.updateState(newStateData); +``` + +## Integration Points + +### UI Manager Integration +```javascript +// In JsonUiManager constructor +this.layoutManager = new UILayoutPlacementManager(); + +// Listen for layout changes +this.layoutManager.stateManager.addStateChangeListener((changeEvent) => { + const { state } = changeEvent; + this.onLayoutChange(state.mode); +}); +``` + +### Container Management +```javascript +// Check container visibility before placing elements +const container2State = this.layoutManager.stateManager.getContainerState('ui-controls-2'); +if (container2State && container2State.visible) { + // Use multi-container layout +} else { + // Use single-container layout +} +``` + +### Content Updates +```javascript +// Update content metrics when UI elements change +this.layoutManager.updateContentMetrics(totalGroups, totalElements); + +// Force layout recalculation when needed +this.layoutManager.forceLayoutUpdate(); +``` + +## Future Enhancements + +### Advanced Layout Modes +- **Split-screen**: Side-by-side canvas and code editor +- **Picture-in-picture**: Floating canvas with overlay UI +- **Full-screen**: Immersive canvas mode with minimal UI +- **Multi-monitor**: Extended layout across multiple displays + +### Dynamic Content +- **Adaptive UI**: UI elements that resize based on content +- **Collapsible sections**: Expandable/collapsible UI groups +- **Floating panels**: Draggable and dockable UI components +- **Custom layouts**: User-defined layout configurations + +### Accessibility +- **Screen reader support**: Proper ARIA labels and navigation +- **Keyboard navigation**: Full keyboard accessibility +- **High contrast**: Theme support for visual accessibility +- **Responsive text**: Scalable fonts and UI elements + +--- + +*This design provides a robust foundation for responsive UI layout management in the FastLED WebAssembly compiler, ensuring optimal user experience across all device types and screen sizes.* \ No newline at end of file diff --git a/v1.5/lib/FastLED/LICENSE b/v1.5/lib/FastLED/LICENSE new file mode 100644 index 0000000..ebe4763 --- /dev/null +++ b/v1.5/lib/FastLED/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 FastLED + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/v1.5/lib/FastLED/PORTING.md b/v1.5/lib/FastLED/PORTING.md new file mode 100644 index 0000000..f90ea47 --- /dev/null +++ b/v1.5/lib/FastLED/PORTING.md @@ -0,0 +1,56 @@ +Platform Porting Guide +========================== + +# Fast porting for a new board on existing hardware + +Sometimes "porting" FastLED simply consists of supplying new pin definitions for the given platform. For example, platforms/avr/fastpin_avr.h contains various pin definitions for all the AVR variant chipsets/boards that FastLED supports. Defining a set of pins involves setting up a set of definitions - for example here's one full set from the avr fastpin file: + +``` +#elif defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega644P__) + +_FL_IO(A); _FL_IO(B); _FL_IO(C); _FL_IO(D); + +#define MAX_PIN 31 +_FL_DEFPIN(0, 0, B); _FL_DEFPIN(1, 1, B); _FL_DEFPIN(2, 2, B); _FL_DEFPIN(3, 3, B); +_FL_DEFPIN(4, 4, B); _FL_DEFPIN(5, 5, B); _FL_DEFPIN(6, 6, B); _FL_DEFPIN(7, 7, B); +_FL_DEFPIN(8, 0, D); _FL_DEFPIN(9, 1, D); _FL_DEFPIN(10, 2, D); _FL_DEFPIN(11, 3, D); +_FL_DEFPIN(12, 4, D); _FL_DEFPIN(13, 5, D); _FL_DEFPIN(14, 6, D); _FL_DEFPIN(15, 7, D); +_FL_DEFPIN(16, 0, C); _FL_DEFPIN(17, 1, C); _FL_DEFPIN(18, 2, C); _FL_DEFPIN(19, 3, C); +_FL_DEFPIN(20, 4, C); _FL_DEFPIN(21, 5, C); _FL_DEFPIN(22, 6, C); _FL_DEFPIN(23, 7, C); +_FL_DEFPIN(24, 0, A); _FL_DEFPIN(25, 1, A); _FL_DEFPIN(26, 2, A); _FL_DEFPIN(27, 3, A); +_FL_DEFPIN(28, 4, A); _FL_DEFPIN(29, 5, A); _FL_DEFPIN(30, 6, A); _FL_DEFPIN(31, 7, A); + +#define HAS_HARDWARE_PIN_SUPPORT 1 +``` + +The ```_FL_IO``` macro is used to define the port registers for the platform while the ```_FL_DEFPIN``` macro is used to define pins. The parameters to the macro are the pin number, the bit on the port that represents that pin, and the port identifier itself. On some platforms, like the AVR, ports are identified by letter. On other platforms, like arm, ports are identified by number. + +The ```HAS_HARDWARE_PIN_SUPPORT``` define tells the rest of the FastLED library that there is hardware pin support available. There may be other platform specific defines for things like hardware SPI ports and such. + +## Setting up the basic files/folders + +* Create platform directory (e.g. platforms/arm/kl26) +* Create configuration header led_sysdefs_arm_kl26.h: + * Define platform flags (like FASTLED_ARM/FASTLED_TEENSY) + * Define configuration parameters re: interrupts, or clock doubling + * Include extar system header files if needed +* Create main platform include, fastled_arm_kl26.h + * Include the various other header files as needed +* Modify led_sysdefs.h to conditionally include platform sysdefs header file +* Modify platforms.h to conditionally include platform fastled header + +## Porting fastpin.h + +The heart of the FastLED library is the fast pin access. This is a templated class that provides 1-2 cycle pin access, bypassing digital write and other such things. As such, this will usually be the first bit of the library that you will want to port when moving to a new platform. Once you have FastPIN up and running then you can do some basic work like testing toggles or running bit-bang'd SPI output. + +There's two low level FastPin classes. There's the base FastPIN template class, and then there is FastPinBB which is for bit-banded access on those MCUs that support bitbanding. Note that the bitband class is optional and primarily useful in the implementation of other functionality internal to the platform. This file is also where you would do the pin to port/bit mapping defines. + +Explaining how the macros work and should be used is currently beyond the scope of this document. + +## Porting fastspi.h + +This is where you define the low level interface to the hardware SPI system (including a writePixels method that does a bunch of housekeeping for writing led data). Use the fastspi_nop.h file as a reference for the methods that need to be implemented. There are ofteh other useful methods that can help with the internals of the SPI code, I recommend taking a look at how the various platforms implement their SPI classes. + +## Porting clockless.h + +This is where you define the code for the clockless controllers. Across ARM platforms this will usually be fairly similar - though different arm platforms will have different clock sources that you can/should use. diff --git a/v1.5/lib/FastLED/QWEN.md b/v1.5/lib/FastLED/QWEN.md new file mode 100644 index 0000000..a3604fd --- /dev/null +++ b/v1.5/lib/FastLED/QWEN.md @@ -0,0 +1,1744 @@ +# FastLED Project Rules for Cursor + +## Cursor Configuration + +### Post-Change Hooks +Run linting after every code change: +```yaml +post_change_hooks: + - command: "bash lint" + description: "Run code formatting and linting" + working_directory: "." +``` + +## MCP Server Configuration +This project includes a custom MCP server (`mcp_server.py`) that provides tools for: +- Running tests with various options +- Compiling examples for different platforms +- Code fingerprinting and change detection +- Linting and formatting +- Project information and status +- **Build info analysis for platform-specific defines, compiler flags, and toolchain information** +- **Symbol analysis for binary optimization (all platforms)** +- Stack trace setup for enhanced debugging +- **🌐 FastLED Web Compiler with Playwright (FOREGROUND AGENTS ONLY)** +- **🚨 CRITICAL: `validate_completion` tool for background agents** + +To use the MCP server, run: `uv run mcp_server.py` + +**BACKGROUND AGENTS:** The MCP server includes a mandatory `validate_completion` tool that MUST be used before indicating task completion. This tool runs `bash test` and ensures all tests pass. + +### FastLED Web Compiler (FOREGROUND AGENTS ONLY) + +**🌐 FOR INTERACTIVE DEVELOPMENT:** The MCP server includes a `run_fastled_web_compiler` tool that: + +**Note:** For direct command-line WASM compilation, see the **WASM Sketch Compilation** section below. + +- **Compiles Arduino sketches to WASM** for browser execution +- **Captures console.log output** with playwright automation +- **Takes screenshots** of running visualizations +- **Monitors FastLED_onFrame calls** to verify proper initialization +- **Provides detailed analysis** of errors and performance + +**PREREQUISITES:** +- `pip install fastled` - FastLED web compiler +- `pip install playwright` - Browser automation (included in pyproject.toml) +- Docker (optional, for faster compilation) + +**USAGE EXAMPLES:** +```python +# Via MCP Server - Basic usage +use run_fastled_web_compiler tool with: +- example_path: "examples/Audio" +- capture_duration: 30 +- headless: false +- save_screenshot: true + +# Via MCP Server - Different examples +use run_fastled_web_compiler tool with: +- example_path: "examples/Blink" +- capture_duration: 15 +- headless: true + +# Via MCP Server - Quick test +use run_fastled_web_compiler tool with: +- example_path: "examples/wasm" +- capture_duration: 10 +``` + +**KEY FEATURES:** +- **Automatic browser installation:** Installs Chromium via playwright +- **Console.log capture:** Records all browser console output with timestamps +- **Error detection:** Identifies compilation failures and runtime errors +- **FastLED monitoring:** Tracks `FastLED_onFrame` calls to verify functionality +- **Screenshot capture:** Saves visualization images with timestamps +- **Docker detection:** Checks for Docker availability for faster builds +- **Background agent protection:** Automatically disabled for CI/background environments + +**🚫 BACKGROUND AGENT RESTRICTION:** +This tool is **COMPLETELY DISABLED** for background agents and CI environments. Background agents attempting to use this tool will receive an error message. This is intentional to prevent: +- Hanging processes in automated environments +- Resource conflicts in CI/CD pipelines +- Interactive browser windows in headless environments + +**CONSOLE.LOG CAPTURE PATTERN:** +The tool follows the pattern established in `ci/wasm_test.py` and `ci/ci/scrapers/`: +```javascript +// Example captured console.log patterns: +[14:25:30] log: FastLED_onFrame called: {"frame":1,"leds":100} +[14:25:30] log: Audio worklet initialized +[14:25:31] error: Missing audio_worklet_processor.js +[14:25:31] warning: WebGL context lost +``` + +**INTEGRATION WITH EXISTING CI:** +- Complements existing `ci/wasm_test.py` functionality +- Uses same playwright patterns as `ci/ci/scrapers/` +- Leverages existing pyproject.toml dependencies +- Compatible with existing Docker-based compilation workflow + +## Project Structure +- `src/` - Main FastLED library source code +- `examples/` - Arduino examples demonstrating FastLED usage +- `tests/` - Test files and infrastructure +- `ci/` - Continuous integration scripts +- `docs/` - Documentation + +## Key Commands +- `uv run test.py` - Run all tests +- `uv run test.py --cpp` - Run C++ tests only +- `uv run test.py TestName` - Run specific C++ test + - For example: running test_xypath.cpp would be uv run test.py xypath +- `./lint` - Run code formatting/linting +- `uv run ci/ci-compile.py uno --examples Blink` - Compile examples for specific platform + - For example (uno): `uv run ci/ci-compile.py uno --examples Blink` + - For example (esp32dev): `uv run ci/ci-compile.py esp32dev --examples Blink` + - For example (esp8266): `uv run ci/ci-compile.py esp01 --examples Blink` + - For example (teensy31): `uv run ci/ci-compile.py teensy31 --examples Blink` +- **WASM Compilation** - Compile Arduino sketches to run in web browsers: + - `uv run ci/wasm_compile.py examples/Blink -b --open` - Compile Blink to WASM and open browser + - `uv run ci/wasm_compile.py path/to/your/sketch -b --open` - Compile any sketch to WASM +- **Symbol Analysis** - Analyze binary size and optimization opportunities: + - `uv run ci/ci/symbol_analysis.py --board uno` - Analyze UNO platform + - `uv run ci/ci/symbol_analysis.py --board esp32dev` - Analyze ESP32 platform + - `uv run ci/demo_symbol_analysis.py` - Analyze all available platforms + +## 🤖 AI AGENT LINTING GUIDELINES + +### FOREGROUND AGENTS (Interactive Development) +**🚨 ALWAYS USE `bash lint` - DO NOT RUN INDIVIDUAL LINTING SCRIPTS** + +- **✅ CORRECT:** `bash lint` +- **❌ FORBIDDEN:** `./lint-js`, `./check-js`, `python3 scripts/enhance-js-typing.py` +- **❌ FORBIDDEN:** `uv run ruff check`, `uv run black`, individual tools + +**WHY:** `bash lint` provides: +- **Comprehensive coverage** - Python, C++, JavaScript, and enhancement analysis +- **Consistent workflow** - Single command for all linting needs +- **Proper sequencing** - Runs tools in the correct order with dependencies +- **Clear output** - Organized sections showing what's being checked +- **Agent guidance** - Shows proper usage for AI agents + +### BACKGROUND AGENTS (Automated/CI Environments) +**CAN USE FINE-GRAINED LINTING FOR SPECIFIC NEEDS** + +Background agents may use individual linting scripts when needed: +- `./lint-js` - JavaScript-only linting +- `./check-js` - JavaScript type checking +- `python3 scripts/enhance-js-typing.py` - JavaScript enhancement analysis +- `uv run ruff check` - Python linting only +- MCP server `lint_code` tool - Programmatic access + +**BUT STILL PREFER `bash lint` FOR COMPREHENSIVE CHECKING** + +### Linting Script Integration + +The `bash lint` command now includes: +1. **📝 Python Linting** - ruff, black, isort, pyright +2. **🔧 C++ Linting** - clang-format (when enabled) +3. **🌐 JavaScript Linting** - Deno lint, format check, type checking +4. **🔍 JavaScript Enhancement** - Analysis and recommendations +5. **💡 AI Agent Guidance** - Clear instructions for proper usage + +### When to Use Individual Scripts + +**FOREGROUND AGENTS:** Never. Always use `bash lint`. + +**BACKGROUND AGENTS:** Only when: +- **Debugging specific issues** with one language/tool +- **Testing incremental changes** to linting configuration +- **Running targeted analysis** for specific files +- **Integrating with automated workflows** via MCP server + +## Development Guidelines +- Follow existing code style and patterns +- Run tests before committing changes +- Use the MCP server tools for common tasks +- Check examples when making API changes + +## 🚨 CRITICAL: .INO FILE CREATION RULES 🚨 + +### ⚠️ THINK BEFORE CREATING .INO FILES ⚠️ + +**.ino files should be created SPARINGLY and ONLY when truly justified.** + +### 🚫 WHEN NOT TO CREATE .INO FILES: +- **Testing minor code changes** - Use existing test files or unit tests +- **Quick API validation** - Use unit tests or modify existing examples +- **Debugging specific functions** - Use test files, not new sketches +- **One-off experiments** - Create temporary test files instead +- **Small feature tests** - Extend existing relevant examples + +### ✅ WHEN TO CREATE .INO FILES: + +#### 1. **Temporary Testing (.ino)** +**Use Pattern:** `temp_.ino` or `test_.ino` +```cpp +// temp_json_api.ino - Testing new JSON fetch functionality +// test_networking.ino - Validating network stack changes +``` +- ✅ **FOR:** Testing new APIs during development +- ✅ **FOR:** Quick prototyping and validation +- ✅ **DELETE AFTER USE** - These are temporary by design + +#### 2. **Significant New Feature Examples** +**Use Pattern:** `examples//.ino` +```cpp +// examples/JsonFetchApi/JsonFetchApi.ino - Comprehensive JSON API example +// examples/NetworkStack/NetworkStack.ino - Major networking features +``` +- ✅ **FOR:** Large, comprehensive new features +- ✅ **FOR:** APIs that warrant dedicated examples +- ✅ **FOR:** Features that users will commonly implement +- ✅ **PERMANENT** - These become part of the example library + +### 📋 CREATION CHECKLIST: + +**Before creating ANY .ino file, ask:** + +1. **🤔 Is this testing a new API?** + - YES → Create `temp_.ino`, delete after testing + - NO → Consider alternatives + +2. **🤔 Is this a significant new feature that users will commonly use?** + - YES → Create `examples//.ino` + - NO → Use existing examples or test files + +3. **🤔 Can I modify an existing example instead?** + - YES → Extend existing example rather than creating new + - NO → Proceed with creation + +4. **🤔 Is this just for debugging/validation?** + - YES → Use unit tests or temporary test files + - NO → Consider if it meets the "significant feature" criteria + +### 🔍 REVIEW CRITERIA: + +**For Feature Examples (.ino files that stay):** +- ✅ **Demonstrates complete, real-world usage patterns** +- ✅ **Covers multiple aspects of the feature comprehensively** +- ✅ **Provides educational value for users** +- ✅ **Shows best practices and common use cases** +- ✅ **Is likely to be referenced by multiple users** + +**For Temporary Testing (.ino files that get deleted):** +- ✅ **Clearly named as temporary (temp_*, test_*)** +- ✅ **Focused on specific API validation** +- ✅ **Will be deleted after development cycle** +- ✅ **Too complex for unit test framework** + +### ❌ EXAMPLES OF WHAT NOT TO CREATE: +- `test_basic_led.ino` - Use existing Blink example +- `debug_colors.ino` - Use existing ColorPalette example +- `quick_brightness.ino` - Use unit tests or modify existing example +- `validate_pins.ino` - Use PinTest example or unit tests + +### ✅ EXAMPLES OF JUSTIFIED CREATIONS: +- `temp_new_wifi_api.ino` - Testing major new WiFi functionality (temporary) +- `examples/MachineLearning/MachineLearning.ino` - New ML integration feature (permanent) +- `temp_performance_test.ino` - Validating optimization changes (temporary) + +### 🧹 CLEANUP RESPONSIBILITY: +- **Temporary files:** Creator must delete when testing is complete +- **Feature examples:** Must be maintained and updated as API evolves +- **Abandoned files:** Regular cleanup reviews to remove unused examples + +**Remember: The examples directory is user-facing documentation. Every .ino file should provide clear value to FastLED users.** + +### Memory Management with Smart Pointers and Moveable Objects +**🚨 CRITICAL: Always use proper RAII patterns - smart pointers, moveable objects, or wrapper classes instead of raw pointers for resource management.** + +**Resource Management Options:** +- ✅ **PREFERRED**: `fl::shared_ptr` for shared ownership (multiple references to same object) +- ✅ **PREFERRED**: `fl::unique_ptr` for exclusive ownership (single owner, automatic cleanup) +- ✅ **PREFERRED**: Moveable wrapper objects (like `fl::promise`) for safe copying and transferring of unique resources +- ✅ **ACCEPTABLE**: `fl::vector` storing objects by value when objects support move/copy semantics +- ❌ **AVOID**: `fl::vector` storing raw pointers - use `fl::vector>` or `fl::vector>` +- ❌ **AVOID**: Manual `new`/`delete` - use `fl::make_shared()` or `fl::make_unique()` + +**Moveable Wrapper Pattern:** +When you have a unique resource (like a future, file handle, or network connection) that needs to be passed around easily, create a moveable wrapper class that: +- Internally manages the unique resource (often with `fl::unique_ptr` or similar) +- Provides copy semantics through shared implementation details +- Maintains clear ownership semantics +- Allows safe transfer between contexts + +**Examples:** +```cpp +// ✅ GOOD - Using smart pointers +fl::vector> mActiveClients; +auto client = fl::make_shared(); +mActiveClients.push_back(client); + +// ✅ GOOD - Using unique_ptr for exclusive ownership +fl::unique_ptr client = fl::make_unique(); + +// ✅ GOOD - Moveable wrapper objects (fl::promise example) +fl::vector> mActivePromises; // Copyable wrapper around unique future +fl::promise promise = fetch.get(url).execute(); +mActivePromises.push_back(promise); // Safe copy, shared internal state + +// ✅ GOOD - Objects stored by value (if copyable/moveable) +fl::vector mRequests; // When Request supports copy/move + +// ❌ BAD - Raw pointers require manual memory management +fl::vector mActivePromises; // Memory leaks possible +Promise* promise = new Promise(); // Who calls delete? +``` + +**fl::promise as Moveable Wrapper Example:** +```cpp +// fl::promise wraps a unique fl::future but provides copyable semantics +class promise { + fl::shared_ptr> mImpl; // Shared wrapper around unique resource +public: + promise(const promise& other) : mImpl(other.mImpl) {} // Safe copying + promise(promise&& other) : mImpl(fl::move(other.mImpl)) {} // Move support + // ... wrapper delegates to internal future +}; + +// Usage - can be copied and passed around safely +fl::promise promise = http_get_promise("https://api.example.com"); +someContainer.push_back(promise); // Copy is safe +processAsync(promise); // Can pass to multiple places +``` + +**Why This Pattern:** +- **Automatic cleanup** - No memory leaks from forgotten `delete` calls +- **Exception safety** - Resources automatically freed even if exceptions occur +- **Clear ownership** - Code clearly shows who owns what objects +- **Thread safety** - Smart pointers provide atomic reference counting +- **Easy sharing** - Moveable wrappers allow safe copying of unique resources +- **API flexibility** - Can pass resources between different contexts safely + +## 🔧 CMAKE BUILD SYSTEM ARCHITECTURE (DEPRECATED - NO LONGER USED) + +**⚠️ IMPORTANT: CMake build system is no longer used. FastLED now uses a Python-based build system.** + +### Build System Overview (Historical Reference) +FastLED previously used a sophisticated CMake build system located in `tests/cmake/` with modular configuration: + +**Core Build Files:** +- `tests/CMakeLists.txt` - Main CMake entry point +- `tests/cmake/` - Modular CMake configuration directory + +**Key CMake Modules:** +- `LinkerCompatibility.cmake` - **🚨 CRITICAL for linker issues** - GNU↔MSVC flag translation, lld-link compatibility, warning suppression +- `CompilerDetection.cmake` - Compiler identification and toolchain setup +- `CompilerFlags.cmake` - Compiler-specific flag configuration +- `DebugSettings.cmake` - Debug symbol and optimization configuration +- `OptimizationSettings.cmake` - LTO and performance optimization settings +- `ParallelBuild.cmake` - Parallel compilation and linker selection (mold, lld) +- `TestConfiguration.cmake` - Test target setup and configuration +- `BuildOptions.cmake` - Build option definitions and defaults + +### Linker Configuration (Most Important for Build Issues) + +**🎯 PRIMARY LOCATION for linker problems: `tests/cmake/LinkerCompatibility.cmake`** + +**Key Functions:** +- `apply_linker_compatibility()` - **Main entry point** - auto-detects lld-link and applies compatibility +- `translate_gnu_to_msvc_linker_flags()` - Converts GNU-style flags to MSVC-style for lld-link +- `get_dead_code_elimination_flags()` - Platform-specific dead code elimination +- `get_debug_flags()` - Debug information configuration +- `get_optimization_flags()` - Performance optimization flags + +**Linker Detection Logic:** +```cmake +if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + find_program(LLDLINK_EXECUTABLE lld-link) + if(LLDLINK_EXECUTABLE) + # Apply lld-link compatibility automatically + endif() +endif() +``` + +**Common Linker Issues & Solutions:** +- **lld-link warnings**: Suppressed via `/ignore:4099` in `apply_linker_compatibility()` +- **GNU→MSVC flag translation**: Automatic in `translate_gnu_to_msvc_linker_flags()` +- **Dead code elimination**: Platform-specific via `get_dead_code_elimination_flags()` +- **Debug symbol conflicts**: Handled in `get_debug_flags()` + +### Compiler Configuration + +**Compiler Detection**: `tests/cmake/CompilerDetection.cmake` +- Auto-detects Clang, GCC, MSVC +- Sets up toolchain-specific configurations +- Handles cross-compilation scenarios + +**Compiler Flags**: `tests/cmake/CompilerFlags.cmake` +- Warning configurations per compiler +- Optimization level management +- Platform-specific adjustments + +### Build Options & Configuration + +**Available Build Options** (defined in `BuildOptions.cmake`): +- `FASTLED_DEBUG_LEVEL` - Debug information level (NONE, MINIMAL, STANDARD, FULL) +- `FASTLED_OPTIMIZATION_LEVEL` - Optimization (O0, O1, O2, O3, Os, Ofast) +- `FASTLED_ENABLE_LTO` - Link-time optimization +- `FASTLED_ENABLE_PARALLEL_BUILD` - Parallel compilation +- `FASTLED_STATIC_RUNTIME` - Static runtime linking + +### Testing Infrastructure + +**Test Configuration**: `tests/cmake/TestConfiguration.cmake` +- Defines test targets and dependencies +- Configures test execution parameters +- Sets up coverage and profiling + +**Test Execution**: +- `bash test` - Comprehensive test runner (preferred) +- `uv run test.py` - Python test interface +- Individual test executables in `.build/bin/` + +### Build Troubleshooting Guide + +**For Linker Issues:** +1. **Check `tests/cmake/LinkerCompatibility.cmake` first** +2. Look for lld-link detection and compatibility functions +3. Check GNU→MSVC flag translation logic +4. Verify warning suppression settings + +**For Compiler Issues:** +1. Check `tests/cmake/CompilerDetection.cmake` for detection logic +2. Review `tests/cmake/CompilerFlags.cmake` for flag conflicts +3. Verify optimization settings in `OptimizationSettings.cmake` + +**For Build Performance:** +1. Check `tests/cmake/ParallelBuild.cmake` for parallel settings +2. Review LTO configuration in `OptimizationSettings.cmake` +3. Verify linker selection (mold, lld, default) + + + + +## 🚨 CRITICAL REQUIREMENTS FOR ALL AGENTS (FOREGROUND & BACKGROUND) 🚨 + +## 🚨 MANDATORY COMMAND EXECUTION RULES 🚨 + +### FOREGROUND AGENTS (Interactive Development) + +**FOREGROUND AGENTS MUST FOLLOW THESE COMMAND EXECUTION PATTERNS:** + +#### Python Code Execution: +- ❌ **NEVER run Python code directly** +- ✅ **ALWAYS create/modify tmp.py** with your code +- ✅ **ALWAYS run: `uv run tmp.py`** + +#### Shell Command Execution: +- ❌ **NEVER run shell commands directly** +- ✅ **ALWAYS create/modify tmp.sh** with your commands +- ✅ **ALWAYS run: `bash tmp.sh`** + +#### Command Execution Examples: + +**Python Code Pattern:** +```python +# tmp.py +import subprocess +result = subprocess.run(['git', 'status'], capture_output=True, text=True) +print(result.stdout) +``` +Then run: `uv run tmp.py` + +**Shell Commands Pattern:** +```bash +# tmp.sh +#!/bin/bash +git status +ls -la +uv run ci/ci-compile.py uno --examples Blink +``` +Then run: `bash tmp.sh` + +### BACKGROUND AGENTS (Automated/CI Environments) + +**BACKGROUND AGENTS MUST FOLLOW THESE RESTRICTED COMMAND EXECUTION PATTERNS:** + +#### Python Code Execution: +- ❌ **NEVER run Python code directly** +- ❌ **NEVER create/use tmp.py files** (forbidden for background agents) +- ✅ **ALWAYS use `uv run` with existing scripts** (e.g., `uv run test.py`, `uv run ci/ci-compile.py`) +- ✅ **ALWAYS use MCP server tools** for programmatic operations when available + +#### Shell Command Execution: +- ❌ **NEVER run shell commands directly** +- ❌ **NEVER create/use tmp.sh files** (forbidden for background agents) +- ✅ **ALWAYS use existing bash scripts** (e.g., `bash test`, `bash lint`) +- ✅ **ALWAYS use `uv run` for Python scripts** with proper arguments +- ✅ **ALWAYS use MCP server tools** for complex operations when available + +#### Background Agent Command Examples: + +**✅ ALLOWED - Using existing scripts:** +```bash +bash test +bash lint +uv run test.py audio_json_parsing +uv run ci/ci-compile.py uno --examples Blink +``` + +**❌ FORBIDDEN - Creating temporary files:** +```bash +# DON'T DO THIS - tmp.sh is forbidden for background agents +echo "git status" > tmp.sh +bash tmp.sh +``` + +**✅ PREFERRED - Using MCP server tools:** +```bash +uv run mcp_server.py +# Then use appropriate MCP tools like: validate_completion, symbol_analysis, etc. +``` + +### DELETE Operations - DANGER ZONE (ALL AGENTS): +- 🚨 **STOP and ask for permission** before ANY delete operations +- ✅ **EXCEPTION:** Single files that you just created are OK to delete +- ❌ **NEVER delete multiple files** without explicit permission +- ❌ **NEVER delete directories** without explicit permission +- ❌ **NEVER delete system files or project files** without permission + +### Git-Bash Terminal Truncation Issue +**🚨 IMPORTANT: Git-Bash terminal may truncate the first character of commands** + +**Problem:** The git-bash terminal on Windows sometimes truncates the first character of commands, causing them to fail or execute incorrectly. + +**Solution:** Pre-pend commands with an additional space when using git-bash: +- ✅ **CORRECT**: ` bash test` (note the leading space) +- ❌ **INCORRECT**: `bash test` (may get truncated to `ash test`) + +**Examples:** +```bash +# Good - with leading space for git-bash compatibility + bash test + uv run test.py + bash lint + +# Bad - may get truncated in git-bash +bash test +uv run test.py +bash lint +``` + +**Why:** This ensures commands work reliably across all terminal environments, especially git-bash on Windows systems. + +### Why These Rules Exist: +- **Ensures all operations are reviewable and traceable** +- **Prevents accidental destructive operations in automated environments** +- **Allows for better debugging and error handling** +- **Maintains consistency across different agent types** +- **Provides audit trail for all system modifications** +- **Prevents background agents from creating unnecessary temporary files** + +**These command execution rules apply to ALL operations including but not limited to:** +- File system operations (creating, modifying, deleting files) +- Git operations (commits, pushes, branch changes) +- Package installations and updates +- Build and compilation commands +- Test execution and validation +- System configuration changes + + +### MANDATORY MEMORY REFRESH BEFORE COMPLETION +**🚨 ALL AGENTS MUST REFRESH THEIR MEMORY BEFORE CONCLUDING WORK:** + +- **The AI shall read the cursor rules again to find out what to do before task is considered complete** + - This is MANDATORY for both foreground and background agents + - Reading the cursor rules refreshes your memory about the latest project rules, coding standards, and requirements + - This ensures you have current information about testing procedures, validation tools, and completion requirements + - Do NOT indicate your work is "done" until you have refreshed your memory by reading the cursor rules + +## 🚨 CRITICAL REQUIREMENTS FOR BACKGROUND AGENTS 🚨 + +### MANDATORY PRE-COMPLETION VALIDATION +**ALL BACKGROUND AGENTS MUST FOLLOW THESE REQUIREMENTS BEFORE INDICATING COMPLETION:** + +1. **🚨 ALWAYS RUN `bash test` BEFORE INDICATING COMPLETION** + - This is MANDATORY and NON-NEGOTIABLE for all background agents + - The command `bash test` runs the full test suite including unit tests and compilation checks + - Background agents MUST NOT indicate they are "done" until ALL tests pass + +2. **🚨 USE MCP SERVER VALIDATION TOOL** + - Use the `validate_completion` tool from the MCP server: `uv run mcp_server.py` + - This tool runs `bash test` and validates that all tests pass + - Only indicate completion if this validation tool returns success + +3. **🚨 ZERO TOLERANCE FOR TEST FAILURES** + - If ANY test fails, the background agent MUST fix the issues before completion + - Do NOT indicate completion with failing tests + - Do NOT ignore test errors or warnings + +### Background Agent Completion Checklist: +- [ ] All code changes have been made +- [ ] `bash test` has been run and ALL tests pass +- [ ] No compilation errors or warnings +- [ ] MCP server `validate_completion` tool shows success +- [ ] Only then indicate task completion + +**FAILURE TO FOLLOW THESE REQUIREMENTS WILL RESULT IN BROKEN CODE SUBMISSIONS.** + +### C++ Design Patterns +**Shared Implementation Pattern:** When writing a lot of code that applies the same operation on a bunch of C++ objects, try and determine if those objects share a common base class or internal object. If so consider whether it's appropriate to move the implementation into a shared space. + +## Code Standards + +### Avoid std:: Prefixed Functions +**DO NOT use `std::` prefixed functions or headers in the codebase.** This project provides its own STL-equivalent implementations under the `fl::` namespace. + +**Examples of what to avoid and use instead:** + +**Headers:** + +**Core Language Support:** +- ❌ `#include ` → ✅ `#include "fl/type_traits.h"` +- ❌ `#include ` → ✅ `#include "fl/algorithm.h"` +- ❌ `#include ` → ✅ `#include "fl/functional.h"` +- ❌ `#include ` → ✅ `#include "fl/initializer_list.h"` + +**Containers:** +- ❌ `#include ` → ✅ `#include "fl/vector.h"` +- ❌ `#include ` → ✅ `#include "fl/map.h"` +- ❌ `#include ` → ✅ `#include "fl/hash_map.h"` +- ❌ `#include ` → ✅ `#include "fl/hash_set.h"` +- ❌ `#include ` → ✅ `#include "fl/set.h"` +- ❌ `#include ` → ✅ `#include "fl/slice.h"` + +**Utilities & Smart Types:** +- ❌ `#include ` → ✅ `#include "fl/optional.h"` +- ❌ `#include ` → ✅ `#include "fl/variant.h"` +- ❌ `#include ` → ✅ `#include "fl/pair.h"` (for std::pair) +- ❌ `#include ` → ✅ `#include "fl/string.h"` +- ❌ `#include ` → ✅ `#include "fl/scoped_ptr.h"` or `#include "fl/ptr.h"` + +**Stream/IO:** +- ❌ `#include ` → ✅ `#include "fl/sstream.h"` + +**Threading:** +- ❌ `#include ` → ✅ `#include "fl/thread.h"` + +**Math & System:** +- ❌ `#include ` → ✅ `#include "fl/math.h"` +- ❌ `#include ` → ✅ `#include "fl/stdint.h"` + +**Functions and classes:** +- ❌ `std::move()` → ✅ `fl::move()` +- ❌ `std::forward()` → ✅ `fl::forward()` +- ❌ `std::vector` → ✅ `fl::vector` +- ❌ `std::enable_if` → ✅ `fl::enable_if` + +**Why:** The project maintains its own implementations to ensure compatibility across all supported platforms and to avoid bloating the library with unnecessary STL dependencies. + +**Before using any standard library functionality, check if there's a `fl::` equivalent in the `src/fl/` directory first.** + +### Compiler Warning Suppression +**ALWAYS use the FastLED compiler control macros from `fl/compiler_control.h` for warning suppression.** This ensures consistent cross-compiler support and proper handling of platform differences. + +**Correct Warning Suppression Pattern:** +```cpp +#include "fl/compiler_control.h" + +// Suppress specific warning around problematic code +FL_DISABLE_WARNING_PUSH +FL_DISABLE_FORMAT_TRUNCATION // Use specific warning macros +// ... code that triggers warnings ... +FL_DISABLE_WARNING_POP +``` + +**Available Warning Suppression Macros:** +- ✅ `FL_DISABLE_WARNING_PUSH` / `FL_DISABLE_WARNING_POP` - Standard push/pop pattern +- ✅ `FL_DISABLE_WARNING(warning_name)` - Generic warning suppression (use sparingly) +- ✅ `FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS` - Clang global constructor warnings +- ✅ `FL_DISABLE_WARNING_SELF_ASSIGN_OVERLOADED` - Clang self-assignment warnings +- ✅ `FL_DISABLE_FORMAT_TRUNCATION` - GCC format truncation warnings + +**What NOT to do:** +- ❌ **NEVER use raw `#pragma` directives** - they don't handle compiler differences +- ❌ **NEVER write manual `#ifdef __clang__` / `#ifdef __GNUC__` blocks** - use the macros +- ❌ **NEVER ignore warnings without suppression** - fix the issue or suppress appropriately + +**Examples:** +```cpp +// ✅ CORRECT - Using FastLED macros +#include "fl/compiler_control.h" + +FL_DISABLE_WARNING_PUSH +FL_DISABLE_FORMAT_TRUNCATION +// Code that triggers format truncation warnings +sprintf(small_buffer, "Very long format string %d", value); +FL_DISABLE_WARNING_POP + +// ❌ INCORRECT - Raw pragma directives +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" +sprintf(small_buffer, "Very long format string %d", value); +#pragma GCC diagnostic pop +``` + +**Why:** The FastLED compiler control system automatically handles: +- **Compiler detection** (GCC vs Clang vs MSVC) +- **Version compatibility** (warnings that don't exist on older compilers) +- **Platform specifics** (AVR, ESP32, ARM, etc.) +- **Consistent naming** across different compiler warning systems + +### Debug Printing +**Use `FL_WARN` for debug printing throughout the codebase.** This ensures consistent debug output that works in both unit tests and live application testing. + +**Usage:** +- ✅ `FL_WARN("Debug message: " << message);` +- ❌ `FL_WARN("Value: %d", value);` + +**Why:** `FL_WARN` provides a unified logging interface that works across all platforms and testing environments, including unit tests and Arduino sketches. + +### Emoticon and Emoji Usage Policy +**NO emoticons or emoji characters are allowed in C++ source files.** This ensures professional, maintainable code that works correctly across all platforms and development environments. + +**Prohibited in C++ Files:** +- ❌ **C++ source files** (*.cpp, *.c) +- ❌ **C++ header files** (*.h, *.hpp) +- ❌ **Comments in C++ files** - use clear text instead +- ❌ **String literals in C++ code** - use descriptive text +- ❌ **Log messages in C++ code** - use text prefixes like "SUCCESS:", "ERROR:", "WARNING:" + +**Examples of what NOT to do in C++ files:** +```cpp +// ❌ BAD - Emoticons in comments +// 🎯 This function handles user input + +// ❌ BAD - Emoticons in log messages +FL_WARN("✅ Operation successful!"); +FL_WARN("❌ Error occurred: " << error_msg); + +// ❌ BAD - Emoticons in string literals +const char* status = "🔄 Processing..."; +``` + +**Examples of correct alternatives in C++ files:** +```cpp +// ✅ GOOD - Clear text in comments +// TUTORIAL: This function handles user input + +// ✅ GOOD - Text prefixes in log messages +FL_WARN("SUCCESS: Operation completed successfully!"); +FL_WARN("ERROR: Failed to process request: " << error_msg); + +// ✅ GOOD - Descriptive text in string literals +const char* status = "PROCESSING: Request in progress..."; +``` + +**Allowed in Python Files:** +- ✅ **Python source files** (*.py) - emoticons are acceptable for build scripts, tools, and utilities +- ✅ **Python comments and docstrings** - can use emoticons for clarity in development tools +- ✅ **Python log messages** - emoticons OK in build/test output for visual distinction + +**Why:** +- **Cross-platform compatibility** - Some compilers/platforms have issues with Unicode characters +- **Professional codebase** - Maintains serious, enterprise-grade appearance +- **Accessibility** - Screen readers and text-based tools work better with plain text +- **Consistency** - Ensures uniform code style across all C++ files +- **Debugging** - Text-based prefixes are easier to search and filter in logs + +**Before adding any visual indicators to C++ code, use text-based alternatives like "TODO:", "NOTE:", "WARNING:", "SUCCESS:", "ERROR:" prefixes.** + +### Naming Conventions +**Follow these naming conventions for consistency across the codebase:** + +**Simple Objects:** +- ✅ Use lowercase class names for simple objects (e.g., `fl::vec2f`, `fl::point`, `fl::rect`) +- ❌ Avoid: `fl::Vec2f`, `fl::Point`, `fl::Rect` + +**Complex Objects:** +- ✅ Use CamelCase with uppercase first character for complex objects (e.g., `Raster`, `Controller`, `Canvas`) +- ❌ Avoid: `raster`, `controller`, `canvas` + +**Pixel Types:** +- ✅ Use ALL CAPS for pixel types (e.g., `CRGB`, `CHSV`, `HSV16`, `RGB24`) +- ❌ Avoid: `crgb`, `Crgb`, `chsv`, `Chsv` + +**Member Variables and Functions:** + +**Complex Classes/Objects:** +- ✅ **Member variables:** Use `mVariableName` format (e.g., `mPixelCount`, `mBufferSize`, `mCurrentIndex`) +- ✅ **Member functions:** Use camelCase (e.g., `getValue()`, `setPixelColor()`, `updateBuffer()`) +- ❌ Avoid: `m_variable_name`, `variableName`, `GetValue()`, `set_pixel_color()` + +**Simple Classes/Structs:** +- ✅ **Member variables:** Use lowercase snake_case (e.g., `x`, `y`, `width`, `height`, `pixel_count`) +- ✅ **Member functions:** Use camelCase (e.g., `getValue()`, `setPosition()`, `normalize()`) +- ❌ Avoid: `mX`, `mY`, `get_value()`, `set_position()` + +**Examples:** + +```cpp +// Complex class - use mVariableName for members +class Controller { +private: + int mPixelCount; // ✅ Complex class member variable + uint8_t* mBuffer; // ✅ Complex class member variable + bool mIsInitialized; // ✅ Complex class member variable + +public: + void setPixelCount(int count); // ✅ Complex class member function + int getPixelCount() const; // ✅ Complex class member function + void updateBuffer(); // ✅ Complex class member function +}; + +// Simple struct - use snake_case for members +struct vec2 { + int x; // ✅ Simple struct member variable + int y; // ✅ Simple struct member variable + + float magnitude() const; // ✅ Simple struct member function + void normalize(); // ✅ Simple struct member function +}; + +struct point { + float x; // ✅ Simple struct member variable + float y; // ✅ Simple struct member variable + float z; // ✅ Simple struct member variable + + void setPosition(float x, float y, float z); // ✅ Simple struct member function + float distanceTo(const point& other) const; // ✅ Simple struct member function +}; +``` + +**Why:** These conventions help distinguish between different categories of objects and maintain consistency with existing FastLED patterns. Complex classes use Hungarian notation for member variables to clearly distinguish them from local variables, while simple structs use concise snake_case for lightweight data containers. + +### Container Parameter Types +**Prefer `fl::span` over `fl::vector` or arrays for function parameters.** `fl::span` provides a non-owning view that automatically converts from various container types, making APIs more flexible and efficient. + +**Examples:** +- ✅ `void processData(fl::span data)` - accepts arrays, vectors, and other containers +- ❌ `void processData(fl::vector& data)` - only accepts fl::Vector +- ❌ `void processData(uint8_t* data, size_t length)` - requires manual length tracking + +**Benefits:** +- **Automatic conversion:** `fl::span` can automatically convert from `fl::vector`, C-style arrays, and other container types +- **Type safety:** Maintains compile-time type checking while being more flexible than raw pointers +- **Performance:** Zero-cost abstraction that avoids unnecessary copying or allocation +- **Consistency:** Provides a uniform interface for working with contiguous data + +**When to use `fl::vector` instead:** +- When you need ownership and dynamic resizing capabilities +- When storing data as a class member that needs to persist + +**Why:** Using `fl::span` for parameters makes functions more reusable and avoids forcing callers to convert their data to specific container types. + +### Exception Handling +**DO NOT use try-catch blocks or C++ exception handling in the codebase.** FastLED is designed to work on embedded systems like Arduino where exception handling may not be available or desired due to memory and performance constraints. + +**Examples of what to avoid and use instead:** + +**Avoid Exception Handling:** +- ❌ `try { ... } catch (const std::exception& e) { ... }` - Exception handling not available on many embedded platforms +- ❌ `throw std::runtime_error("error message")` - Throwing exceptions not supported +- ❌ `#include ` or `#include ` - Exception headers not needed + +**Use Error Handling Alternatives:** +- ✅ **Return error codes:** `bool function() { return false; }` or custom error enums +- ✅ **Optional types:** `fl::optional` for functions that may not return a value +- ✅ **Assertions:** `FL_ASSERT(condition)` for debug-time validation +- ✅ **Early returns:** `if (!valid) return false;` for error conditions +- ✅ **Status objects:** Custom result types that combine success/failure with data + +**Examples of proper error handling:** +```cpp +// Good: Using return codes +bool initializeHardware() { + if (!setupPins()) { + FL_WARN("Failed to setup pins"); + return false; + } + return true; +} + +// Good: Using fl::optional +fl::optional calculateValue(int input) { + if (input < 0) { + return fl::nullopt; // No value, indicates error + } + return fl::make_optional(sqrt(input)); +} + +// Good: Using early returns +void processData(const uint8_t* data, size_t len) { + if (!data || len == 0) { + FL_WARN("Invalid input data"); + return; // Early return on error + } + // Process data... +} +``` + +**Why:** Many embedded platforms (especially Arduino-compatible boards) don't support C++ exceptions or have them disabled to save memory and improve performance. FastLED must work reliably across all supported platforms. + +### JSON Usage - Ideal API Patterns +**🎯 PREFERRED: Use the modern `fl::Json` class for all JSON operations.** FastLED provides an ideal JSON API that prioritizes type safety, ergonomics, and crash-proof operation. + +**✅ IDIOMATIC JSON USAGE:** +```cpp +// NEW: Clean, safe, idiomatic API +fl::Json json = fl::Json::parse(jsonStr); +int brightness = json["config"]["brightness"] | 128; // Gets value or 128 default +string name = json["device"]["name"] | string("default"); // Type-safe with default +bool enabled = json["features"]["networking"] | false; // Never crashes + +// Array operations +if (json["effects"].contains("rainbow")) { + // Safe array checking +} +``` + +**❌ DISCOURAGED: Verbose legacy API:** +```cpp +// OLD: Verbose, error-prone API (still works, but not recommended) +fl::JsonDocument doc; +fl::string error; +fl::parseJson(jsonStr, &doc, &error); +int brightness = doc["config"]["brightness"].as(); // Can crash if missing +``` + +**Key Benefits of Ideal API:** +- **🛡️ Type Safety** - No crashes on missing fields or type mismatches +- **🎯 Default Values** - Clean `json["key"] | default` syntax +- **📖 Readable Code** - 50% less boilerplate for common operations +- **🚀 Testing** - Easy test data construction with `JsonBuilder` + +**📚 Reference Example:** See `examples/Json/Json.ino` for comprehensive usage patterns and API comparison. + +**Testing with JsonBuilder:** +```cpp +// Easy test data construction +auto json = JsonBuilder() + .set("brightness", 128) + .set("enabled", true) + .set("name", "test_device") + .build(); + +// Type-safe testing +CHECK_EQ(json["brightness"] | 0, 128); +CHECK(json["enabled"] | false); +``` + +**🎯 GUIDELINE:** Always prefer the ideal `fl::Json` API for new code. The legacy `fl::parseJson` API remains available for backward compatibility but should be avoided in new implementations. + +## ⚠️ CRITICAL WARNING: C++ ↔ JavaScript Bindings + +**🚨 EXTREMELY IMPORTANT: DO NOT MODIFY FUNCTION SIGNATURES IN WEBASSEMBLY BINDINGS WITHOUT EXTREME CAUTION! 🚨** + +The FastLED project includes WebAssembly (WASM) bindings that bridge C++ and JavaScript code. **Changing function signatures in these bindings is a major source of runtime errors and build failures.** + +### Key Binding Files (⚠️ HIGH RISK ZONE ⚠️): +- `src/platforms/wasm/js_bindings.cpp` - Main JavaScript interface via EM_ASM +- `src/platforms/wasm/ui.cpp` - UI update bindings with extern "C" wrappers +- `src/platforms/wasm/active_strip_data.cpp` - Strip data bindings via EMSCRIPTEN_BINDINGS +- `src/platforms/wasm/fs_wasm.cpp` - File system bindings via EMSCRIPTEN_BINDINGS + +### Before Making ANY Changes to These Files: + +1. **🛑 STOP and consider if the change is absolutely necessary** +2. **📖 Read the warning comments at the top of each binding file** +3. **🧪 Test extensively on WASM target after any changes** +4. **🔗 Verify both C++ and JavaScript sides remain synchronized** +5. **📝 Update corresponding JavaScript code if function signatures change** + +### Common Binding Errors: +- **Parameter type mismatches** (e.g., `const char*` vs `std::string`) +- **Return type changes** that break JavaScript expectations +- **Function name changes** without updating JavaScript calls +- **Missing `extern "C"` wrappers** for EMSCRIPTEN_KEEPALIVE functions +- **EMSCRIPTEN_BINDINGS macro changes** without updating JS Module calls + +### If You Must Modify Bindings: +1. **Update BOTH sides simultaneously** (C++ and JavaScript) +2. **Maintain backward compatibility** when possible +3. **Add detailed comments** explaining the interface contract +4. **Test thoroughly** with real WASM builds, not just compilation +5. **Update documentation** and interface specs + +**Remember: The bindings are a CONTRACT between C++ and JavaScript. Breaking this contract causes silent failures and mysterious bugs that are extremely difficult to debug.** + +## 🚨 WASM PLATFORM SPECIFIC RULES 🚨 + +### WASM Unified Build Awareness + +**🚨 CRITICAL: WASM builds use unified compilation when `FASTLED_ALL_SRC=1` is enabled (automatic for Clang builds)** + +**Root Cause**: Multiple .cpp files are compiled together in a single compilation unit, causing: +- Duplicate function definitions +- Type signature conflicts +- Symbol redefinition errors + +**MANDATORY RULES:** +- **ALWAYS check for unified builds** when modifying WASM platform files +- **NEVER create duplicate function definitions** across WASM .cpp files +- **USE `EMSCRIPTEN_KEEPALIVE` functions as canonical implementations** +- **MATCH Emscripten header signatures exactly** for external C functions +- **REMOVE conflicting implementations** and add explanatory comments + +**Fix Pattern Example:** +```cpp +// In timer.cpp (CANONICAL) +extern "C" { +EMSCRIPTEN_KEEPALIVE uint32_t millis() { + // Implementation +} +} + +// In js.cpp (FIXED) +extern "C" { +// NOTE: millis() and micros() functions are defined in timer.cpp with EMSCRIPTEN_KEEPALIVE +// to avoid duplicate definitions in unified builds +} +``` + +### WASM Async Platform Pump Pattern + +**🚨 CRITICAL: Avoid long blocking sleeps that prevent responsive async processing** + +**MANDATORY RULES:** +- **AVOID long blocking sleeps** (e.g., `emscripten_sleep(100)`) in main loops +- **USE short sleep intervals** (1ms) for responsive yielding in main loops +- **ALLOW JavaScript to control timing** via extern functions rather than blocking C++ loops + +**Correct Implementation:** +```cpp +// ✅ GOOD - responsive yielding without blocking +while (true) { + // Keep pthread alive for extern function calls + // Use short sleep for responsiveness + emscripten_sleep(1); // 1ms yield for responsiveness +} +``` + +**Why This Matters:** +- Long blocking sleeps prevent responsive browser interaction +- JavaScript should control FastLED timing via requestAnimationFrame +- Short sleep intervals maintain responsiveness while allowing other threads to work + +### WASM Function Signature Matching + +**🚨 CRITICAL: External C function declarations must match Emscripten headers exactly** + +**Common Error Pattern:** +```cpp +// ❌ WRONG - causes compilation error +extern "C" void emscripten_sleep(int ms); + +// ✅ CORRECT - matches official Emscripten header +extern "C" void emscripten_sleep(unsigned int ms); +``` + +**MANDATORY RULES:** +- **ALWAYS verify against official Emscripten header signatures** +- **NEVER assume parameter types** - check the actual headers +- **UPDATE signatures immediately** when compilation errors occur + +### WASM Sketch Compilation + +**🎯 HOW TO COMPILE ANY ARDUINO SKETCH TO WASM:** + +**Basic Compilation Commands:** +```bash +# Compile any sketch directory to WASM +uv run ci/wasm_compile.py path/to/your/sketch + +# Compile with build and open browser automatically +uv run ci/wasm_compile.py path/to/your/sketch -b --open + +# Compile examples/Blink to WASM +uv run ci/wasm_compile.py examples/Blink -b --open + +# Compile examples/DemoReel100 to WASM +uv run ci/wasm_compile.py examples/DemoReel100 -b --open +``` + +**Output:** Creates `fastled_js/` folder with: +- `fastled.js` - JavaScript loader +- `fastled.wasm` - WebAssembly binary +- `index.html` - HTML page to run the sketch + +**Run in Browser:** +```bash +# Simple HTTP server to test +cd fastled_js +python -m http.server 8000 +# Open http://localhost:8000 +``` + +**🚨 REQUIREMENTS:** +- **Docker must be installed and running** +- **Internet connection** (for Docker image download on first run) +- **~1GB RAM** for Docker container during compilation + +### WASM Testing Requirements + +**🚨 MANDATORY: Always test WASM compilation after platform file changes** + +**Platform Testing Commands:** +```bash +# Test WASM platform changes (for platform developers) +uv run ci/wasm_compile.py -b examples/wasm + +# Quick test without full build +uv run ci/wasm_compile.py examples/wasm --quick +``` + +**Watch For These Error Patterns:** +- `error: conflicting types for 'function_name'` +- `error: redefinition of 'function_name'` +- `warning: attribute declaration must precede definition` +- `RuntimeError: unreachable` (often async-related) + +**MANDATORY RULES:** +- **ALWAYS test WASM compilation** after modifying any WASM platform files +- **USE `uv run ci/wasm_compile.py` for validation** +- **WATCH for unified build conflicts** in compilation output +- **VERIFY async operations work properly** in browser environment + +### WASM Platform File Organization + +**Best Practices for WASM platform files:** +- ✅ **Use `timer.cpp` for canonical timing functions** with `EMSCRIPTEN_KEEPALIVE` +- ✅ **Use `entry_point.cpp` for main() and setup/loop coordination** with async pumping +- ✅ **Use `js.cpp` for JavaScript utility functions** without duplicating timer functions +- ✅ **Include proper async infrastructure** (`fl/async.h`) in entry points +- ✅ **Comment when removing duplicate implementations** to explain unified build conflicts + +## Testing +The project uses a comprehensive test suite including: +- C++ unit tests +- Platform compilation tests +- Code quality checks (ruff, clang-format) +- Example compilation verification + +**🚨 CRITICAL: Manual compiling of tests should never be attempted.** Always put tests in `tests/test_.cpp` and run with `bash test `. + +The `bash test` command now automatically detects when test files are added or removed and will clean the build accordingly. + +### Test Assertion Macros +**🚨 CRITICAL: Always use the proper assertion macros for better error messages and debugging:** + +**Equality Assertions:** +- ✅ **CORRECT**: `CHECK_EQ(A, B)` - for equality comparisons +- ❌ **INCORRECT**: `CHECK(A == B)` - provides poor error messages + +**Inequality Assertions:** +- ✅ **CORRECT**: `CHECK_LT(A, B)` - for less than comparisons +- ✅ **CORRECT**: `CHECK_LE(A, B)` - for less than or equal comparisons +- ✅ **CORRECT**: `CHECK_GT(A, B)` - for greater than comparisons +- ✅ **CORRECT**: `CHECK_GE(A, B)` - for greater than or equal comparisons +- ❌ **INCORRECT**: `CHECK(A < B)`, `CHECK(A <= B)`, `CHECK(A > B)`, `CHECK(A >= B)` + +**Boolean Assertions:** +- ✅ **CORRECT**: `CHECK_TRUE(condition)` - for true conditions +- ✅ **CORRECT**: `CHECK_FALSE(condition)` - for false conditions +- ❌ **INCORRECT**: `CHECK(condition)` - for boolean checks + +**String Assertions:** +- ✅ **CORRECT**: `CHECK_STREQ(str1, str2)` - for string equality +- ✅ **CORRECT**: `CHECK_STRNE(str1, str2)` - for string inequality +- ❌ **INCORRECT**: `CHECK(str1 == str2)` - for string comparisons + +**Floating Point Assertions:** +- ✅ **CORRECT**: `CHECK_DOUBLE_EQ(a, b)` - for floating point equality +- ✅ **CORRECT**: `CHECK_DOUBLE_NE(a, b)` - for floating point inequality +- ❌ **INCORRECT**: `CHECK(a == b)` - for floating point comparisons + +**Examples:** +```cpp +// Good assertion usage +CHECK_EQ(expected_value, actual_value); +CHECK_LT(current_index, max_index); +CHECK_GT(temperature, 0.0); +CHECK_TRUE(is_initialized); +CHECK_FALSE(has_error); +CHECK_STREQ("expected", actual_string); +CHECK_DOUBLE_EQ(3.14159, pi_value, 0.001); + +// Bad assertion usage +CHECK(expected_value == actual_value); // Poor error messages +CHECK(current_index < max_index); // Poor error messages +CHECK(is_initialized); // Unclear intent +CHECK("expected" == actual_string); // Wrong comparison type +``` + +**Why:** Using the proper assertion macros provides: +- **Better error messages** with actual vs expected values +- **Clearer intent** about what is being tested +- **Consistent debugging** across all test failures +- **Type safety** for different comparison types + +### Test File Creation Guidelines +**🚨 CRITICAL: Minimize test file proliferation - Consolidate tests whenever possible** + +**PREFERRED APPROACH:** +- ✅ **CONSOLIDATE:** Add new test cases to existing related test files +- ✅ **EXTEND:** Expand existing `TEST_CASE()` blocks with additional scenarios +- ✅ **REUSE:** Leverage existing test infrastructure and helper functions +- ✅ **COMPREHENSIVE:** Create single comprehensive test files that cover entire feature areas + +**CREATE NEW TEST FILES ONLY WHEN:** +- ✅ **Testing completely new subsystems** with no existing related tests +- ✅ **Isolated feature areas** that don't fit logically into existing test structure +- ✅ **Complex integration tests** that require dedicated setup/teardown + +**AVOID:** +- ❌ **Creating new test files for minor bug fixes** - add to existing tests +- ❌ **One test case per file** - consolidate related functionality +- ❌ **Duplicate test patterns** across multiple files +- ❌ **Scattered feature testing** - keep related tests together + +**EXAMPLES:** + +**✅ GOOD - Consolidation:** +```cpp +// Add to existing tests/test_json_comprehensive.cpp +TEST_CASE("JSON - New Feature Addition") { + // Add new functionality tests to existing comprehensive file +} +``` + +**❌ BAD - Proliferation:** +```cpp +// Don't create tests/test_json_new_feature.cpp for minor additions +// Instead add to existing comprehensive test file +``` + +**DEVELOPMENT WORKFLOW:** +1. **During Development/Bug Fixing:** Temporary test files are acceptable for rapid iteration +2. **Near Completion:** **MANDATORY** - Consolidate temporary tests into existing files +3. **Final Review:** Remove temporary test files and ensure comprehensive coverage in main test files + +**CONSOLIDATION CHECKLIST:** +- [ ] Can this test be added to an existing `TEST_CASE` in the same file? +- [ ] Does an existing test file cover the same functional area? +- [ ] Would this test fit better as a sub-section of a comprehensive test? +- [ ] Are there duplicate test patterns that can be eliminated? + +**Why:** Maintaining a clean, consolidated test suite: +- **Easier maintenance** - fewer files to manage and update +- **Better organization** - related functionality tested together +- **Faster builds** - fewer compilation units +- **Cleaner repository** - less file clutter +- **Improved discoverability** - easier to find existing test coverage + +### Test Execution Format +**🚨 CRITICAL: Always use the correct test execution format:** +- ✅ **CORRECT**: `bash test ` (e.g., `bash test audio_json_parsing`) +- ❌ **INCORRECT**: `./.build/bin/test_.exe` +- ❌ **INCORRECT**: Running executables directly +- ❌ **INCORRECT**: Manual compilation of tests + +**Examples:** +- `bash test` - Run all tests (includes debug symbols) +- `bash test audio_json_parsing` - Run specific test +- `bash test xypath` - Run test_xypath.cpp +- `bash compile uno --examples Blink` - Compile examples + +**Quick Build Options:** +- `bash test --quick --cpp` - Quick C++ tests only (when no *.py changes) +- `bash test --quick` - Quick tests including Python (when *.py changes) + +**Why:** The `bash test` wrapper handles platform differences, environment setup, and proper test execution across all supported systems. + +Use `bash test` as specified in user rules for running unit tests. For compilation tests, use `bash compile --examples ` (e.g., `bash compile uno --examples Blink`). + +**🚨 FOR BACKGROUND AGENTS:** Running `bash test` is MANDATORY before indicating completion. Use the MCP server `validate_completion` tool to ensure all tests pass before completing any task. + +## Debugging and Stack Traces + +### Stack Trace Setup +The FastLED project supports enhanced debugging through stack trace functionality for crash analysis and debugging. + +**For Background Agents**: Use the MCP server tool `setup_stack_traces` to automatically install and configure stack trace support: + +```bash +# Via MCP server (recommended for background agents) +uv run mcp_server.py +# Then use setup_stack_traces tool with method: "auto" +``` + +**Manual Installation**: + +**Ubuntu/Debian**: +```bash +sudo apt-get update +sudo apt-get install -y libunwind-dev build-essential +``` + +**CentOS/RHEL/Fedora**: +```bash +sudo yum install -y libunwind-devel gcc-c++ # CentOS/RHEL +sudo dnf install -y libunwind-devel gcc-c++ # Fedora +``` + +**macOS**: +```bash +brew install libunwind +``` + +### Available Stack Trace Methods +1. **LibUnwind** (Recommended) - Enhanced stack traces with symbol resolution +2. **Execinfo** (Fallback) - Basic stack traces using standard glibc +3. **Windows** (On Windows) - Windows-specific debugging APIs +4. **No-op** (Last resort) - Minimal crash handling + +The build system automatically detects and configures the best available option. + +### Testing Stack Traces +```bash +# Note: Stack trace testing now uses Python build system +# CMake commands are deprecated +cd tests +cmake . && make crash_test_standalone crash_test_execinfo # DEPRECATED + +# Test libunwind version +./.build/bin/crash_test_standalone manual # Manual stack trace +./.build/bin/crash_test_standalone nullptr # Crash test + +# Test execinfo version +./.build/bin/crash_test_execinfo manual # Manual stack trace +./.build/bin/crash_test_execinfo nullptr # Crash test +``` + +### Using in Code +```cpp +#include "tests/crash_handler.h" + +int main() { + setup_crash_handler(); // Enable crash handling + // Your code here... + return 0; +} +``` + +**For Background Agents**: Always run the `setup_stack_traces` MCP tool when setting up a new environment to ensure proper debugging capabilities are available. + +## Platform Build Information Analysis + +The FastLED project includes comprehensive tools for analyzing platform-specific build information, including preprocessor defines, compiler flags, and toolchain paths from `build_info.json` files generated during compilation. + +### Overview + +Platform build information is stored in `.build/{platform}/build_info.json` files that are automatically generated when compiling for any platform. These files contain: + +- **Platform Defines** - Preprocessor definitions (`#define` values) specific to each platform +- **Compiler Information** - Paths, flags, and types for C/C++ compilers +- **Toolchain Aliases** - Tool paths for gcc, g++, ar, objcopy, nm, etc. +- **Build Configuration** - Framework, build type, and other settings + +### Quick Start + +#### 1. Generate Build Information + +Before analyzing, ensure you have compiled the platform: + +```bash +# Compile a platform to generate build_info.json +uv run ci/ci-compile.py uno --examples Blink +uv run ci/ci-compile.py esp32dev --examples Blink +``` + +This creates `.build/{platform}/build_info.json` with all platform information. + +#### 2. Analyze Platform Defines + +**Get platform-specific preprocessor defines:** +```bash +# Command line tool +python3 ci/ci/build_info_analyzer.py --board uno --show-defines + +# Via MCP server +uv run mcp_server.py +# Use build_info_analysis tool with: board="uno", show_defines=true +``` + +**Example output for UNO:** +``` +📋 Platform Defines for UNO: + PLATFORMIO=60118 + ARDUINO_AVR_UNO + F_CPU=16000000L + ARDUINO_ARCH_AVR + ARDUINO=10808 + __AVR_ATmega328P__ +``` + +**Example output for ESP32:** +``` +📋 Platform Defines for ESP32DEV: + ESP32=ESP32 + ESP_PLATFORM + F_CPU=240000000L + ARDUINO_ARCH_ESP32 + IDF_VER="v5.3.2-174-g083aad99cf-dirty" + ARDUINO_ESP32_DEV + # ... and 23 more defines +``` + +#### 3. Compare Platforms + +**Compare defines between platforms:** +```bash +# Command line +python3 ci/ci/build_info_analyzer.py --compare uno esp32dev + +# Via MCP +# Use build_info_analysis tool with: board="uno", compare_with="esp32dev" +``` + +Shows differences and commonalities between platform defines. + +### Available Tools + +#### Command Line Tool + +**Basic Usage:** +```bash +# List available platforms +python3 ci/ci/build_info_analyzer.py --list-boards + +# Show platform defines +python3 ci/ci/build_info_analyzer.py --board uno --show-defines + +# Show compiler information +python3 ci/ci/build_info_analyzer.py --board esp32dev --show-compiler + +# Show toolchain aliases +python3 ci/ci/build_info_analyzer.py --board teensy31 --show-toolchain + +# Show everything +python3 ci/ci/build_info_analyzer.py --board digix --show-all + +# Compare platforms +python3 ci/ci/build_info_analyzer.py --compare uno esp32dev + +# JSON output for automation +python3 ci/ci/build_info_analyzer.py --board uno --show-defines --json +``` + +#### MCP Server Tool + +**For Background Agents**, use the MCP server `build_info_analysis` tool: + +```bash +# Start MCP server +uv run mcp_server.py + +# Use build_info_analysis tool with parameters: +# - board: "uno", "esp32dev", "teensy31", etc. or "list" to see available +# - show_defines: true/false (default: true) +# - show_compiler: true/false +# - show_toolchain: true/false +# - show_all: true/false +# - compare_with: "other_board_name" for comparison +# - output_json: true/false for programmatic use +``` + +### Supported Platforms + +The build info analysis works with **ANY platform** that generates a `build_info.json` file: + +**Embedded Platforms:** +- ✅ **UNO (AVR)** - 8-bit microcontroller (6 defines) +- ✅ **ESP32DEV** - WiFi-enabled 32-bit platform (29 defines) +- ✅ **ESP32S3, ESP32C3, ESP32C6** - All ESP32 variants +- ✅ **TEENSY31, TEENSYLC** - ARM Cortex-M platforms +- ✅ **DIGIX, BLUEPILL** - ARM Cortex-M3 platforms +- ✅ **STM32, NRF52** - Various ARM platforms +- ✅ **RPIPICO, RPIPICO2** - Raspberry Pi Pico platforms +- ✅ **ATTINY85, ATTINY1616** - Small AVR microcontrollers + +### Use Cases + +#### For Code Development + +**Understanding Platform Differences:** +```bash +# See what defines are available for conditional compilation +python3 ci/ci/build_info_analyzer.py --board esp32dev --show-defines + +# Compare two platforms to understand differences +python3 ci/ci/build_info_analyzer.py --compare uno esp32dev +``` + +**Compiler and Toolchain Information:** +```bash +# Get compiler paths and flags for debugging builds +python3 ci/ci/build_info_analyzer.py --board teensy31 --show-compiler + +# Get toolchain paths for symbol analysis +python3 ci/ci/build_info_analyzer.py --board digix --show-toolchain +``` + +#### For Automation + +**JSON output for scripts:** +```bash +# Get defines as JSON for processing +python3 ci/ci/build_info_analyzer.py --board uno --show-defines --json + +# Get all build info as JSON +python3 ci/ci/build_info_analyzer.py --board esp32dev --show-all --json +``` + +### Integration with Other Tools + +Build info analysis integrates with other FastLED tools: + +1. **Symbol Analysis** - Uses build_info.json to find toolchain paths +2. **Compilation** - Generated automatically during example compilation +3. **Testing** - Provides platform context for test results + +### Troubleshooting + +**Common Issues:** + +1. **"No boards with build_info.json found"** + - **Solution:** Compile a platform first: `uv run ci/ci-compile.py {board} --examples Blink` + +2. **"Board key not found in build_info.json"** + - **Solution:** Check available boards: `python3 ci/ci/build_info_analyzer.py --list-boards` + +3. **"Build info not found for platform"** + - **Solution:** Ensure the platform compiled successfully and check `.build/{board}/build_info.json` exists + +### Best Practices + +1. **Always compile first** before analyzing build information +2. **Use comparison feature** to understand platform differences +3. **Check defines** when adding platform-specific code +4. **Use JSON output** for automated processing and CI/CD +5. **Combine with symbol analysis** for complete platform understanding + +**For Background Agents:** Use the MCP server `build_info_analysis` tool for consistent access to platform build information and proper error handling. + +## Symbol Analysis for Binary Optimization + +The FastLED project includes comprehensive symbol analysis tools to identify optimization opportunities and understand binary size allocation across all supported platforms. + +### Overview + +Symbol analysis examines compiled ELF files to: +- **Identify large symbols** that may be optimization targets +- **Understand memory allocation** across different code sections +- **Find unused features** that can be eliminated to reduce binary size +- **Compare symbol sizes** between different builds or platforms +- **Provide optimization recommendations** based on actual usage patterns + +### Supported Platforms + +The symbol analysis tools work with **ANY platform** that generates a `build_info.json` file, including: + +**Embedded Platforms:** +- ✅ **UNO (AVR)** - Small 8-bit microcontroller (typically ~3-4KB symbols) +- ✅ **ESP32DEV (Xtensa)** - WiFi-enabled 32-bit platform (typically ~200-300KB symbols) +- ✅ **ESP32S3, ESP32C3, ESP32C6, etc.** - All ESP32 variants supported + +**ARM Platforms:** +- ✅ **TEENSY31 (ARM Cortex-M4)** - High-performance 32-bit (typically ~10-15KB symbols) +- ✅ **TEENSYLC (ARM Cortex-M0+)** - Low-power ARM platform (typically ~8-10KB symbols) +- ✅ **DIGIX (ARM Cortex-M3)** - Arduino Due compatible (typically ~15-20KB symbols) +- ✅ **STM32 (ARM Cortex-M3)** - STMicroelectronics platform (typically ~12-18KB symbols) + +**And many more!** Any platform with toolchain support and `build_info.json` generation. + +### Quick Start + +#### 1. Prerequisites + +Before running symbol analysis, ensure you have compiled the platform: + +```bash +# Compile platform first (required step) +uv run ci/ci-compile.py uno --examples Blink +uv run ci/ci-compile.py esp32dev --examples Blink +uv run ci/ci-compile.py teensy31 --examples Blink +``` + +This generates the required `.build/{platform}/build_info.json` file and ELF binary. + +#### 2. Run Symbol Analysis + +**Analyze specific platform:** +```bash +uv run ci/ci/symbol_analysis.py --board uno +uv run ci/ci/symbol_analysis.py --board esp32dev +uv run ci/ci/symbol_analysis.py --board teensy31 +``` + +**Analyze all available platforms at once:** +```bash +uv run ci/demo_symbol_analysis.py +``` + +**Using MCP Server (Recommended for Background Agents):** +```bash +# Use MCP server tools +uv run mcp_server.py +# Then use symbol_analysis tool with board: "uno" or "auto" +# Or use symbol_analysis tool with run_all_platforms: true +``` + +### Analysis Output + +Symbol analysis provides detailed reports including: + +**Summary Information:** +- **Total symbols count** and **total size** +- **Symbol type breakdown** (text, data, bss, etc.) +- **Platform-specific details** (toolchain, ELF file location) + +**Largest Symbols List:** +- **Top symbols by size** for optimization targeting +- **Demangled C++ names** for easy identification +- **Size in bytes** and **percentage of total** + +**Optimization Recommendations:** +- **Feature analysis** - unused features that can be disabled +- **Size impact estimates** - potential savings from removing features +- **Platform-specific suggestions** based on symbol patterns + +### Example Analysis Results + +**UNO Platform (Small embedded):** +``` +================================================================================ +UNO SYMBOL ANALYSIS REPORT +================================================================================ + +SUMMARY: + Total symbols: 51 + Total symbol size: 3767 bytes (3.7 KB) + +LARGEST SYMBOLS (sorted by size): + 1. 1204 bytes - ClocklessController<...>::showPixels(...) + 2. 572 bytes - CFastLED::show(unsigned char) + 3. 460 bytes - main + 4. 204 bytes - CPixelLEDController<...>::show(...) +``` + +**ESP32 Platform (Feature-rich):** +``` +================================================================================ +ESP32DEV SYMBOL ANALYSIS REPORT +================================================================================ + +SUMMARY: + Total symbols: 2503 + Total symbol size: 237092 bytes (231.5 KB) + +LARGEST SYMBOLS (sorted by size): + 1. 12009 bytes - _vfprintf_r + 2. 11813 bytes - _svfprintf_r + 3. 8010 bytes - _vfiprintf_r + 4. 4192 bytes - port_IntStack +``` + +### Advanced Features + +#### JSON Output +Save detailed analysis results for programmatic processing: +```bash +uv run symbol_analysis.py --board esp32dev --output-json +# Results saved to: .build/esp32dev_symbol_analysis.json +``` + +#### Batch Analysis +Analyze multiple platforms in sequence: +```bash +for board in uno esp32dev teensy31; do + uv run symbol_analysis.py --board $board +done +``` + +#### Integration with Build Systems +The symbol analysis can be integrated into automated build processes: +```python +# In your Python build script +import subprocess +result = subprocess.run(['uv', 'run', 'symbol_analysis.py', '--board', 'uno'], + capture_output=True, text=True) +``` + +### MCP Server Tools + +**For Background Agents**, use the MCP server tools for symbol analysis: + +1. **Generic Symbol Analysis** (`symbol_analysis` tool): + - Works with **any platform** (UNO, ESP32, Teensy, STM32, etc.) + - Auto-detects available platforms from `.build/` directory + - Can analyze single platform or all platforms simultaneously + - Provides comprehensive usage instructions + +2. **ESP32-Specific Analysis** (`esp32_symbol_analysis` tool): + - **ESP32 platforms only** with FastLED-focused filtering + - Includes feature analysis and optimization recommendations + - FastLED-specific symbol identification and grouping + +**Usage via MCP:** +```bash +# Start MCP server +uv run mcp_server.py + +# Use symbol_analysis tool with parameters: +# - board: "uno", "esp32dev", or "auto" +# - run_all_platforms: true/false +# - output_json: true/false +``` + +### Troubleshooting + +**Common Issues:** + +1. **"build_info.json not found"** + - **Solution:** Compile the platform first: `uv run ci/ci-compile.py {board} --examples Blink` + +2. **"ELF file not found"** + - **Solution:** Ensure compilation completed successfully and check `.build/{board}/` directory + +3. **"Tool not found"** (nm, c++filt, etc.) + - **Solution:** The platform toolchain isn't installed or configured properly + - **Check:** Verify PlatformIO platform installation: `uv run pio platform list` + +4. **"No symbols found"** + - **Solution:** The ELF file may be stripped or compilation failed + - **Check:** Verify ELF file exists and has debug symbols + +**Debug Mode:** +```bash +# Run with verbose Python output for debugging +uv run python -v ci/ci/symbol_analysis.py --board uno +``` + +### Best Practices + +1. **Always compile first** before running symbol analysis +2. **Use consistent examples** (like Blink) for size comparisons +3. **Run analysis on clean builds** to avoid cached/incremental build artifacts +4. **Compare results across platforms** to understand feature scaling +5. **Focus on the largest symbols first** for maximum optimization impact +6. **Use JSON output for automated processing** and trend analysis + +**For Background Agents:** Always use the MCP server `symbol_analysis` tool for consistent results and proper error handling. diff --git a/v1.5/lib/FastLED/README.md b/v1.5/lib/FastLED/README.md new file mode 100644 index 0000000..5b005f1 --- /dev/null +++ b/v1.5/lib/FastLED/README.md @@ -0,0 +1,510 @@ +# FastLED - The Universal LED Library + +**Transform any microcontroller into an LED powerhouse.** + +Drive **30,000+ LEDs** on high-end devices • **Sub-$1 compatibility** on tiny chips • **Background rendering** for responsive apps • **Nearly every LED chipset supported** • [**#2 most popular Arduino library**](https://docs.arduino.cc/libraries/) + +[![Arduino's 2nd Most Popular Library](https://www.ardu-badge.com/badge/FastLED.svg)](https://www.ardu-badge.com/FastLED) [![Build Status](https://github.com/FastLED/FastLED/workflows/build/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build.yml) [![Unit Tests](https://github.com/FastLED/FastLED/actions/workflows/build_unit_test.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_unit_test.yml) [![Documentation](https://img.shields.io/badge/Docs-Doxygen-blue.svg)](http://fastled.io/docs) [![Community](https://img.shields.io/badge/reddit-/r/FastLED-orange.svg?logo=reddit)](https://www.reddit.com/r/FastLED/) + + +## ⚡ Get Blinking in 30 Seconds + +```cpp +#include +#define NUM_LEDS 60 +CRGB leds[NUM_LEDS]; + +void setup() { + FastLED.addLeds(leds, NUM_LEDS); +} + +void loop() { + leds[0] = CRGB::Red; FastLED.show(); delay(500); + leds[0] = CRGB::Blue; FastLED.show(); delay(500); +} +``` + +**✅ Works on Arduino, ESP32, Teensy, Raspberry Pi, and 50+ other platforms** + +## 🚀 Why FastLED? + +| **Massive Scale** | **Tiny Footprint** | **Background Rendering** | **Universal** | +|-------------------|---------------------|--------------------------|---------------| +| Drive 30,000 LEDs on Teensy 4.1 | Runs on $0.50 ATtiny chips | ESP32/Teensy render while you code | Works on 50+ platforms | +| 50 parallel strips on Teensy | <2KB on Arduino Uno | Never miss user input | Nearly every LED chipset | + +**🎯 Performance**: Zero-cost global brightness • High-performance 8-bit math, memory efficient on platforms that need it. +**🔧 Developer Experience**: Quick platform switching • Extensive examples • Active community support + +## Table of Contents + +- [🆕 Latest Feature](#-latest-feature) +- [⭐ Community Growth](#-community-growth) +- [🆕 Latest Features](#-latest-features) +- [🌍 Platform Support](#-platform-support) +- [📦 Installation](#-installation) +- [📚 Documentation & Support](#-documentation--support) +- [🎮 Advanced Features](#-advanced-features) +- [🤝 Contributing](#-contributing) + +📊 Detailed Build Status + +### Arduino Family +**Core Boards:** [![uno](https://github.com/FastLED/FastLED/actions/workflows/build_uno.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_uno.yml) [![nano_every](https://github.com/FastLED/FastLED/actions/workflows/build_nano_every.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_nano_every.yml) [![uno_r4_wifi](https://github.com/FastLED/FastLED/actions/workflows/build_uno_r4_wifif.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_uno_r4_wifif.yml) [![yun](https://github.com/FastLED/FastLED/actions/workflows/build_yun.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_yun.yml) + +**ARM Boards:** [![due](https://github.com/FastLED/FastLED/actions/workflows/build_due.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_due.yml) [![digix](https://github.com/FastLED/FastLED/actions/workflows/build_digix.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_digix.yml) [![Arduino Giga-R1](https://github.com/FastLED/FastLED/actions/workflows/build_giga_r1.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_giga_r1.yml)* + +**ATtiny Series:** [![attiny85](https://github.com/FastLED/FastLED/actions/workflows/build_attiny85.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_attiny85.yml) [![attiny88](https://github.com/FastLED/FastLED/actions/workflows/build_attiny88.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_attiny88.yml) [![attiny1604](https://github.com/FastLED/FastLED/actions/workflows/build_attiny1604.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_attiny1604.yml) [![attiny1616](https://github.com/FastLED/FastLED/actions/workflows/build_attiny1616.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_attiny1616.yml) [![attiny4313](https://github.com/FastLED/FastLED/actions/workflows/build_attiny4313.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_attiny4313.yml)** + +*Notes: * Giga-R1 support added in 3.9.14 • ** ATtiny4313 has limited memory (WS2812 Blink + APA102 examples only) + +### Teensy Series +**Standard Models:** [![teensy30](https://github.com/FastLED/FastLED/actions/workflows/build_teensy30.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_teensy30.yml) [![teensy31](https://github.com/FastLED/FastLED/actions/workflows/build_teensy31.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_teensy31.yml) [![teensyLC](https://github.com/FastLED/FastLED/actions/workflows/build_teensyLC.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_teensyLC.yml) [![teensy40](https://github.com/FastLED/FastLED/actions/workflows/build_teensy40.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_teensy40.yml) [![teensy41](https://github.com/FastLED/FastLED/actions/workflows/build_teensy41.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_teensy41.yml) + +**Special Features:** [![teensy_octoWS2811](https://github.com/FastLED/FastLED/actions/workflows/build_teensy_octo.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_teensy_octo.yml) [![teensy41 ObjectFLED](https://github.com/FastLED/FastLED/actions/workflows/build_teensy41_ofled.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_teensy41_ofled.yml) + +### NRF52 (Nordic) +[![nrf52840_sense](https://github.com/FastLED/FastLED/actions/workflows/build_adafruit_feather_nrf52840_sense.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_adafruit_feather_nrf52840_sense.yml) [![nordicnrf52_dk](https://github.com/FastLED/FastLED/actions/workflows/build_nrf52840_dk.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_nrf52840_dk.yml) [![adafruit_xiaoblesense](https://github.com/FastLED/FastLED/actions/workflows/build_adafruit_xiaoblesense.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_adafruit_xiaoblesense.yml) [![nrf52_xiaoblesense](https://github.com/FastLED/FastLED/actions/workflows/build_nrf52_xiaoblesense.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_nrf52_xiaoblesense.yml) + +*Note: NRF52 XiaoBLE board has mbed engine but doesn't compile against Arduino.h for unknown reasons. + +### Apollo3 (Ambiq) +[![apollo3_red](https://github.com/FastLED/FastLED/actions/workflows/build_apollo3_red.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_apollo3_red.yml) [![apollo3_thing_explorable](https://github.com/FastLED/FastLED/actions/workflows/build_apollo3_thing_explorable.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_apollo3_thing_explorable.yml) + +*Beta support added in 3.10.2 + +### STM32 (STMicroelectronics) +[![Bluepill (STM32F103C8)](https://github.com/FastLED/FastLED/actions/workflows/build_bluepill.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_bluepill.yml) [![Blackpill (STM32F411CE)](https://github.com/FastLED/FastLED/actions/workflows/build_blackpill_stm32f4.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_blackpill_stm32f4.yml) [![Maple Mini (STM32F103CB)](https://github.com/FastLED/FastLED/actions/workflows/build_maple_map.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_maple_map.yml) [![stm103tb](https://github.com/FastLED/FastLED/actions/workflows/build_stm103tb.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_stm103tb.yml) + +*Note: STM103TB has limited PlatformIO support + +### Silicon Labs (SiLabs) +[![ThingPlusMatter_mgm240s](https://github.com/FastLED/FastLED/actions/workflows/build_mgm240s_thingplusmatter.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_mgm240s_thingplusmatter.yml) + +*MGM240 (EFR32MG24) support for Arduino Nano Matter and SparkFun Thing Plus Matter boards + +### Raspberry Pi Pico +[![rp2040](https://github.com/FastLED/FastLED/actions/workflows/build_rp2040.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_rp2040.yml) [![rp2350](https://github.com/FastLED/FastLED/actions/workflows/build_rp2350.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_rp2350.yml) [![rp2350B SparkfunXRP](https://github.com/FastLED/FastLED/actions/workflows/build_rp2350B.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_rp2350B.yml) + +### ESP32 (Espressif) +**ESP8266:** [![esp32-8266](https://github.com/FastLED/FastLED/actions/workflows/build_esp8622.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp8622.yml) + +**ESP32 Classic:** [![esp32dev](https://github.com/FastLED/FastLED/actions/workflows/build_esp32dev.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32dev.yml) [![esp32wroom](https://github.com/FastLED/FastLED/actions/workflows/build_esp32wroom.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32wroom.yml) + +**ESP32 S-Series:** [![esp32s2](https://github.com/FastLED/FastLED/actions/workflows/build_esp32s2.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32s2.yml) [![esp32s3](https://github.com/FastLED/FastLED/actions/workflows/build_esp32s3.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32s3.yml) + +**ESP32 C-Series:** [![esp32c2](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c2.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c2.yml)* [![esp32c3](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c3.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c3.yml) [![esp32c5](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c5.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c5.yml)[![esp32c6](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c6.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32c6.yml) + +**ESP32 Advanced:** [![esp32h2](https://github.com/FastLED/FastLED/actions/workflows/build_esp32h2.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32h2.yml) [![esp32p4](https://github.com/FastLED/FastLED/actions/workflows/build_esp32p4.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32p4.yml) + +**Special Features:** [![esp32_i2s_ws2812](https://github.com/FastLED/FastLED/actions/workflows/build_esp32_i2s_ws2812.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32_i2s_ws2812.yml) [![esp32 extra libs](https://github.com/FastLED/FastLED/actions/workflows/build_esp_extra_libs.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp_extra_libs.yml) [![esp32dev_namespace](https://github.com/FastLED/FastLED/actions/workflows/build_esp32dev_namespace.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32dev_namespace.yml) + +**QEMU Tests:** [![ESP32-DEV QEMU Test](https://github.com/FastLED/FastLED/actions/workflows/qemu_esp32dev_test.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/qemu_esp32dev_test.yml) [![ESP32-C3 QEMU Test](https://github.com/FastLED/FastLED/actions/workflows/qemu_esp32c3_test.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/qemu_esp32c3_test.yml) [![ESP32-S3 QEMU Test](https://github.com/FastLED/FastLED/actions/workflows/qemu_esp32s3_test.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/qemu_esp32s3_test.yml) + +**Legacy:** [![esp32dev-idf3.3-lts](https://github.com/FastLED/FastLED/actions/workflows/build_esp32dev_idf3.3.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_esp32dev_idf3.3.yml) + +*Notes: * ESP32-C2 support added in 3.9.10 • [Espressif compatibility evaluation](https://github.com/espressif/arduino-esp32/blob/gh-pages/LIBRARIES_TEST.md) + +### Specialty Platforms +**x86:** [![linux_native](https://github.com/FastLED/FastLED/actions/workflows/build_linux.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_linux.yml) + +**WebAssembly:** [![wasm](https://github.com/FastLED/FastLED/actions/workflows/build_wasm.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_wasm.yml) [![wasm_compile_test](https://github.com/FastLED/FastLED/actions/workflows/build_wasm_compilers.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_wasm_compilers.yml) + +### Library Size Validation +**Core Platforms:** [![attiny85_binary_size](https://github.com/FastLED/FastLED/actions/workflows/check_attiny85.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/check_attiny85.yml) [![uno_binary_size](https://github.com/FastLED/FastLED/actions/workflows/check_uno_size.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/check_uno_size.yml) [![esp32dev_binary_size](https://github.com/FastLED/FastLED/actions/workflows/check_esp32_size.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/check_esp32_size.yml) + +**Teensy Platforms:** [![check_teensylc_size](https://github.com/FastLED/FastLED/actions/workflows/check_teensylc_size.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/check_teensylc_size.yml) [![check_teensy30_size](https://github.com/FastLED/FastLED/actions/workflows/check_teensy30_size.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/check_teensy30_size.yml) [![check_teensy31_size](https://github.com/FastLED/FastLED/actions/workflows/check_teensy31_size.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/check_teensy31_size.yml) [![teensy41_binary_size](https://github.com/FastLED/FastLED/actions/workflows/check_teensy41_size.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/check_teensy41_size.yml) + + +## ⭐ Community Growth + + + + + + Star History Chart + + + +## 🆕 Latest Features + +## **FastLED 3.10.2: Corkscrew Mapping** + +See [examples/FestivalStick/FestivalStick.ino](examples/FestivalStick/FestivalStick.ino) + +https://github.com/user-attachments/assets/19af4b54-c189-482b-b13b-2d7f73efa4e0 + +### **FastLED 3.10.0: Animartrix Out of Beta** +Advanced animation framework for complex LED visualizations and effects. + +[![FastLED Animartrix Demo](https://fastled.github.io/assets/fastled_3_10_animartrix.jpg)](https://fastled.github.io/assets/fastled_3_10_animartrix.mp4) + +### **FastLED 3.9.16: WaveFx / Multi Layer Compositing** +Multi-layer compositing & time-based animation control for tech-artists. + + +https://github.com/user-attachments/assets/ff8e0432-3e0d-47cc-a444-82ce27f562af + +| **3.9.13** | **3.9.10** | **3.9.8** | **3.9.2** | +|------------|------------|-----------|-----------| +| [**HD107 Turbo**](#new-in-3913-hd107-turbo-40mhz-led-support)
40MHz LED support | [**ESP32 SPI**](#new-in-3910-super-stable-ws2812-spi-driver-for-esp32)
Super stable WS2812 driver | [**Massive Teensy**](#new-in-398---massive-teensy-41--40-ws2812-led-output)
50 parallel pins on 4.1 | [**WS2812 Overclock**](#new-in-392---overclocking-of-ws2812)
Up to 70% speed boost | + +| **3.7.7** | **More Features** | +|-----------|-------------------| +| [**RGBW Support**](#new-in-377---rgbw-led-strip-support)
White channel LED strips | [📋 Full Changelog](https://github.com/FastLED/FastLED/releases)
[📺 Demo Videos](https://zackees.github.io/fastled-wasm/) | + +[**📺 Live Demos**](https://zackees.github.io/fastled-wasm/) • [**📋 Full Changelog**](https://github.com/FastLED/FastLED/releases) + +
+📖 Detailed Feature Information (Click to expand) + +## New in 3.10.2: Corkscrew Mapping + +See [examples/FestivalStick/FestivalStick.ino](examples/FestivalStick/FestivalStick.ino) + +https://github.com/user-attachments/assets/19af4b54-c189-482b-b13b-2d7f73efa4e0 + +## New in 3.10.0: Animartrix out of beta + +[![FastLED 3.10 Animartrix](https://fastled.github.io/assets/fastled_3_10_animartrix.jpg)](https://fastled.github.io/assets/fastled_3_10_animartrix.mp4) + + +## New in 3.9.16: WaveFx / Multi Layer Compositing / Time-based animation control + +Video: + + + +https://github.com/user-attachments/assets/ff8e0432-3e0d-47cc-a444-82ce27f562af + + + +#### Major release for tech-artists! + +Lots of improvements in this release, read the full [change list here](https://github.com/FastLED/FastLED/releases/tag/3.9.16) + +#### Links + + * This demo -> [FxWave2d](https://github.com/FastLED/FastLED/blob/master/examples/FxWave2d/FxWave2d.ino) + * [Wave Simulation Library](https://github.com/FastLED/FastLED/blob/master/src/fl/wave_simulation.h) + * [FireCylinder](https://github.com/FastLED/FastLED/blob/master/examples/FireCylinder/FireCylinder.ino) + * Wraps around so that (0,y) ~= (width-1,y) + * [TimeAlpha](https://github.com/FastLED/FastLED/blob/master/src/fl/time_alpha.h) + * Precision control of animations with time-based alpha transition. + + + +## New in 3.9.13: HD107 "Turbo" 40Mhz LED Support + +![image](https://github.com/user-attachments/assets/9684ab7d-2eaa-40df-a00d-0dff18098917) + +## New in 3.9.12: WS2816 "HD" LED support + +![image](https://github.com/user-attachments/assets/258ec44c-af82-44b7-ad7b-fac08daa9bcb) + +## New in 3.9.10: Super Stable WS2812 SPI driver for ESP32 + +![image (2)](https://github.com/user-attachments/assets/b3c5801c-66df-40af-a6b8-bbd1520fbb36) + +## New in 3.9.9: 16-way Yves I2S parallel driver for the ESP32-S3 + +![perpetualmaniac_an_led_display_in_a_room_lots_of_refaction_of_t_eb7c170a-7b2c-404a-b114-d33794b4954b](https://github.com/user-attachments/assets/982571fc-9b8d-4e58-93be-5bed76a0c53d) + +*Note some users find that newer versions of the ESP32 Arduino core (3.10) don't work very well, but older versions do, see [issue 1903](https://github.com/FastLED/FastLED/issues/1903) + +## New in 3.9.8 - Massive Teensy 4.1 & 4.0 WS2812 LED output + + * *Teensy 4.1: 50 parallel pins* + * *Teensy 4.0: 42 parallel pins* + +![New Project](https://github.com/user-attachments/assets/79dc2801-5161-4d5a-90a2-0126403e215f) + + + + +## New in 3.9.2 - Overclocking of WS2812 +![image](https://github.com/user-attachments/assets/be98fbe6-0ec7-492d-8ed1-b7eb6c627e86) +Update: max overclock has been reported at +70%: https://www.reddit.com/r/FastLED/comments/1gkcb6m/fastled_FASTLED_OVERCLOCK_17/ + +### New in 3.7.7 - RGBW LED Strip Support + +![image (1)](https://github.com/user-attachments/assets/d4892626-3dc6-4d6d-a740-49ddad495fa5) + +
+ +## 🌍 Platform Support + +### Platform Categories +| **Arduino Family** | **ESP32 Series** | **Teensy** | **ARM** | **Specialty** | +|--------------------|------------------|------------|---------|---------------| +| Uno, Nano, Mega
Due, Giga R1, R4 | ESP32, S2, S3, C3
C6, H2, P4 | 3.0, 3.1, 4.0, 4.1
LC + OctoWS2811 | STM32, NRF52
Apollo3, Silicon Labs | Raspberry Pi
WASM, x86 | + +**FastLED supports 50+ platforms!** From sub-$1 ATtiny chips to high-end Teensy 4.1 with 50 parallel outputs. + +## 📦 Installation + +### Quick Install Options + +| **Arduino IDE** | **PlatformIO** | **Package Managers** | +|-----------------|----------------|----------------------| +| Library Manager → Search "FastLED" → Install | `lib_deps = fastled/FastLED` | `pio pkg install --library "fastled/FastLED"` | +| Or install [latest release .zip](https://github.com/FastLED/FastLED/releases/latest/) | Add to `platformio.ini` | Command line installation | + +### Template Projects + +- **🎯 [Arduino + PlatformIO Starter](https://github.com/FastLED/PlatformIO-Starter)** - Best of both worlds, works with both IDEs +- **🚀 [FastLED Examples](examples)** - 100+ ready-to-run demos +- **🌐 [Web Compiler](https://zackees.github.io/fastled-wasm/)** - Test in your browser + +
+Arduino IDE Setup Instructions (Click to expand) + +After installing the Arduino IDE, add FastLED through the Library Manager: + +![Arduino IDE Library Manager](https://github.com/user-attachments/assets/b1c02cf9-aba6-4f80-851e-78df914e2501) + +![FastLED Library Search](https://github.com/user-attachments/assets/508eb700-7dd4-4901-a901-68c56cc4d0e1) + +
+ + +## 📚 Documentation & Support + +| **📖 Documentation** | **💬 Community** | **🐛 Issues** | **📺 Examples** | +|---------------------|------------------|---------------|-----------------| +| [API Reference](http://fastled.io/docs) | [Reddit r/FastLED](https://www.reddit.com/r/FastLED/) | [GitHub Issues](http://fastled.io/issues) | [Live Demos](https://zackees.github.io/fastled-wasm/) | +| [Doxygen Docs](https://fastled.io/docs/files.html) | 1000s of users & solutions | Bug reports & feature requests | [GitHub Examples](examples) | + +**Need Help?** Visit [r/FastLED](https://reddit.com/r/FastLED) - thousands of knowledgeable users and extensive solution history! + +## 🎮 Advanced Features + +### Performance Leaders: Parallel Output Records + +| **Platform** | **Max Parallel Outputs** | **Performance Notes** | +|--------------|---------------------------|----------------------| +| **Teensy 4.1** | 50 parallel strips | Current record holder - [Example](examples/TeensyMassiveParallel/TeensyMassiveParallel.ino) | +| **Teensy 4.0** | 42 parallel strips | High-performance ARM Cortex-M7 | +| **ESP32DEV** | 24 via I2S + 8 via RMT | [I2S Example](examples/EspI2SDemo/EspI2SDemo.ino) | +| **ESP32-S3** | 16 via I2S + 4 via RMT | [S3 Example](examples/Esp32S3I2SDemo/Esp32S3I2SDemo.ino) | + +*Note: Some ESP32 Arduino core versions (3.10+) have compatibility issues. Older versions work better - see [issue #1903](https://github.com/FastLED/FastLED/issues/1903)* + +### Exotic Setups (120+ Outputs!) +**Custom shift register boards** can achieve extreme parallel output: +- **ESP32DEV**: [120-output virtual driver](https://github.com/hpwit/I2SClocklessVirtualLedDriver) +- **ESP32-S3**: [120-output S3 driver](https://github.com/hpwit/I2SClockLessLedVirtualDriveresp32s3) + +### Parallel WS2812 Drivers + +FastLED supports several drivers for parallel WS2812 output. + +#### Teensy + +The following drivers are available for Teensy boards: + +##### WS2812Serial driver + +The `WS2812Serial` driver leverages serial ports for LED data transmission on Teensy boards. + +###### Usage +To use this driver, you must define `USE_WS2812SERIAL` before including the FastLED header. + +```cpp +#define USE_WS2812SERIAL +#include +``` + +Then, use `WS2812SERIAL` as the chipset type in your `addLeds` call. The data pin is not used for this driver. + +```cpp +FastLED.addLeds(leds, NUM_LEDS); +``` + +###### Supported Pins & Serial Ports + +| Port | Teensy LC | Teensy 3.2 | Teensy 3.5 | Teensy 3.6 | Teensy 4.0 | Teensy 4.1 | +| :------ | :---------: | :--------: | :--------: | :--------: | :--------: | :--------: | +| Serial1 | 1, 4, 5, 24 | 1, 5 | 1, 5, 26 | 1, 5, 26 | 1 | 1, 53 | +| Serial2 | | 10, 31 | 10 | 10 | 8 | 8 | +| Serial3 | | 8 | 8 | 8 | 14 | 14 | +| Serial4 | | | 32 | 32 | 17 | 17 | +| Serial5 | | | 33 | 33 | 20, 39 | 20, 47 | +| Serial6 | | | 48 | | 24 | 24 | +| Serial7 | | | | | 29 | 29 | +| Serial8 | | | | | | 35 | + +##### ObjectFLED Driver + +The `ObjectFLED` driver is an advanced parallel output driver specifically optimized for Teensy 4.0 and 4.1 boards when using WS2812 LEDs. It is designed to provide high-performance, multi-pin output. + +By default, FastLED automatically uses the `ObjectFLED` driver for WS2812 LEDs on Teensy 4.0/4.1 boards. + +###### Disabling ObjectFLED (Reverting to Legacy Driver) +If you encounter compatibility issues or wish to use the standard clockless driver instead of `ObjectFLED`, you can disable it by defining `FASTLED_NOT_USES_OBJECTFLED` before including the FastLED header: + +```cpp +#define FASTLED_NOT_USES_OBJECTFLED +#include +``` + +###### Re-enabling ObjectFLED (Explicitly) +While `ObjectFLED` is the default for Teensy 4.0/4.1, you can explicitly enable it (though not strictly necessary) using: + +```cpp +#define FASTLED_USES_OBJECTFLED +#include +``` + +#### ESP32 + +The following drivers are available for ESP32 boards: + +##### ESP32-S3 I2S Driver + +The `ESP32-S3 I2S` driver leverages the I2S peripheral for high-performance parallel WS2812 output on ESP32-S3 boards. This driver is a dedicated clockless implementation. + +###### Usage +To use this driver, you must define `FASTLED_USES_ESP32S3_I2S` before including the FastLED header. + +```cpp +#define FASTLED_USES_ESP32S3_I2S +#include +``` + +Then, use `WS2812` as the chipset type in your `addLeds` call. The data pin will be configured by the I2S driver. + +```cpp +FastLED.addLeds(leds, NUM_LEDS); +``` + +**Note:** This driver requires a compatible Arduino-ESP32 core/IDF. + +##### Generic ESP32 I2S Driver + +The generic `ESP32 I2S` driver provides parallel WS2812 output for various ESP32 boards (e.g., ESP32-DevKitC). It uses the I2S peripheral for efficient data transmission. + +###### Usage +To use this driver, you must define `FASTLED_ESP32_I2S` before including the FastLED header. + +```cpp +#define FASTLED_ESP32_I2S +#include +``` + +Then, use `WS2812` as the chipset type in your `addLeds` call. The data pin will be configured by the I2S driver. + +```cpp +FastLED.addLeds(leds, NUM_LEDS); +``` + +###### DMA Buffer Configuration +For improved resilience under interrupt load (e.g., Wi-Fi activity), you can increase the number of I2S DMA buffers by defining `FASTLED_ESP32_I2S_NUM_DMA_BUFFERS`. A value of `4` is often recommended. + +```cpp +#define FASTLED_ESP32_I2S_NUM_DMA_BUFFERS 4 +``` + +**Note:** All I2S lanes must share the same chipset/timings. If per-lane timing differs, consider using the RMT driver instead. + +### Supported LED Chipsets + +FastLED supports virtually every LED chipset available: + +| **Clockless (3-wire)** | **SPI-based (4-wire)** | **Specialty** | +|------------------------|------------------------|---------------| +| **WS281x Family**: WS2811, WS2812 (NeoPixel), WS2812-V5B, WS2815 | **APA102 / DotStars**: Including HD107s (40MHz turbo) | **SmartMatrix Panels** | +| **TM180x Series**: TM1809/4, TM1803 | **High-Speed SPI**: LPD8806, WS2801, SM16716 | **DMX Output** | +| **Other 3-wire**: UCS1903, GW6205, SM16824E | **APA102HD**: Driver-level gamma correction | **P9813 Total Control** | + +**RGBW Support**: WS2816 and other white-channel LED strips • **Overclocking**: WS2812 up to 70% speed boost + +More details: [Chipset Reference Wiki](https://github.com/FastLED/FastLED/wiki/Chipset-reference) + +### APA102 High Definition Mode +```cpp +#define LED_TYPE APA102HD // Enables hardware gamma correction +void setup() { + FastLED.addLeds(leds, NUM_LEDS); +} +``` + +![APA102HD Comparison](https://github.com/user-attachments/assets/999e68ce-454f-4f15-9590-a8d2e8d47a22) + +Read more: [APA102 HD Documentation](APA102.md) • [Rust Implementation](https://docs.rs/apa102-spi/latest/apa102_spi/) + +## 🛠️ Development & Contributing + +[![clone and compile](https://github.com/FastLED/FastLED/actions/workflows/build_clone_and_compile.yml/badge.svg)](https://github.com/FastLED/FastLED/actions/workflows/build_clone_and_compile.yml) + +**Ready to contribute?** FastLED welcomes code contributions, platform testing, documentation improvements, and community support. + +
+🔧 Development Setup & Contributing Guide (Click to expand) + +### Quick Development Setup +Zero pain setup - can be done from command line in seconds with `uv` or `python`: + +![FastLED Development Setup](https://github.com/user-attachments/assets/f409120e-be6f-4158-81b5-d4bf935833b2) + +**Steps:** +1. Fork the repository on GitHub +2. Clone your fork locally: `git clone https://github.com/yourusername/FastLED.git` +3. Test compilation: `bash compile` (select your target platform) +4. Make your changes and test +5. Push to your fork and create a pull request + +**See our detailed [Contributing Guide](CONTRIBUTING.md) for more information.** + +### Testing Other Devices +Use the `compile` script to test on 20+ platforms: +- Teensy series, ESP32 variants, Arduino family, ARM boards, and more +- Automated testing ensures compatibility across all supported platforms + +### How to Help + +| **📋 How to Help** | **🔗 Resources** | +|-------------------|------------------| +| **Code contributions**: Bug fixes, new features, optimizations | [Contributing Guide](CONTRIBUTING.md) | +| **Platform support**: Help test on new/existing platforms | [Platform Testing](compile) | +| **Documentation**: Improve examples, fix typos, add tutorials | [Documentation](http://fastled.io/docs) | +| **Community**: Answer questions on Reddit, report issues | [r/FastLED](https://reddit.com/r/FastLED) | + +**Platform Porting**: Information on porting FastLED to new platforms: [PORTING.md](PORTING.md) + +### About FastLED + +**What's in the name?** Originally "FastSPI_LED" focused on high-speed SPI, but evolved to support all LED types and became "FastLED" - everything fast, for LEDs. + +**Official Site**: [fastled.io](http://fastled.io) - documentation, issues, and news + +
+ +--- + +## Daniel Garcia, Founder of FastLED + +In Memory of Daniel Garcia +Daniel Garcia, the brilliant founder of FastLED, tragically passed away in September 2019 in the Conception dive boat fire alongside his partner, Yulia. This heartbreaking loss was felt deeply by the maker and developer community, where Daniel's contributions had left an indelible mark. + +Daniel was more than just a talented programmer; he was a passionate innovator who transformed the way creators interacted with LED technology. His work on FastLED brought high-performance LED control to countless projects, empowering developers to craft breathtaking installations. + +In his personal life, Daniel was known for his kindness and creativity. His pride in FastLED and the vibrant community it fostered was a testament to his dedication to open-source development and his commitment to helping others bring light into the world. + +While Daniel is no longer with us, his legacy continues through the FastLED library and the countless makers who use it. The community he built serves as a living tribute to his ingenuity, generosity, and the joy he found in sharing his work with the world. + +## About the Current Contributor + +Zach Vorhies, the current main contributor to FastLED, briefly worked with Dan in 2014 in San Francisco and was an avid user of the FastLED library for over 13 years. After Daniel Garcia’s untimely passing, Zach stepped up to ensure FastLED’s continued growth and development. + +Zach has this to say about FastLED: + +*"The true power of FastLED lies in its ability to transform programmers into LED artists. Free space becomes their canvas; bending light is their medium. FastLED is a collective effort by programmers who want to manifest the world that science fiction writers promised us. -- To contribute code to FastLED is to leave behind a piece of something immortal."* + + +--- + +**💫 In memory of Daniel Garcia and the vision that transforms programmers into LED artists** + +*To stay updated on the latest feature releases, please click the `Watch` button in the upper right* +"" diff --git a/v1.5/lib/FastLED/RELEASE.md b/v1.5/lib/FastLED/RELEASE.md new file mode 100644 index 0000000..a2e0ab9 --- /dev/null +++ b/v1.5/lib/FastLED/RELEASE.md @@ -0,0 +1,39 @@ +# FastLED Release howto + +*Pushing a fastled release, the short version, last updated May 2024* + +## Example + +https://github.com/FastLED/FastLED/commit/4444758ffaf853ba4f8deb973532548c9c1ee231 + +## How to + +Edit these files to update the version number + * library.json + * library.properties + * src/FastLED.h + * docs/Doxyfile + * RELEASE.md + * This file: update instructions with the current release. + + +Edit this file with release notes and version number. + * release_notes.md + +Release notes should list highlight changes (not necessarily all minor bug fixes) and thank people for their help. + +Git commands to commit and tag release' +```bash +$ git commit -am "Rev 3.10.3" +$ git tag 3.10.3 master +$ git push +$ git push origin 3.10.3 +``` + +Then use the GitHub UI to make a new “Release”: + +https://github.com/FastLED/FastLED/releases/new + +Announce new version on subreddit, highlighting major changes and thanking people for helping. + +That’s it. diff --git a/v1.5/lib/FastLED/RMT.md b/v1.5/lib/FastLED/RMT.md new file mode 100644 index 0000000..8edf6ad --- /dev/null +++ b/v1.5/lib/FastLED/RMT.md @@ -0,0 +1,806 @@ +Do a deep research on fastled's RMT5 driver and the persistance of flickering. + +The RMT4 driver is MORE resistant to flickering. The RMT5 driver is pretty bad. + +Cause: WIFI takes high prioritiy interrupts and starves the the RMT driver. + +Potention solutions: + +create an interrupt than level 3, but this requires us to create a asm shim to trampoline into the c function handler and do proper stack cleanup. + +ESP32 & ESp32c3 and esp32s3 might have different cores. ones an xtensa, whats the other? + +list them. + +Another solution is to try and ping pong the rmt buffer like we do on the rmt4. However we must have better documentation +to see if this is even possible. + +Investigate the documentation and list it here on the RMT device for idf 5 and higher. + +Then do a feasability status to see if the ping pong is p0ossible. + +These are the two solutions. Both need to be investigated. But your investigation into RMT5.md + + +## Deep Research results + +FastLED RMT5 Driver Flickering Investigation +Background: FastLED’s original ESP32 driver (“RMT4”) used Espressif’s low-level RMT APIs, with one RMT memory block per channel and built‐in double-buffering for non-PSRAM chips. The newer “RMT5” driver (used on ESP32-S3, C3, etc.) wraps Espressif’s led_strip component (IDF’s higher-level RMT LED driver) +GitHub +. Under heavy Wi‑Fi load, the RMT5 approach has proven much more prone to gaps: Wi‑Fi interrupts at high priority can delay the RMT ISR, causing the RMT buffer to run dry and LEDs to flicker +esp32.com +esp-idf-lib.readthedocs.io +. (Indeed, the esp-idf led_strip documentation warns that “simultaneous use with Wi‑Fi” causes RMT bugs +esp-idf-lib.readthedocs.io +.) +ESP32 Core Architectures +The ESP32 family spans multiple architectures: classic ESP32 and ESP32-S3 use Tensilica Xtensa cores, while newer chips like ESP32-C3/C6/H2 use 32-bit RISC‑V cores +en.wikipedia.org +. For example, the ESP32-S3 is a dual-core Xtensa LX7, whereas the ESP32-C3 is a single-core RISC‑V. This matters for RMT timing (the underlying CPU instruction timing differs), but the flicker issue itself arises on any core when Wi‑Fi interrupts interfere with the RMT ISR. +RMT Peripheral (IDF v5+) Documentation +Espressif’s IDF documentation describes the RMT as a hardware peripheral that transmits sequences of “symbols” via GPIO, optionally using DMA. Because the RMT memory block is limited, long data transfers are split in a ping-pong fashion: the encoder callback can be invoked multiple times to refill the buffer as it empties +docs.espressif.com +. The docs note that these encoders run in ISR context and must be IRAM-safe (so that cache-disabled delays do not postpone interrupts) +docs.espressif.com +docs.espressif.com +. In particular, IDF has a CONFIG_RMT_ISR_IRAM_SAFE option that ensures RMT ISRs can run even when flash cache is off, to avoid unpredictable timing when using ping-pong buffering +docs.espressif.com +. The RMT API also allows setting the ISR priority explicitly: in rmt_tx_channel_config_t, the field intr_priority chooses the interrupt level (default “0” picks a medium priority 1–3) or a user-specified priority +docs.espressif.com +. This means one can elevate the RMT interrupt to a higher level (4 or 5) than Wi‑Fi (which normally runs at a high but fixed level). +Flickering Issue – Interrupt Starvation +Users consistently report that with RMT5, enabling Wi‑Fi causes periodic color glitches. Under a light web server load, one user measured ~50 µs jitter in what should be a ~35 µs ISR interval +esp32.com +, due to Wi‑Fi activity stealing CPU time. When this jitter exceeds the tolerance of the WS2812 timing, the LED strip “runs out” of data and flickers +esp32.com +. In contrast, the older RMT4 code (especially when using two memory blocks) could often absorb such delays. Espressif’s community notes that any Wi‑Fi use with the RMT LED driver can trigger glitches, and one workaround is to run the RMT tasks on the second CPU core +esp-idf-lib.readthedocs.io +. In practice, however, the flicker appears because high-priority Wi‑Fi interrupts delay the RMT driver’s buffer refill callbacks. +Potential Solutions +Increase RMT ISR Priority: Configure the RMT channel to use a higher interrupt level than Wi‑Fi. In IDF, setting intr_priority=4 or 5 in rmt_tx_channel_config_t will raise the ISR above normal Wi‑Fi interrupts +docs.espressif.com +. This requires writing the RMT encoding callback in IRAM and may require an assembly shim if using priority 5. (On Xtensa, level‑5 interrupts cannot call C routines directly, so one writes a small ASM stub that calls into a safe C function.) The FastLED RMT code would need to initialize the RMT channel with a high intr_priority and mark its handler IRAM-safe (via CONFIG_RMT_ISR_IRAM_SAFE) +docs.espressif.com +. A community example confirms that raising to level‑5 can eliminate most Wi‑Fi jitter, but notes you must modify the ISR entry (often with inline ASM) to avoid stack/ISR constraints +esp32.com +. +Ping-Pong (Double-Buffer) Mode: Use two RMT memory blocks per channel so the peripheral can alternate between them, thus buffering extra data. The IDF RMT encoder model explicitly supports ping‑pong buffering: the encoder callback is invoked each time a block fills, allowing the next symbols to be written +docs.espressif.com +. In FastLED, one enables this by allocating 2 blocks (e.g. setting mem_block_symbols to twice the usual) or enabling MEM_BLOCK_NUM=2. In practice, simply doubling the memory (using two blocks) absorbs Wi‑Fi-induced gaps: a forum user found that switching from 1 to 2 blocks removed all LED glitches by covering up ~120 µs of jitter +esp32.com +. The FastLED release notes even mention this behavior: on non-DMA chips (ESP32, C3, etc.), the RMT5 driver is configured to “double its memory” (i.e. use 2 blocks) by default, mimicking RMT4’s approach +GitHub +. Thus, ping-pong is technically supported. It requires verifying that the FastLED led_strip setup indeed uses two blocks (and if not, forcing it via FASTLED_RMT5_RECYCLE=0 or similar flags). +Feasibility and Status +Both strategies have merit. High-Priority ISR: requires low-level tweaking (setting intr_priority and possibly writing an IRAM ISR shim). This could be complex (especially on Xtensa) but would ensure timely buffer refills. Espressif docs confirm the ability to change RMT interrupt level +docs.espressif.com + and their IRAM-safe option mitigates deferred ISRs +docs.espressif.com +. However, once set, the interrupt priority can’t be changed without deleting the channel +docs.espressif.com +, so it must be done at RMT init. Ping-Pong Buffering: is simpler to implement since it leverages the existing RMT mechanism. The IDF RMT hardware natively supports multi-block transfers +docs.espressif.com +, and FastLED’s own code (via the led_strip component) already allows mem_block_symbols setting. Doubling the blocks is a one-line config change and was empirically validated by users +esp32.com +. FastLED even defaults to 2 blocks on chips without external RAM +GitHub +. In summary, raising the interrupt level could almost eliminate flicker at its source, but is invasive; enabling double-buffering is much easier and likely sufficient to cover typical Wi‑Fi spikes. Sources: Espressif’s RMT documentation (IDF v5+) and community forums provide detailed guidance on interrupt priorities, IRAM-safe handlers, and ping-pong transfers +docs.espressif.com +docs.espressif.com +docs.espressif.com +. FastLED release notes and forum tests confirm that Wi‑Fi interrupts cause RMT underruns and that using two RMT blocks removes glitches +esp32.com +esp32.com +GitHub +. Core architectures are listed for completeness +en.wikipedia.org +. +Citations +GitHub +idf5_rmt.cpp + +https://github.com/chaosgoo/mcompass/blob/d9d37398995c8e2b1806a6c90af03a1b8bc1c47e/Firmware/lib/FastLED/src/platforms/esp/32/idf5_rmt.cpp#L70-L78 +Amount of jitter on LEVEL3 interrupt? - ESP32 Forum + +https://esp32.com/viewtopic.php?t=17206 + +led_strip - RMT-based driver for WS2812B/SK6812/APA106 LED strips — esp-idf-lib 1.0 documentation + +https://esp-idf-lib.readthedocs.io/en/latest/groups/led_strip.html + +ESP32 - Wikipedia + +https://en.wikipedia.org/wiki/ESP32 + +Remote Control Transceiver (RMT) - ESP32 - — ESP-IDF Programming Guide v5.0 documentation + +https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32/api-reference/peripherals/rmt.html + +Remote Control Transceiver (RMT) - ESP32 - — ESP-IDF Programming Guide v5.0 documentation + +https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32/api-reference/peripherals/rmt.html + +Remote Control Transceiver (RMT) - ESP32 - — ESP-IDF Programming Guide v5.0 documentation + +https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32/api-reference/peripherals/rmt.html + +Remote Control Transceiver (RMT) - ESP32 - — ESP-IDF Programming Guide v5.5.1 documentation + +https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/rmt.html +Amount of jitter on LEVEL3 interrupt? - ESP32 Forum + +https://esp32.com/viewtopic.php?t=17206 +Amount of jitter on LEVEL3 interrupt? - ESP32 Forum + +https://esp32.com/viewtopic.php?t=17206 +GitHub +release_notes.md + +https://github.com/FastLED/FastLED/blob/0c22d156836639119fd6d0dc84d3c42f8bca7e0f/release_notes.md#L42-L47 +All Sources + +## Audit Comments and Criticisms (2025-09-16) + +### Structural Issues + +1. **Incomplete Research Questions**: The initial questions (lines 1-23) remain unanswered in parts: + - The ASM shim solution for level 5 interrupts is mentioned but lacks implementation details + - Core architectures are listed but the impact on RMT timing differences isn't fully explored + +2. **Missing Concrete Implementation Path**: While two solutions are proposed (high-priority ISR and ping-pong buffering), there's no clear recommendation on which to pursue first or implementation timeline. + +3. **Documentation Gaps**: + - No code examples showing how to implement either solution in FastLED + - Missing comparison table of RMT4 vs RMT5 architectural differences + - No benchmarks showing actual flicker frequency/severity metrics + +### Technical Concerns + +4. **Incomplete Testing Coverage**: + - No mention of testing methodology to validate fixes + - Missing list of affected board configurations (which ESP32 variants + which IDF versions) + - No quantification of "pretty bad" flickering (frequency, duration, visual impact) + +5. **Risk Assessment Missing**: + - Level 5 interrupt solution needs warning about potential system stability impacts + - No discussion of power consumption implications of either solution + - Missing compatibility matrix with other FastLED features + +### Content Issues + +6. **Redundant Citations**: Multiple duplicate citations to the same ESP32 forum thread and documentation pages (lines 103-140) + +7. **Inconsistent Formatting**: + - Mix of bullet points and paragraph styles in solutions section + - Inconsistent use of technical terms (RMT4 vs "RMT4", Wi-Fi vs WiFi) + +### Missing Critical Information + +8. **User Impact Analysis**: + - No guidance on which use cases are most affected + - Missing workaround recommendations for users experiencing issues now + - No mention of whether this affects all LED types or specific protocols + +9. **Configuration Details**: + - The ping-pong solution mentions "FASTLED_RMT5_RECYCLE=0" but doesn't explain what this does + - No clear documentation of all relevant FastLED compile-time flags + +10. **Priority and Severity**: + - No clear statement on whether this is a critical bug or edge case + - Missing timeline for fix deployment + - No mention of whether this is a regression from previous versions + +### Recommendations for Next Steps + +1. **Immediate**: Test ping-pong buffering solution as it's simpler and less risky +2. **Short-term**: Create reproducible test case with measurable flicker metrics +3. **Medium-term**: Document all FastLED RMT configuration options in one place +4. **Long-term**: Consider abstracting RMT driver to allow runtime switching between implementations + +### New Issues Identified + +- **Thread Safety**: No discussion of thread safety when RMT ISR runs at different priority levels +- **Memory Usage**: No analysis of memory impact when doubling RMT blocks +- **Backwards Compatibility**: No mention of how fixes will affect existing code using RMT5 +- **Error Handling**: No discussion of graceful degradation when solutions fail + +--- + +## QEMU TESTING FRAMEWORK (2025-09-16) + +### Overview +This section outlines comprehensive test criteria for validating RMT interrupt solutions using QEMU emulation on ESP32-S3 and ESP32-C3 platforms. QEMU provides a controlled environment to reproduce Wi-Fi interference patterns and measure interrupt latency without physical hardware variations. + +### Test Infrastructure Requirements + +#### QEMU Setup Commands +```bash +# Install QEMU for ESP32 emulation +uv run ci/install-qemu.py + +# Run QEMU tests with ESP32-S3 (Xtensa) +uv run test.py --qemu esp32s3 + +# Run QEMU tests with ESP32-C3 (RISC-V) +uv run test.py --qemu esp32c3 + +# Skip QEMU installation if already present +FASTLED_QEMU_SKIP_INSTALL=true uv run test.py --qemu esp32s3 +``` + +#### Platform Coverage Matrix + +| Platform | Architecture | RMT Version | Max Official Priority | Max Custom Priority | Test Priority | +|----------|-------------|-------------|----------------------|-------------------|---------------| +| ESP32-S3 | Xtensa LX7 | RMT5 | 3 | 5 | High | +| ESP32-C3 | RISC-V | RMT5 | 3 | 7 | High | +| ESP32-C6 | RISC-V | RMT5 | 3 | 7 | Medium | +| ESP32 | Xtensa LX6 | RMT4 | N/A | N/A | Reference | + +### Test Scenarios + +#### 1. Baseline RMT Timing Tests +**Objective**: Establish baseline RMT performance without interference + +**Test Cases**: +- **T1.1**: RMT5 default priority (level 3) LED refresh timing +- **T1.2**: Memory buffer utilization patterns (single vs double block) +- **T1.3**: WS2812B timing compliance under no-load conditions +- **T1.4**: Multiple channel simultaneous operation + +**Success Criteria**: +- WS2812B timing within ±150ns tolerance +- Zero dropped frames for 300 LED array +- Consistent ~35µs refresh intervals +- No interrupt overruns in baseline conditions + +**QEMU Implementation**: +```cpp +// Test harness in tests/qemu_rmt_baseline.cpp +void test_rmt_baseline_timing() { + // Configure RMT channel with default priority 3 + rmt_channel_config_t config = { + .gpio_num = TEST_GPIO_PIN, + .clk_div = 2, + .mem_block_num = 1, // Single block baseline + .intr_priority = 3 // Official maximum + }; + + // Measure timing consistency across 1000 refresh cycles + for (int i = 0; i < 1000; i++) { + uint64_t start = esp_timer_get_time(); + fastled_rmt_refresh_leds(test_pattern, 300); + uint64_t elapsed = esp_timer_get_time() - start; + + // Verify timing bounds + assert(elapsed >= 34500 && elapsed <= 35500); // ±500µs tolerance + } +} +``` + +#### 2. Wi-Fi Interference Simulation +**Objective**: Reproduce the Wi-Fi starvation issue that causes RMT5 flickering + +**Test Cases**: +- **T2.1**: Simulated Wi-Fi interrupt bursts at level 4 priority +- **T2.2**: Sustained Wi-Fi traffic during LED refresh cycles +- **T2.3**: Timing jitter measurement under interference +- **T2.4**: Buffer underrun detection and quantification + +**QEMU Wi-Fi Simulation**: +```cpp +// Simulate Wi-Fi interrupt load in QEMU +void simulate_wifi_interference() { + // Install level 4 interrupt to simulate Wi-Fi + esp_intr_alloc(ETS_WIFI_MAC_INTR_SOURCE, + ESP_INTR_FLAG_LEVEL4 | ESP_INTR_FLAG_IRAM, + wifi_interference_isr, NULL, &wifi_handle); + + // Generate periodic 40-50µs interrupt bursts + esp_timer_create(&wifi_timer_config, &wifi_timer); + esp_timer_start_periodic(wifi_timer, 100); // 100µs period +} + +void IRAM_ATTR wifi_interference_isr(void* arg) { + // Simulate 40µs Wi-Fi processing time + uint64_t start = esp_timer_get_time(); + while ((esp_timer_get_time() - start) < 40) { + // Busy wait to consume CPU time + __asm__ volatile ("nop"); + } +} +``` + +**Success Criteria for Problem Reproduction**: +- Measured jitter > ±10µs with Wi-Fi simulation active +- Buffer underruns detected in RMT interrupt handler +- Visual flicker correlation with Wi-Fi interrupt timing + +#### 3. High-Priority Interrupt Solution Validation +**Objective**: Test level 4/5 interrupt priority solutions + +**Test Cases**: +- **T3.1**: ESP32-S3 level 4 interrupt implementation (experimental) +- **T3.2**: ESP32-S3 level 5 interrupt with assembly shim +- **T3.3**: ESP32-C3 level 4-7 interrupt testing (RISC-V advantage) +- **T3.4**: Interrupt latency measurement and comparison + +**Implementation Reference**: `src/platforms/esp/32/interrupts/esp32_s3.hpp:149-161` + +**Test Implementation**: +```cpp +// Test level 5 interrupt solution (ESP32-S3 only) +void test_level5_interrupt_solution() { + // Use custom high-priority RMT initialization + esp_err_t err = fastled_esp32s3_rmt_init_custom( + RMT_CHANNEL_0, + TEST_GPIO_PIN, + 40000000, // 40MHz resolution + 128, // Single block + 5 // Level 5 priority - EXPERIMENTAL + ); + + assert(err == ESP_OK); + + // Activate Wi-Fi interference simulation + simulate_wifi_interference(); + + // Measure timing stability with level 5 priority + for (int i = 0; i < 1000; i++) { + uint64_t start = esp_timer_get_time(); + fastled_rmt_refresh_leds(test_pattern, 300); + uint64_t elapsed = esp_timer_get_time() - start; + + // Level 5 should maintain tight timing despite Wi-Fi + assert(elapsed >= 34800 && elapsed <= 35200); // ±200µs tolerance + } +} +``` + +#### 4. Double-Buffer Solution Validation +**Objective**: Test ping-pong buffering approaches (limited by LED strip API) + +**Test Cases**: +- **T4.1**: Memory block doubling via `mem_block_symbols` parameter +- **T4.2**: Custom low-level RMT implementation with true ping-pong +- **T4.3**: Buffer refill timing analysis +- **T4.4**: Comparison with RMT4 dual-buffer behavior + +**Critical Finding**: LED strip API limitation prevents true ping-pong (`RMT.md:254-258`) + +**Test Implementation**: +```cpp +// Test doubled memory blocks (not true ping-pong) +void test_doubled_memory_blocks() { + rmt_channel_config_t config = { + .gpio_num = TEST_GPIO_PIN, + .clk_div = 2, + .mem_block_num = 2, // Double the memory + .intr_priority = 3 + }; + + // This creates larger buffer, NOT ping-pong + // Test if larger buffer helps with Wi-Fi jitter + simulate_wifi_interference(); + + // Measure improvement (if any) + uint32_t underrun_count = 0; + for (int i = 0; i < 1000; i++) { + if (fastled_rmt_refresh_with_underrun_detection(test_pattern, 300)) { + underrun_count++; + } + } + + // Expect fewer underruns than single block, but not elimination + printf("Double block underruns: %lu (should be < single block)\n", underrun_count); +} +``` + +#### 5. Cross-Architecture Comparison Tests +**Objective**: Compare RMT behavior between Xtensa and RISC-V cores + +**Test Cases**: +- **T5.1**: Identical test patterns on ESP32-S3 (Xtensa) vs ESP32-C3 (RISC-V) +- **T5.2**: Interrupt latency differences between architectures +- **T5.3**: Assembly shim requirement validation (Xtensa only) +- **T5.4**: Priority level effectiveness comparison + +**Architecture-Specific Considerations**: +- **Xtensa (ESP32-S3)**: Requires assembly shims for level 4+ (`esp32_s3.hpp:316-368`) +- **RISC-V (ESP32-C3)**: C handlers work at all priority levels (`esp32_c3_c6.hpp:180-194`) + +### Automated Test Execution + +#### Test Harness Integration +```bash +# Add to existing test framework +uv run test.py RMTInterruptValidation --qemu esp32s3 +uv run test.py RMTInterruptValidation --qemu esp32c3 + +# Specific test scenarios +uv run test.py RMTBaseline --qemu esp32s3 +uv run test.py RMTWiFiInterference --qemu esp32s3 +uv run test.py RMTHighPriority --qemu esp32s3 +uv run test.py RMTDoubleBuffer --qemu esp32s3 +``` + +#### Expected Test Results + +| Test Scenario | ESP32-S3 Expected | ESP32-C3 Expected | Pass/Fail Criteria | +|---------------|-------------------|-------------------|-------------------| +| T1.1 Baseline | ±200µs jitter | ±200µs jitter | PASS if within tolerance | +| T2.1 Wi-Fi Interference | ±10-50µs jitter | ±10-50µs jitter | PASS if problem reproduced | +| T3.1 Level 4 Priority | ±500µs improvement | ±500µs improvement | PASS if jitter reduced | +| T3.2 Level 5 Priority | ±1000µs improvement | N/A (max level 7) | PASS if jitter eliminated | +| T4.1 Double Buffer | Partial improvement | Partial improvement | PASS if some reduction | + +#### Performance Benchmarks + +**Target Metrics**: +- **Baseline RMT5**: 35±10µs refresh time (no Wi-Fi) +- **RMT5 + Wi-Fi**: 35±50µs refresh time (problem case) +- **Level 5 Priority**: 35±2µs refresh time (solution target) +- **Double Buffer**: 35±20µs refresh time (partial solution) + +### Test Data Collection + +#### Logging and Metrics +```cpp +// Timing data collection structure +typedef struct { + uint64_t timestamp; + uint32_t refresh_time_us; + uint32_t interrupt_latency_us; + uint8_t priority_level; + bool wifi_active; + bool buffer_underrun; +} rmt_timing_sample_t; + +// Collect 10,000 samples per test scenario +#define TIMING_SAMPLES 10000 +rmt_timing_sample_t timing_data[TIMING_SAMPLES]; +``` + +#### Statistical Analysis +- **Mean refresh time** and standard deviation +- **95th percentile latency** measurements +- **Buffer underrun frequency** (occurrences per 1000 refreshes) +- **Jitter distribution** histograms + +### QEMU Limitations and Considerations + +#### QEMU Timing Accuracy +- QEMU provides relative timing comparison, not absolute accuracy +- Interrupt priority simulation may not perfectly match hardware +- Focus on comparative analysis between configurations + +#### Hardware Validation Requirements +- QEMU results must be validated on physical ESP32-S3/C3 hardware +- Real Wi-Fi interference patterns may differ from simulation +- Oscilloscope measurements required for WS2812B timing verification + +--- + +## FEASIBILITY AUDIT (2025-09-16) + +### Executive Summary +After analyzing the FastLED RMT5 driver implementation and ESP32 IDF documentation, **both proposed solutions are technically feasible**. The ping-pong buffering approach is recommended as the primary solution due to its simplicity and proven effectiveness. + +### Core Architecture Analysis + +#### ESP32 Variants and Impact +- **ESP32/ESP32-S3**: Xtensa LX6/LX7 cores - Support level 5 interrupts but require ASM shims +- **ESP32-C3/C6**: RISC-V cores - Different interrupt architecture but same priority levels +- **Impact on RMT**: Core architecture affects interrupt entry overhead (~10-15% difference) but doesn't fundamentally change the Wi-Fi starvation issue + +### Solution 1: High-Priority Interrupt (Level 5) + +#### Current State +- FastLED RMT5 currently uses default priority 3 (found in `strip_rmt.cpp:31`) +- Wi-Fi typically runs at priority 4, causing starvation +- Infrastructure exists: `interrupt_priority` parameter already plumbed through (`strip_rmt.h:23`) + +#### Implementation Requirements +1. **Xtensa ASM Shim** (ESP32/S3): + ```asm + ; Required for level 5 interrupts on Xtensa + ; Must save/restore registers manually + ; Call C handler with proper stack alignment + ``` +2. **IRAM Safety**: All ISR code must be marked `IRAM_ATTR` +3. **Configuration**: Set `CONFIG_RMT_ISR_IRAM_SAFE=y` in IDF + +#### Feasibility: **MEDIUM COMPLEXITY** +- ✅ Technically possible - IDF supports priority levels 1-5 +- ✅ Infrastructure partially exists +- ⚠️ Requires platform-specific ASM code +- ⚠️ Risk of system instability if not done correctly +- ⚠️ Cannot change priority after initialization + +### Solution 2: Ping-Pong Buffering (Double Memory Blocks) + +#### Current State +- RMT5 uses ESP-IDF's `led_strip` API which abstracts away direct RMT control +- `mem_block_symbols` parameter increases buffer size but **NOT true ping-pong** +- RMT4 had direct access to RMT hardware registers for manual buffer switching +- LED strip API handles encoding internally, no access to `fillNext()` mechanism + +#### Critical Issue: **LED Strip API Limitation** +The `led_strip` API in IDF v5 doesn't expose ping-pong buffer control: +- RMT4: Direct hardware access, manual buffer refill via `fillNext()` interrupt +- RMT5: High-level API, encoder runs in ISR but no manual buffer control +- Increasing `mem_block_symbols` just makes a larger single buffer +- **This is NOT equivalent to RMT4's dual-buffer ping-pong** + +#### Implementation Challenges +1. **No Manual Buffer Control**: + ```cpp + // RMT4 had this in interrupt handler: + pController->fillNext(true); // Manually refill next buffer half + + // RMT5 LED strip API has no equivalent + led_strip_refresh_async(); // Fire and forget, no buffer control + ``` + +2. **Encoder Abstraction**: + - Encoder callback runs automatically in ISR context + - No way to manually trigger buffer refills + - Can't implement true ping-pong without bypassing LED strip API + +#### Feasibility: **COMPLEX - REQUIRES MAJOR REFACTOR** +- ❌ NOT a simple one-line change +- ❌ LED strip API doesn't support true ping-pong +- ⚠️ Would need to bypass LED strip API entirely +- ⚠️ Requires reimplementing low-level RMT control like RMT4 +- ⚠️ Loses benefits of IDF's maintained LED strip driver + +### Memory Impact Analysis + +#### Current Memory Usage +- **Without DMA**: 48-64 symbols × 4 bytes = 192-256 bytes per channel +- **With DMA**: 1024 symbols × 4 bytes = 4KB per channel + +#### With Double Buffering +- **Without DMA**: 384-512 bytes per channel (+192-256 bytes) +- **With DMA**: No change (already large) + +For typical 8-channel setup: +1.5-2KB total RAM usage + +### Testing Validation Required + +1. **Reproduce Issue**: + - Enable Wi-Fi AP mode + web server + - Drive 300+ LEDs on multiple pins + - Measure flicker frequency with oscilloscope + +2. **Validate Fix**: + - Test both solutions independently + - Measure interrupt latency improvement + - Verify no regressions on non-Wi-Fi use cases + +### Implementation Priority + +1. **Phase 1 (Immediate)**: Implement ping-pong buffering + - Low risk, high reward + - Can ship in patch release + +2. **Phase 2 (Future)**: Consider high-priority ISR for extreme cases + - Only if ping-pong proves insufficient + - Requires extensive platform testing + +### Configuration Recommendations + +Add these defines to allow user tuning: +```cpp +#define FASTLED_RMT5_MEM_BLOCKS_MULTIPLIER 2 // Double buffer by default +#define FASTLED_RMT5_INTERRUPT_PRIORITY 3 // Keep default, allow override +#define FASTLED_RMT5_FORCE_IRAM_SAFE 0 // Optional IRAM safety +``` + +### Solution 3: Bypass LED Strip API (Use Low-Level RMT) + +#### Approach +- Abandon `led_strip` API entirely +- Use IDF v5's low-level RMT TX API directly +- Implement custom encoder with manual buffer management +- Port RMT4's `fillNext()` logic to IDF v5 RMT API + +#### Implementation Requirements +```cpp +// Use raw RMT API instead of led_strip +rmt_new_tx_channel(&config, &channel); +rmt_new_bytes_encoder(&encoder_config, &encoder); +// Implement custom ISR with buffer refill logic +``` + +#### Feasibility: **MODERATE - MOST VIABLE** +- ✅ Gives full control over buffer management +- ✅ Can implement true ping-pong like RMT4 +- ⚠️ Requires significant code rewrite +- ⚠️ Must maintain custom encoder implementation +- ✅ Proven approach (RMT4 uses this successfully) + +### Revised Conclusion + +**The initial assessment was incorrect.** The LED strip API abstraction in RMT5 fundamentally prevents implementing true ping-pong buffering as a "simple one-line change." + +**Recommended approach:** +1. **Immediate**: Increase interrupt priority (Solution 1) - simplest to implement +2. **Short-term**: Bypass LED strip API and use low-level RMT (Solution 3) - most effective +3. **Long-term**: Work with Espressif to add ping-pong support to LED strip API + +The flickering issue is real and significant, but the solution requires more substantial changes than initially thought. Simply increasing `mem_block_symbols` only creates a larger buffer, not the dual-buffer ping-pong mechanism that made RMT4 resistant to Wi-Fi interference. + +--- + +## INTERRUPT IMPLEMENTATION ANALYSIS (2025-09-16) + +### Overview of Current FastLED Interrupt Infrastructure + +Based on analysis of the interrupt header files (`src/platforms/esp/32/interrupts/`), FastLED has already implemented comprehensive infrastructure for high-priority interrupt handling on both Xtensa (ESP32-S3) and RISC-V (ESP32-C3/C6) architectures. + +### Architecture-Specific Implementations + +#### ESP32-S3 (Xtensa LX7) - `esp32_s3.hpp` + +**Key Capabilities**: +- **Official Support**: Priority levels 1-3 via `rmt_tx_channel_config_t` +- **Experimental Support**: Priority levels 4-5 with custom assembly shims +- **Assembly Trampoline Macro**: `FASTLED_ESP_XTENSA_ASM_INTERRUPT_TRAMPOLINE` (`esp32_s3.hpp:316-368`) +- **Interrupt Installation Functions**: Pre-built helpers for level 3-5 priority installation + +**Critical Requirements for Level 4-5 Implementation**: +```cpp +// Level 5 interrupt requires assembly shim (lines 149-161) +extern void xt_highint5(void); +void IRAM_ATTR fastled_esp32s3_level5_handler(void); + +// Installation helper already available +esp_err_t fastled_esp32s3_install_level5_interrupt( + int source, void *arg, esp_intr_handle_t *handle); +``` + +**Build Configuration Required**: +- `CONFIG_RMT_ISR_IRAM_SAFE=y` +- Assembly `.S` files in build system +- `-mlongcalls` compiler flag + +#### ESP32-C3/C6 (RISC-V) - `esp32_c3_c6.hpp` + +**Key Advantages**: +- **Simplified Implementation**: C handlers work at any priority level (1-7) +- **No Assembly Required**: Standard `IRAM_ATTR` functions sufficient +- **PLIC Integration**: Platform-Level Interrupt Controller handles arbitration +- **Maximum Priority**: Level 7 (vs. Xtensa level 5) + +**Implementation Benefits**: +```cpp +// RISC-V can use C handlers directly at high priority +void IRAM_ATTR fastled_riscv_experimental_handler(void *arg); + +// Simpler installation (lines 286-292) +esp_err_t fastled_riscv_install_interrupt( + int source, int priority, void (*handler)(void *), + void *arg, esp_intr_handle_t *handle); +``` + +### Testing Infrastructure Integration + +#### Current Test Framework Compatibility + +The interrupt implementations are designed to integrate with the existing QEMU test infrastructure: + +```bash +# ESP32-S3 Xtensa testing +uv run test.py --qemu esp32s3 + +# ESP32-C3 RISC-V testing +uv run test.py --qemu esp32c3 +``` + +#### Proposed Test Case Implementation + +Based on the interrupt header analysis, the test framework should include: + +1. **Level 3 Baseline Tests** (Official FastLED support) + - Verify standard `rmt_tx_channel_config_t` integration + - Test maximum officially supported priority + - Validate against existing FastLED RMT driver + +2. **Level 4-5 Experimental Tests** (ESP32-S3) + - Test assembly shim functionality (`FASTLED_ESP_XTENSA_ASM_INTERRUPT_TRAMPOLINE`) + - Validate custom RMT initialization bypassing official driver + - Measure Wi-Fi interference immunity + +3. **Level 4-7 RISC-V Tests** (ESP32-C3/C6) + - Test C handler high-priority interrupts + - Compare RISC-V vs Xtensa interrupt latency + - Validate PLIC priority arbitration + +### Implementation Readiness Assessment + +#### What's Already Available + +**✅ Complete Infrastructure**: +- Assembly trampoline macros for Xtensa high-priority interrupts +- C handler support for RISC-V high-priority interrupts +- Interrupt installation helper functions for both architectures +- Build configuration documentation +- IRAM safety considerations documented + +**✅ Test Integration Points**: +- QEMU support for both ESP32-S3 and ESP32-C3 +- Existing test framework can be extended +- Performance measurement infrastructure available + +#### What Needs Implementation + +**🔨 RMT Driver Integration**: +- `fastled_esp32s3_rmt_init_custom()` function implementation +- `fastled_riscv_rmt_init_experimental()` function implementation +- Bridge between interrupt infrastructure and RMT hardware configuration + +**🔨 Custom RMT Control**: +- Bypass LED strip API for true ping-pong buffering +- Low-level RMT register manipulation +- Custom encoder implementation for buffer management + +### Recommended Implementation Sequence + +#### Phase 1: High-Priority Interrupt (Ready to Implement) +```cpp +// ESP32-S3 Level 5 Implementation - infrastructure exists +void IRAM_ATTR rmt_level5_handler(void *arg) { + // Custom RMT buffer refill logic + // Bypass official LED strip API +} + +// Use existing trampoline macro +FASTLED_ESP_XTENSA_ASM_INTERRUPT_TRAMPOLINE(rmt_level5_isr, rmt_level5_handler) + +// Install with existing helper +fastled_esp32s3_install_level5_interrupt(ETS_RMT_INTR_SOURCE, NULL, &handle); +``` + +#### Phase 2: Custom RMT Implementation +```cpp +// Bypass led_strip API entirely - use raw RMT API +rmt_new_tx_channel(&channel_config, &channel); +rmt_new_bytes_encoder(&encoder_config, &encoder); + +// Implement custom buffer management +// Port RMT4 fillNext() logic to IDF v5 RMT API +``` + +#### Phase 3: QEMU Validation +- Implement test cases in `tests/qemu_rmt_interrupts.cpp` +- Validate both Xtensa and RISC-V implementations +- Performance comparison and regression testing + +### Risk Assessment + +**Low Risk (High-Priority Interrupts)**: +- Infrastructure already exists and documented +- Clear implementation path with existing helpers +- QEMU testing available for validation + +**Medium Risk (Custom RMT Implementation)**: +- Requires bypassing official LED strip API +- Must maintain custom encoder implementation +- Compatibility with future IDF versions uncertain + +**Critical Success Factors**: +1. **IRAM Safety**: All high-priority code must be IRAM-resident +2. **Timing Validation**: QEMU + hardware oscilloscope verification +3. **Regression Testing**: Ensure no impact on normal priority operation +4. **Documentation**: Clear usage guidelines for different priority levels + +### Conclusion + +The interrupt infrastructure analysis reveals that FastLED already has **comprehensive, production-ready infrastructure** for high-priority interrupt implementation on both Xtensa and RISC-V architectures. The main implementation work required is: + +1. **Integration layer** between interrupt infrastructure and RMT hardware +2. **Custom RMT driver** that bypasses LED strip API limitations +3. **Test validation** using existing QEMU framework + +This significantly reduces the implementation complexity compared to the initial assessment. The high-priority interrupt solution is **immediately implementable** using existing infrastructure, while the ping-pong buffering solution requires the more substantial custom RMT driver development. \ No newline at end of file diff --git a/v1.5/lib/FastLED/TESTCASE_INSTRUCTIONS.md b/v1.5/lib/FastLED/TESTCASE_INSTRUCTIONS.md new file mode 100644 index 0000000..94b6d7b --- /dev/null +++ b/v1.5/lib/FastLED/TESTCASE_INSTRUCTIONS.md @@ -0,0 +1,261 @@ +# FastLED TEST_CASE Execution Guide + +## Overview + +FastLED uses the **doctest** framework for C++ unit testing. Tests are organized in files named `test_*.cpp` in the `tests/` directory. Each file can contain multiple `TEST_CASE` macros that define individual test functions. + +## Test Structure + +- **Test Files**: Located in `tests/test_*.cpp` (e.g., `test_algorithm.cpp`, `test_easing.cpp`) +- **TEST_CASEs**: Individual test functions defined with `TEST_CASE("name")` macro +- **SUBCASEs**: Nested test sections within TEST_CASEs using `SUBCASE("name")` +- **Assertions**: `CHECK()`, `CHECK_EQ()`, `CHECK_LT()`, etc. for validation + +## Quick Start + +### Using the MCP Server (Recommended) + +1. **Start the MCP server:** + ```bash + uv run mcp_server.py + ``` + +2. **Available MCP tools:** + - `run_tests`: Run tests with various options + - `list_test_cases`: List available TEST_CASEs + - `test_instructions`: Show detailed instructions + +### Command Line Usage + +1. **Run all tests:** + ```bash + uv run test.py + # or using the user rule: + bash test + ``` + +2. **Run only C++ tests:** + ```bash + uv run test.py --cpp + ``` + +3. **Run specific test file:** + ```bash + uv run test.py --cpp algorithm + # This runs test_algorithm.cpp + ``` + +## Running Specific TEST_CASEs + +### Method 1: Direct Executable (Most Precise) + +First, compile the tests, then run the specific executable with doctest filtering: + +```bash +# 1. Compile the test (if not already compiled) +uv run test.py --cpp algorithm + +# 2. Run specific TEST_CASE +./tests/.build/bin/test_algorithm --test-case="reverse an int list" + +# 3. Run with verbose output +./tests/.build/bin/test_algorithm --test-case="reverse an int list" --verbose +``` + +### Method 2: Using MCP Server + +Use the `run_tests` tool with specific parameters: +- `test_type`: "specific" +- `specific_test`: "algorithm" (without test_ prefix) +- `test_case`: "reverse an int list" +- `verbose`: true (optional) + +### Method 3: List Available TEST_CASEs First + +```bash +# Using MCP list_test_cases tool, or manually: +grep -r "TEST_CASE" tests/test_algorithm.cpp +``` + +## Common Test Files and Their Purpose + +| File | Purpose | Example TEST_CASEs | +|------|---------|-------------------| +| `test_algorithm.cpp` | Algorithm utilities | "reverse an int list" | +| `test_easing.cpp` | Easing functions | "8-bit easing functions", "easeInOutQuad16" | +| `test_hsv16.cpp` | HSV color space | "HSV to RGB conversion", "HSV hue wraparound" | +| `test_math.cpp` | Mathematical functions | "sin32 accuracy", "random number generation" | +| `test_vector.cpp` | Vector container | "vector push_back", "vector iteration" | +| `test_fx.cpp` | Effects framework | "fx engine initialization" | +| `test_apa102_hd.cpp` | APA102 HD driver | "APA102 color mapping" | +| `test_bitset.cpp` | Bitset operations | "bitset operations" | + +## Example Workflows + +### Debug a Failing Test + +1. **List TEST_CASEs in a specific file:** + ```bash + # Using MCP server list_test_cases tool with test_file: "easing" + ``` + +2. **Run with verbose output:** + ```bash + uv run test.py --cpp easing --verbose + ``` + +3. **Run specific failing TEST_CASE:** + ```bash + ./tests/.build/bin/test_easing --test-case="8-bit easing functions" --verbose + ``` + +### Test Development Workflow + +1. **Clean build and run:** + ```bash + uv run test.py --cpp mytest --clean --verbose + ``` + +2. **Run only failed tests:** + ```bash + cd tests/.build && ctest --rerun-failed + ``` + +3. **Test with different compilers:** + ```bash + uv run test.py --cpp mytest --clang + ``` + +### Finding Specific Tests + +1. **Search for TEST_CASEs by pattern:** + ```bash + # Using MCP list_test_cases tool with search_pattern: "easing" + ``` + +2. **Manual search:** + ```bash + grep -r "TEST_CASE.*easing" tests/ + ``` + +## Doctest Command Line Options + +When running test executables directly, you can use these doctest options: + +- `--test-case=`: Run specific TEST_CASE +- `--test-case-exclude=`: Exclude specific TEST_CASE +- `--subcase=`: Run specific SUBCASE +- `--subcase-exclude=`: Exclude specific SUBCASE +- `--list-test-cases`: List all TEST_CASEs in the executable +- `--list-test-suites`: List all test suites +- `--verbose`: Show detailed output including successful assertions +- `--success`: Show successful assertions too +- `--no-colors`: Disable colored output +- `--force-colors`: Force colored output +- `--no-breaks`: Disable breaking into debugger +- `--no-skip`: Don't skip any tests + +### Examples: + +```bash +# List all TEST_CASEs in a test file +./tests/.build/bin/test_easing --list-test-cases + +# Run multiple TEST_CASEs with pattern +./tests/.build/bin/test_easing --test-case="*quad*" + +# Exclude specific TEST_CASEs +./tests/.build/bin/test_easing --test-case-exclude="*16-bit*" + +# Run with full verbose output +./tests/.build/bin/test_easing --success --verbose +``` + +## Understanding Test Output + +### Successful Test: +``` +[doctest] run with "--help" for options +=============================================================================== +TEST_CASE: reverse an int list +=============================================================================== +tests/test_algorithm.cpp:12 +all tests passed! +=============================================================================== +[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped +[doctest] assertions: 5 | 5 passed | 0 failed | +[doctest] Status: SUCCESS! +``` + +### Failed Test: +``` +=============================================================================== +TEST_CASE: reverse an int list +=============================================================================== +tests/test_algorithm.cpp:12 +tests/test_algorithm.cpp:20: ERROR: CHECK_EQ(vec[0], 6) is NOT correct! + values: CHECK_EQ(5, 6) + +=============================================================================== +[doctest] test cases: 1 | 0 passed | 1 failed | 0 skipped +[doctest] assertions: 5 | 4 passed | 1 failed | +[doctest] Status: FAILURE! +``` + +## Tips and Best Practices + +1. **Use `--verbose`** to see detailed test output and assertions +2. **Use `--clean`** when testing after significant code changes +3. **List TEST_CASEs first** to see what's available before running specific tests +4. **Individual TEST_CASE execution** is useful for debugging specific functionality +5. **Check test output carefully** - doctest provides detailed failure information with line numbers +6. **Use pattern matching** with `--test-case="*pattern*"` to run related tests +7. **Combine with debugger** - tests are compiled with debug symbols (`-g3`) + +## Environment Setup + +- Tests are compiled with Debug flags (`-g3`, `-O0`) for better debugging +- GDB integration available for crash analysis +- Static analysis warnings enabled as errors +- Cross-platform support (Linux, macOS, Windows) +- Uses zig/clang as the default compiler for consistency + +## Troubleshooting + +### Common Issues: + +1. **Test executable not found:** + ```bash + # Make sure tests are compiled first + uv run test.py --cpp + ``` + +2. **TEST_CASE name not found:** + ```bash + # List available TEST_CASEs + ./tests/.build/bin/test_ --list-test-cases + ``` + +3. **Compilation errors:** + ```bash + # Clean build + uv run test.py --cpp --clean + ``` + +4. **Permission denied on executable:** + ```bash + chmod +x tests/.build/bin/test_ + ``` + +## Integration with Development + +### Git Hooks +Consider adding a pre-commit hook to run relevant tests: +```bash +#!/bin/bash +# Run tests for changed files +uv run test.py --cpp +``` + +### IDE Integration +Most IDEs can be configured to run individual TEST_CASEs directly from the editor by configuring the test executable path and doctest arguments. \ No newline at end of file diff --git a/v1.5/lib/FastLED/TODO.md b/v1.5/lib/FastLED/TODO.md new file mode 100644 index 0000000..9c309cf --- /dev/null +++ b/v1.5/lib/FastLED/TODO.md @@ -0,0 +1,47 @@ +# Testing + + * REPO SYNC MODE: When there is a compiler error there isn't a hard failure. The compiler error needs to get propagate back to the top. + + + * Esp32 testing + * https://github.com/marketplace/actions/esp32-qemu-runner will run a sketch for X seconds and see's if it crashes + * There's specific tests we'd like to run with this including the WS2812 and APA102 tests to test the clockless and clocked drivers + +# Feature Enhancements + + [ ] Adafruit converter driver + [ ] NeoPixel converter driver + + * I2S driver for ESP32 WS2812 + * https://github.com/hpwit/I2SClocklessLedDriver + * Our copy is here: https://github.com/FastLED/FastLED/blob/master/src/platforms/esp/32/clockless_i2s_esp32.h + * S3: + * https://github.com/hpwit/I2SClockLessLedDriveresp32s3 + * Apparently, this driver allows MASSIVE parallelization for WS2812 + * Timing guide for reducing RMT frequency https://github.com/Makuna/NeoPixelBus/pull/795 + * ESp32 LED guide + * web: https://components.espressif.com/components/espressif/led_strip + * repo: https://github.com/espressif/idf-extra-components/tree/60c14263f3b69ac6e98ecae79beecbe5c18d5596/led_strip + * adafruit conversation on RMT progress: https://github.com/adafruit/Adafruit_NeoPixel/issues/375 + + + * MIT Licensed SdFat library + * https://github.com/greiman/SdFat + * YVes LittleFS implementation for ESP + * https://github.com/hpwit/ledOS/blob/main/src/fileSystem.h + + * NimBLE for Arduino + * https://github.com/h2zero/NimBLE-Arduino?tab=readme-ov-file + + * Arduino test compile + * https://github.com/hpwit/arduino-test-compile/blob/master/arduino-test-compile.sh + + +# Misc: + + * sutaburosu's guide to playing around with FastLED 4 + * https://github.com/sutaburosu/FastLED4-ESP32-playpen + + +# Animartrix: try using sinf, cosf and other found trip instead of trying to go +full double precision. diff --git a/v1.5/lib/FastLED/VIDEO.md b/v1.5/lib/FastLED/VIDEO.md new file mode 100644 index 0000000..871d03d --- /dev/null +++ b/v1.5/lib/FastLED/VIDEO.md @@ -0,0 +1,352 @@ +# Video Recording Feature for FastLED Web App + +## Implementation Status: ✅ COMPLETE - FULLY IMPLEMENTED + +**Last Updated**: September 18, 2025 +**Commit**: Refactored video recording code to fix bugs and improve reliability + +### Overview +A video recording feature has been implemented for the FastLED web application in `src/platforms/wasm/compiler/`. This feature allows users to capture both the canvas animations and audio output to create high-quality video recordings. + +## Features Implemented + +### 1. Recording Button UI +- **Location**: Upper right corner of the canvas +- **Visual States**: + - **Idle**: Gray circular button with a record icon (⚫) + - **Recording**: Red pulsing button with a stop icon (⬛) +- **Animation**: Smooth pulse effect when recording +- **Note**: Stdout button should be moved to upper left of canvas to avoid conflict with record button placement + +### 2. Video Capture Specifications +- **Default Format**: MP4 with H.264 video codec and AAC audio codec +- **Fallback Formats**: WebM with VP9/VP8 video codec and Opus audio codec (auto-detected based on browser support) +- **Video Bitrate**: 10 Mbps (high quality) +- **Audio Bitrate**: 128 kbps +- **Frame Rate**: 30 FPS default (configurable to 60 FPS) +- **Resolution**: Captures at canvas native resolution +- **Frame Logging**: Each frame insertion is logged to console (e.g., "frame 16") +- **Automatic File Extension**: Correct file extension (.mp4 or .webm) based on selected codec +- **Recording Statistics**: Logs frame count, duration, FPS, and file size upon completion +- **Improved Error Handling**: Graceful fallback when MediaRecorder is unavailable + +### 3. Audio Capture +- **Source**: Web Audio API AudioContext +- **Integration**: Automatically captures audio if AudioContext is available +- **MP3 Player Integration**: + - Captures audio from the existing MP3 player module + - Audio streams from MP3 playback are routed through the AudioContext + - Mixed audio (synthesized + MP3) is captured in the recording +- **Audio Mixing**: Combines all audio sources (synthesized sounds + MP3 playback) into a single stream +- **Fallback**: Records video-only if audio is unavailable +- **Sync**: Audio and video are automatically synchronized + +### 4. User Interaction +- **Start Recording**: Click the record button or press `Ctrl+R` / `Cmd+R` +- **Stop Recording**: Click the button again (now showing stop icon) +- **File Save**: Automatic download prompt when recording stops +- **Filename Format**: `fastled-recording-YYYY-MM-DD-HH-MM-SS.{mp4|webm}` (extension matches codec) +- **Graceful Degradation**: Record button automatically hidden if MediaRecorder unsupported + +## Frame Handling & Encoder Optimizations + +### Dropped Frame Detection & Recovery +The video recording system implements comprehensive dropped frame handling: + +#### Detection Methods +- **Frame Counter Monitoring**: Tracks expected vs. actual frame sequence +- **Timestamp Analysis**: Compares MediaRecorder timestamps with canvas refresh rate +- **Performance Observer**: Monitors frame presentation delays and skips + +#### Recovery Strategies +```javascript +// Dropped frame detection +let expectedFrameCount = 0; +let actualFrameCount = 0; +const frameDropThreshold = 2; // Allow 2 consecutive drops before intervention + +mediaRecorder.addEventListener('dataavailable', (event) => { + actualFrameCount++; + const timestamp = performance.now(); + const expectedTimestamp = startTime + (expectedFrameCount * (1000 / fps)); + + if (timestamp - expectedTimestamp > frameDropThreshold * (1000 / fps)) { + console.warn(`Dropped frames detected: expected ${expectedFrameCount}, got ${actualFrameCount}`); + // Trigger recovery actions + handleDroppedFrames(expectedFrameCount - actualFrameCount); + } + + console.log(`frame ${actualFrameCount}`); + expectedFrameCount++; +}); + +function handleDroppedFrames(droppedCount) { + // Option 1: Duplicate last frame to maintain timing + // Option 2: Reduce capture frame rate temporarily + // Option 3: Skip frame interpolation for smooth playback +} +``` + +### Duplicate Frame Optimization + +#### H.264 Encoder Optimizations +The H.264 encoder in MP4 format provides several optimizations for static or repetitive content: + +- **P-frames (Predicted frames)**: Encode only differences from previous frames +- **B-frames (Bidirectional frames)**: Reference both previous and future frames for maximum compression +- **Temporal Compression**: Automatically detects static regions and encodes efficiently +- **Rate Control**: Adjusts bitrate dynamically based on content complexity + +#### Implementation Details +```javascript +// Configure encoder for optimal duplicate frame handling +const encoderConfig = { + mimeType: 'video/mp4;codecs=h264,aac', + videoBitsPerSecond: 10000000, + audioBitsPerSecond: 128000, + // H.264 specific optimizations + keyFrameInterval: 30, // I-frame every 30 frames (1 second at 30fps) + frameSkipping: true, // Allow encoder to skip identical frames + contentHint: 'motion' // Optimize for animation content +}; + +// Monitor encoding efficiency +mediaRecorder.addEventListener('start', () => { + console.log('Encoder supports duplicate frame optimization: ', + mediaRecorder.mimeType.includes('h264')); +}); +``` + +#### Fallback Encoder Behavior +When MP4/H.264 is not available, the system falls back to WebM/VP9: + +- **VP9 Optimization**: Similar temporal compression but different algorithm +- **Opus Audio**: Efficient silence detection and compression +- **WebM Container**: Supports variable frame rates for dropped frame scenarios + +#### Performance Monitoring +```javascript +// Track encoding performance +const encodingStats = { + framesEncoded: 0, + framesDuplicated: 0, + bytesGenerated: 0, + compressionRatio: 0 +}; + +mediaRecorder.addEventListener('dataavailable', (event) => { + encodingStats.bytesGenerated += event.data.size; + encodingStats.framesEncoded++; + + // Estimate duplicate frame optimization + const expectedSize = (canvas.width * canvas.height * 3) / 8; // RGB to bytes + const actualSize = event.data.size; + const efficiency = 1 - (actualSize / expectedSize); + + if (efficiency > 0.8) { + encodingStats.framesDuplicated++; + console.log(`High compression frame ${encodingStats.framesEncoded} (${efficiency.toFixed(2)} efficiency)`); + } +}); +``` + +## Technical Implementation + +### Files Created/Modified + +1. **`modules/video_recorder.js`** (NEW) + - Core VideoRecorder class + - MediaRecorder API integration + - Canvas stream capture + - Audio context integration with MP3 player support + - Mixed audio capture (MP3 + synthesized sounds) + - File save functionality + +2. **`index.html`** (MODIFIED) + - Added record button HTML structure + - SVG icons for record/stop states + +3. **`index.css`** (MODIFIED) + - Record button styling + - Animation effects (pulse, hover) + - State transitions + +4. **`index.js`** (MODIFIED) + - Video recorder initialization + - Button event handling + - Keyboard shortcut support + - Global debug functions + +## API Documentation + +### MediaRecorder API Usage + +```javascript +// Canvas capture +canvas.captureStream(fps) // Creates MediaStream from canvas + +// Audio capture with MP3 mixing +audioContext.createMediaStreamDestination() // Creates audio destination node + +// MP3 player audio routing +mp3Player.connect(audioDestination) // Route MP3 audio to recording destination +synthesizer.connect(audioDestination) // Route synthesized audio to destination + +// Combined stream with mixed audio +new MediaStream([ + ...videoStream.getVideoTracks(), + ...audioDestination.stream.getAudioTracks() // Contains mixed audio from all sources +]) + +// Recording with mixed audio +mediaRecorder = new MediaRecorder(stream, { + mimeType: 'video/mp4;codecs=h264,aac', // Default: MP4 with H.264 video and AAC audio + videoBitsPerSecond: 10000000, + audioBitsPerSecond: 128000 // Captures all mixed audio sources +}) + +// Frame logging during recording +mediaRecorder.addEventListener('dataavailable', (event) => { + console.log(`frame ${++frameCounter}`); // Logs each frame insertion +}); +``` + +### Key Design Decisions + +1. **File Save Timing**: Files are saved AFTER recording stops (not when it starts) + - Ensures precise start/stop timing for A/V sync + - User sees save dialog after recording completes + - No filename prompt interrupts recording start + +2. **High Bitrate Choice**: 10 Mbps video bitrate + - Ensures high quality capture + - Suitable for 60 FPS content + - Can be compressed later if needed + +3. **MP4/H.264 Format**: Primary codec choice + - Universal browser support for high-quality video + - Good compression efficiency with AAC audio + - Supports both video and audio tracks + - WebM/VP9 fallback for browsers with limited MP4 support + +## Usage Instructions + +### For Users +1. Click the gray record button in the upper-right corner of the canvas +2. Button turns red and pulses to indicate recording +3. Perform your LED animation +4. Click the red button to stop recording +5. Browser will prompt to save the video file + +### For Developers + +#### Global Functions (Console Access) +```javascript +// Get recorder instance +window.getVideoRecorder() + +// Start recording programmatically +window.startVideoRecording() + +// Stop recording programmatically +window.stopVideoRecording() +``` + +#### Keyboard Shortcuts +- `Ctrl+R` / `Cmd+R`: Toggle recording on/off + +## Browser Compatibility + +- ✅ Chrome/Chromium (Full support) +- ✅ Firefox (Full support) +- ✅ Edge (Full support) +- ⚠️ Safari (Limited codec support, uses fallback) + +## Recent Bug Fixes & Improvements + +### September 18, 2025 Refactor +- **Fixed codec configuration**: Changed default from VP9 to H.264/MP4 for better compatibility +- **Improved audio routing**: Enhanced audio connection logic for more reliable audio capture +- **Dynamic file extensions**: Automatically uses correct extension (.mp4 or .webm) based on codec +- **Enhanced error handling**: Added graceful fallback when MediaRecorder API is unavailable +- **Better resource cleanup**: Improved disposal method to prevent memory leaks +- **Frame logging implementation**: Added proper frame counting and logging as documented +- **Recording statistics**: Added comprehensive logging of recording metrics (duration, FPS, file size) +- **Canvas content detection**: Improved blank canvas detection without interfering with actual content +- **UI positioning fix**: Moved stdout label to upper left to avoid conflicts with record button + +### Bug Fixes Addressed +1. **Inconsistent codec defaults** - Now prioritizes H.264/MP4 over VP9/WebM +2. **Audio connection failures** - Improved audio routing with better error handling +3. **Wrong file extensions** - Files now use correct extension matching the codec +4. **Missing frame logging** - Frame counter now properly logs each frame +5. **Memory leaks** - Enhanced cleanup in disposal and error scenarios +6. **Canvas interference** - Removed test pattern drawing that could interfere with content +7. **MediaRecorder detection** - Added proper feature detection and graceful degradation + +## Future Enhancements (Optional) + +- [ ] Settings menu for quality presets +- [ ] Frame rate selector (30/60 FPS toggle) +- [ ] Recording timer display +- [ ] Pause/resume functionality +- [ ] Custom filename input +- [ ] Multiple format export options +- [ ] Recording preview before save +- [ ] Audio level meters for MP3 and synthesized audio +- [ ] Separate audio track controls (mute individual sources) +- [ ] Audio normalization for mixed sources + +## Troubleshooting + +### No Audio in Recording +- Check if browser has microphone permissions +- Verify AudioContext is initialized +- Some browsers require user interaction to start AudioContext +- Ensure MP3 player audio nodes are properly connected to the recording destination +- Verify that audio mixing is working (check browser console for audio node errors) + +### Recording Button Not Appearing +- Check browser console for errors +- Verify canvas element exists with ID "myCanvas" +- Ensure JavaScript modules are loading correctly + +### Low Quality Video +- Browser may be using fallback codec +- Check available disk space +- Try different browser for VP9 support + +### Dropped Frames Issues +- **High CPU Usage**: Reduce canvas complexity or frame rate +- **Memory Pressure**: Monitor browser memory usage, restart if needed +- **Background Tabs**: Ensure recording tab has focus for optimal performance +- **Hardware Acceleration**: Enable GPU acceleration in browser settings + +### Encoder Optimization Not Working +- **H.264 Unavailable**: Check browser codec support with `MediaRecorder.isTypeSupported()` +- **Poor Compression**: Verify content has static regions for optimization +- **Frame Skipping Disabled**: Some browsers may not support frameSkipping parameter +- **Content Hint Ignored**: Fallback to default encoding if contentHint not supported + +## Testing Checklist + +### Basic Functionality +- [x] Record button appears in correct position +- [x] Button changes color when recording +- [x] Pulse animation works during recording +- [x] Canvas content is captured correctly +- [x] Audio is captured (when available) +- [x] File saves with correct timestamp +- [x] Keyboard shortcut works +- [x] Multiple start/stop cycles work correctly +- [x] Memory is properly released after recording + +### Frame Handling & Optimization +- [x] Frame logging appears in console during recording +- [x] Dropped frame detection triggers warnings when appropriate +- [x] Static content shows high compression efficiency +- [x] H.264 encoder optimization is active when supported +- [x] Fallback to WebM/VP9 works when MP4 unavailable +- [x] Performance monitoring tracks encoding statistics +- [x] Recovery strategies activate during frame drops +- [x] Variable frame rate handling works under load \ No newline at end of file diff --git a/v1.5/lib/FastLED/apa102fix.md b/v1.5/lib/FastLED/apa102fix.md new file mode 100644 index 0000000..a6c1453 --- /dev/null +++ b/v1.5/lib/FastLED/apa102fix.md @@ -0,0 +1,57 @@ +I was dissatisfied with a gamma corrected ramp using the APA102HD mode (it seemed to visibly step at one or more points), and some analysis of the 8 bit rgb + 5 bit brightness function showed why: +https://www.argyllcms.com/APA102_loglog.svg. + +So here is a suggested replacement for five_bit_bitshift() in fl/five_bit_hd_gamma.cpp, that fixed the problem for me: + +// Improved FastLED five_bit_bitshift() function +// This appears to exactly match the floating point reference, +// with a worst case resulting error of 0.2% over the full 16 bit input range, +// and an average error of 0.05% over that range. Errors scale with maximum +// magnitude. + +// ix/31 * 255/65536 * 256 scaling factors, valid for indexes 1..31 +static uint32_t bright_scale[32] = { + 0, 2023680, 1011840, 674560, 505920, 404736, 337280, 289097, + 252960, 224853, 202368, 183971, 168640, 155668, 144549, 134912, + 126480, 119040, 112427, 106509, 101184, 96366, 91985, 87986, + 84320, 80947, 77834, 74951, 72274, 69782, 67456, 65280 +}; + +// Since the return value wasn't used, it has been omitted. +// It's not clear what scale brightness is, or how it is to be applied, +// so we assume 8 bits applied over the given rgb values. +void five_bit_bitshift(uint16_t r16, uint16_t g16, uint16_t b16, uint8_t brightness, + CRGB *out, uint8_t *out_power_5bit) { + uint8_t r8 = 0, g8 = 0, b8 = 0; + + // Apply any brightness setting (we assume brightness is 0..255) + if (brightness != 0xff) { + r16 = scale16by8(r16, brightness); + g16 = scale16by8(g16, brightness); + b16 = scale16by8(b16, brightness); + } + + // Locate the largest value to set the brightness/scale factor + uint16_t scale = max3(r16, g16, b16); + + if (scale == 0) { + *out = CRGB(0, 0, 0); + *out_power_5bit = 0; + return; + } else { + uint32_t scalef; + + // Compute 5 bit quantized scale that is at or above the maximum value. + scale = (scale + (2047 - (scale >> 5))) >> 11; + + // Adjust the 16 bit values to account for the scale, then round to 8 bits + scalef = bright_scale[scale]; + r8 = (r16 * scalef + 0x808000) >> 24; + g8 = (g16 * scalef + 0x808000) >> 24; + b8 = (b16 * scalef + 0x808000) >> 24; + + *out = CRGB(r8, g8, b8); + *out_power_5bit = scale; + return; + } +} \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/AGENTS.md b/v1.5/lib/FastLED/ci/AGENTS.md new file mode 100644 index 0000000..231f8cc --- /dev/null +++ b/v1.5/lib/FastLED/ci/AGENTS.md @@ -0,0 +1,246 @@ +# FastLED CI/Build System Agent Guidelines + +## 🔧 Python Build System + +### Core Architecture +FastLED uses a high-performance Python-based build system: + +**Key Components:** +- `ci/compiler/clang_compiler.py` - Core compiler with PCH and fingerprint cache +- `ci/ci/fingerprint_cache.py` - 99% faster PCH rebuilds through intelligent caching +- `ci/util/build_flags.py` - TOML-based build configuration management +- `ci/compiler/test_example_compilation.py` - Example compilation system + +**Performance Features:** +- **PCH (Precompiled Headers)** - Dramatically faster compilation +- **Fingerprint Cache** - 99% time reduction for unchanged dependencies +- **Parallel Compilation** - Automatic CPU-optimized parallel builds +- **Smart Rebuilds** - Only rebuild when source files actually change + +### 🚨 Command Execution Rules + +#### FOREGROUND AGENTS (Interactive Development) +**Python Code Execution:** +- ✅ **Create/modify tmp.py** in project root (NOT subdirectories) +- ✅ **Always run: `uv run python tmp.py`** (never just `python`) +- ✅ **Correct import paths:** `from ci.ci.module` for ci.ci modules, `from ci.module` for top-level ci modules +- ❌ **NEVER cd to subdirectories** - stay in project root + +**Example:** +```python +# tmp.py (created in project root) +from ci.ci.clang_compiler import Compiler # CORRECT import path +import subprocess +result = subprocess.run(['git', 'status'], capture_output=True, text=True) +print(result.stdout) +``` +Then run: `uv run python tmp.py` + +#### BACKGROUND AGENTS (Automated/CI Environments) +**Restrictions:** +- ❌ **NO tmp.py files** (forbidden for background agents) +- ✅ **Use existing scripts:** `uv run python ci/ci-compile.py uno --examples Blink` +- ✅ **Use MCP server tools** for programmatic operations +- ✅ **Always use `uv run python`** with existing scripts + +## 🤖 MCP Server Configuration + +**Start server:** `uv run mcp_server.py` + +**Available Tools:** +- Running tests with various options +- Compiling examples for different platforms +- Code fingerprinting and change detection +- Linting and formatting +- **Build info analysis** for platform-specific defines, compiler flags, toolchain information +- **Symbol analysis** for binary optimization (all platforms) +- Stack trace setup for enhanced debugging +- **🌐 FastLED Web Compiler** with Playwright (FOREGROUND AGENTS ONLY) +- **🚨 `validate_completion` tool** (MANDATORY for background agents) + +### FastLED Web Compiler (FOREGROUND AGENTS ONLY) + +**Prerequisites:** +- `pip install fastled` - FastLED web compiler +- `pip install playwright` - Browser automation +- Docker (optional, for faster compilation) + +**Usage via MCP Server:** +```python +# Basic usage +use run_fastled_web_compiler tool with: +- example_path: "examples/Audio" +- capture_duration: 30 +- headless: false +- save_screenshot: true +``` + +**Key Features:** +- Compiles Arduino sketches to WASM for browser execution +- Captures console.log output with playwright automation +- Takes screenshots of running visualizations +- Monitors FastLED_onFrame calls to verify proper initialization + +**🚫 BACKGROUND AGENT RESTRICTION:** This tool is COMPLETELY DISABLED for background agents and CI environments. + +## 🤖 Linting Guidelines + +### FOREGROUND AGENTS +**🚨 ALWAYS USE `bash lint` - DO NOT RUN INDIVIDUAL LINTING SCRIPTS** + +- ✅ **CORRECT:** `bash lint` +- ❌ **FORBIDDEN:** `./lint-js`, `./check-js`, `python3 scripts/enhance-js-typing.py` +- ❌ **FORBIDDEN:** `uv run ruff check`, `uv run black`, individual tools + +**WHY:** `bash lint` provides comprehensive coverage of Python, C++, JavaScript, and enhancement analysis. + +### BACKGROUND AGENTS +**CAN USE FINE-GRAINED LINTING FOR SPECIFIC NEEDS** + +Background agents may use individual linting scripts when needed: +- `./lint-js` - JavaScript-only linting +- `./check-js` - JavaScript type checking +- `python3 scripts/enhance-js-typing.py` - JavaScript enhancement analysis +- `uv run ruff check` - Python linting only +- MCP server `lint_code` tool - Programmatic access + +**BUT STILL PREFER `bash lint` FOR COMPREHENSIVE CHECKING** + +## Build System Commands + +**Unit Test Compilation:** +```bash +# Fast Python API (default) +bash test --unit --verbose # Uses PCH optimization + +# Disable PCH if needed +bash test --unit --no-pch --verbose + +# Legacy CMake system (8x slower) +bash test --unit --legacy --verbose +``` + +**Available Build Options:** +- `--no-pch` - Disable precompiled headers +- `--clang` - Use Clang compiler (recommended for speed) +- `--clean` - Force full rebuild +- `--verbose` - Show detailed compilation output +- `--legacy` - Use old CMake system (discouraged) + +**Platform Compilation:** +```bash +# Compile examples for specific platforms +uv run ci/ci-compile.py uno --examples Blink +uv run ci/ci-compile.py esp32dev --examples Blink +uv run ci/ci-compile.py teensy31 --examples Blink +``` + +**WASM Compilation:** +```bash +# Compile any sketch directory to WASM +uv run ci/wasm_compile.py path/to/your/sketch + +# Quick compile test (compile only, no browser) +uv run ci/wasm_compile.py examples/Blink --just-compile + +# Compile and open browser +uv run ci/wasm_compile.py examples/Blink -b --open +``` + +## Build Troubleshooting + +**For Linker Issues:** +1. Check `tests/cmake/LinkerCompatibility.cmake` first +2. Look for lld-link detection and compatibility functions +3. Check GNU→MSVC flag translation logic +4. Verify warning suppression settings + +**For Compiler Issues:** +1. Check `tests/cmake/CompilerDetection.cmake` for detection logic +2. Review `tests/cmake/CompilerFlags.cmake` for flag conflicts +3. Verify optimization settings in `OptimizationSettings.cmake` + +**For Build Performance:** +1. Check `tests/cmake/ParallelBuild.cmake` for parallel settings +2. Review LTO configuration in `OptimizationSettings.cmake` +3. Verify linker selection (mold, lld, default) + +## 🚨 Critical User Feedback Corrections + +**Always Use `uv run python` - NEVER just `python` or `uv run`:** +- ❌ **WRONG**: `python -c "..."` +- ❌ **WRONG**: `uv run -c "..."` +- ✅ **CORRECT**: `uv run python -c "..."` + +**Correct Import Paths:** +- ✅ **CORRECT**: `from ci.ci.clang_compiler import ...` (for ci.ci modules) +- ✅ **CORRECT**: `from ci.clang_compiler import ...` (for top-level ci modules, if they exist) + +**Never Change Directory:** +- ❌ **WRONG**: `cd ci && python ...` +- ❌ **WRONG**: `cd ci/ci && python ...` +- ✅ **CORRECT**: Stay in project root, use full paths: `uv run python ci/script.py` + +**Git-Bash Terminal Compatibility:** +Use leading space for git-bash compatibility: +- ✅ **CORRECT**: ` bash test` (note the leading space) +- ❌ **INCORRECT**: `bash test` (may get truncated to `ash test`) + +## Platform Build Information Analysis + +**Generate Build Info:** +```bash +# Compile a platform to generate build_info.json +uv run ci/ci-compile.py uno --examples Blink +uv run ci/ci-compile.py esp32dev --examples Blink +``` + +**Analyze Platform Defines:** +```bash +# Command line tool +python3 ci/ci/build_info_analyzer.py --board uno --show-defines + +# Via MCP server +uv run mcp_server.py +# Use build_info_analysis tool with: board="uno", show_defines=true +``` + +**Compare Platforms:** +```bash +# Command line +python3 ci/ci/build_info_analyzer.py --compare uno esp32dev + +# Via MCP +# Use build_info_analysis tool with: board="uno", compare_with="esp32dev" +``` + +## Symbol Analysis for Binary Optimization + +**Analyze specific platform:** +```bash +uv run ci/ci/symbol_analysis.py --board uno +uv run ci/ci/symbol_analysis.py --board esp32dev +uv run ci/ci/symbol_analysis.py --board teensy31 +``` + +**Analyze all available platforms:** +```bash +uv run ci/demo_symbol_analysis.py +``` + +**Using MCP Server:** +```bash +# Use MCP server tools +uv run mcp_server.py +# Then use symbol_analysis tool with board: "uno" or "auto" +# Or use symbol_analysis tool with run_all_platforms: true +``` + +**JSON Output:** +```bash +uv run symbol_analysis.py --board esp32dev --output-json +# Results saved to: .build/esp32dev_symbol_analysis.json +``` + +## Memory Refresh Rule +**🚨 ALL AGENTS: Read ci/AGENTS.md before concluding CI/build work to refresh memory about current Python build system rules and MCP server requirements.** diff --git a/v1.5/lib/FastLED/ci/BUILD_SYSTEM_MIGRATION.md b/v1.5/lib/FastLED/ci/BUILD_SYSTEM_MIGRATION.md new file mode 100644 index 0000000..3301ce0 --- /dev/null +++ b/v1.5/lib/FastLED/ci/BUILD_SYSTEM_MIGRATION.md @@ -0,0 +1,228 @@ +# FastLED Python Build System - Implementation Summary + +## Overview + +Successfully implemented a new Python-based build system to replace the complex CMake approach for FastLED example compilation testing. The new system delivers on all design goals: **Simple, Fast, and Transparent**. + +## Key Achievements + +### ⚡ Performance Results +- **4.3 examples/second** compilation rate +- **90%+ faster** than CMake-based approach +- **Sub-second builds** for incremental changes +- **Intelligent caching** with 30%+ hit rates on repeated builds + +### 🏗️ Architecture Implemented + +``` +FastLED Python Build System +├── ci/build_system/ +│ ├── __init__.py # Package initialization +│ ├── build_config.py # Configuration management +│ ├── example_scanner.py # .ino file discovery & analysis +│ ├── change_detector.py # Fast file change detection +│ ├── compiler.py # Direct Clang compilation +│ ├── cache_manager.py # Build cache management +│ └── fastled_build.py # Main build engine +├── ci/fastled_build # CLI entry point +├── ci/test_example_compilation_python.py # New test script +└── ci/test_example_compilation.py # Updated original (with --use-python-build flag) +``` + +### 🎯 Core Features Delivered + +#### 1. **Fast Example Discovery** +- Scans 80+ examples in milliseconds +- Categorizes by FastLED usage and PCH compatibility +- Intelligent filtering of problematic examples + +#### 2. **Content-Based Change Detection** +- SHA-256 fingerprinting of source files +- Dependency tracking for headers +- Smart cache invalidation + +#### 3. **Precompiled Header Optimization** +- Automatic PCH generation for FastLED headers +- PCH reuse across compatible examples +- 50%+ compilation speed improvement + +#### 4. **Intelligent Caching** +- Content-addressable build cache +- sccache integration for compiler-level caching +- Automatic cleanup of stale entries + +#### 5. **Parallel Compilation** +- Multi-threaded compilation (16 cores utilized) +- ThreadPoolExecutor-based job management +- Optimal resource utilization + +## Usage Examples + +### CLI Tool +```bash +# Quick status check +./fastled_build status + +# Build all examples +./fastled_build build + +# Build specific examples +./fastled_build build Blink DemoReel100 + +# Clean build +./fastled_build clean +./fastled_build build + +# System diagnostics +./fastled_build diagnostics +``` + +### Test Scripts +```bash +# Default Python build system (recommended) +python test_example_compilation.py + +# Alternative Python build system script +python test_example_compilation_python.py + +# Both scripts now use Python build system by default +python test_example_compilation.py Blink DemoReel100 # Python system +``` + +## Technical Implementation Details + +### Example Categorization +- **FastLED PCH Compatible**: Examples that can use precompiled headers +- **FastLED Full Build**: Examples requiring full compilation +- **Basic Examples**: Non-FastLED examples +- **Platform Specific**: Hardware-dependent examples +- **Problematic**: Known issues (excluded by default) + +### Build Configuration +- **Compiler**: Auto-detection (Clang preferred) +- **Platform**: STUB implementation for testing +- **Flags**: C++17, -O0 for fast compilation +- **Arduino Compatibility**: WASM Arduino.h integration + +### Performance Optimizations +- **Fast Paths**: "No changes" detection in < 1 second +- **PCH Reuse**: Share PCH across compatible examples +- **Content Hashing**: Only hash changed files +- **Parallel Processing**: All I/O and compilation parallelized + +## Migration Status + +### ✅ Phase 1: Parallel Implementation - COMPLETE +- ✅ Implemented Python build system alongside CMake +- ✅ Added `--use-python-build` flag to test script +- ✅ Validated equivalent functionality +- ✅ Performance testing shows 4x+ improvement + +### 🚧 Phase 2: Feature Parity - IN PROGRESS +- ✅ All CMake features replicated +- ✅ Performance optimization completed +- ✅ CLI and API documentation +- ⏳ Extended platform testing + +### ✅ Phase 3: Migration - COMPLETE +- ✅ Switched default to Python build system +- ✅ Removed CMake implementation completely +- ✅ Updated `test_example_compilation.py` to use Python build system only +- ✅ Maintained backward compatibility with same CLI interface + +### 🧹 Phase 4: Cleanup - ⚠️ **INCOMPLETE** +- ❌ Remove misleading "CMake" references in function names and error messages +- ❌ Clean up dead code paths that reference non-existent CMake directories +- ❌ Fix confusing system names (both systems are Python-based) +- ❌ Update all documentation to reflect accurate system architecture +- ⚠️ **Status**: Misleading naming caused confusion and bugs; cleanup required + +## Performance Comparison + +| Metric | CMake System | Python System | Improvement | +|--------|--------------|---------------|-------------| +| Cold start | 15-30s | 1-2s | **15x faster** | +| Incremental (5 examples) | 8-15s | 1-3s | **5x faster** | +| Full build (80 examples) | 45-60s | 10-15s | **4x faster** | +| Cache hit rate | ~60% | ~90% | **50% better** | +| Memory usage | 2-4GB | 200-500MB | **80% less** | + +## Error Handling & Diagnostics + +### Comprehensive Error Reporting +- Clear, actionable error messages +- Compilation error aggregation +- Performance timing breakdown +- Cache statistics and health + +### Diagnostic Tools +```bash +./fastled_build diagnostics +``` +Shows: +- Compiler information and availability +- Cache statistics and hit rates +- sccache integration status +- File change detection stats + +## Integration with Existing Workflow + +### Backward Compatibility +- ✅ Original `test_example_compilation.py` still works with same interface +- ✅ Same command-line arguments and exit codes +- ✅ Same behavior and output format +- ✅ Complete drop-in replacement (CMake removed) +- ✅ Performance improvement with no API changes + +### Testing Integration +```python +# Easy integration in existing test scripts +from build_system import BuildConfig, FastLEDBuildEngine + +config = BuildConfig.from_environment() +engine = FastLEDBuildEngine(config) +result = engine.build(examples=["Blink", "DemoReel100"]) +assert result.success +``` + +## Benefits Realized + +### 1. **Simplicity** +- ✅ Direct compiler control (no CMake abstraction) +- ✅ Python stack traces for easy debugging +- ✅ Transparent, understandable build logic + +### 2. **Performance** +- ✅ 4x+ faster than CMake approach +- ✅ Intelligent caching tailored to FastLED +- ✅ Efficient incremental builds + +### 3. **Maintainability** +- ✅ Python codebase (team expertise) +- ✅ Modular, testable design +- ✅ Clear separation of concerns + +### 4. **Flexibility** +- ✅ Easy FastLED-specific optimizations +- ✅ Simple tool integration +- ✅ Consistent cross-platform behavior + +## Next Steps + +1. **Extended Testing**: Test with more examples and edge cases +2. **Documentation**: Update development docs to recommend Python system +3. **CI Integration**: Consider switching CI to use Python build system +4. **Feature Additions**: Add requested features (linking, size analysis, etc.) + +## Conclusion + +⚠️ **MIGRATION STATUS**: The Python build system has successfully replaced the CMake approach, but **Phase 4 cleanup was never completed**, leaving misleading function names and error messages that reference "CMake" when both systems are actually Python-based. + +### Final Results +- **🚀 13.3 examples/second** compilation rate (in cached scenarios) +- **⚡ Ultra-fast builds** with intelligent caching +- **🔄 100% backward compatibility** with existing workflows +- **🧹 Clean codebase** with CMake complexity completely removed +- **📊 Superior performance** across all metrics + +The system delivers on all original design goals: **Simple, Fast, and Transparent**. However, **incomplete cleanup has left confusing naming** that makes the system less transparent than intended. The naming issues have been identified and are being addressed. diff --git a/v1.5/lib/FastLED/ci/README.md b/v1.5/lib/FastLED/ci/README.md new file mode 100644 index 0000000..607dc30 --- /dev/null +++ b/v1.5/lib/FastLED/ci/README.md @@ -0,0 +1,11 @@ +This is the Continuous Integration tool that builds the product in various configurations. + +Running + +Install the python `uv` tool. + +`pip install uv` + +Now run the python in this directory + +`uv run ci-compile.py` \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/SYMBOL_ANALYSIS.md b/v1.5/lib/FastLED/ci/SYMBOL_ANALYSIS.md new file mode 100644 index 0000000..52fe883 --- /dev/null +++ b/v1.5/lib/FastLED/ci/SYMBOL_ANALYSIS.md @@ -0,0 +1,313 @@ +# FastLED Symbol Analysis + +This document describes the new generic symbol analysis functionality that works across all supported platforms including UNO, ESP32, and others. + +## Overview + +The symbol analysis tool provides comprehensive analysis of compiled ELF files to help identify optimization opportunities and understand binary size allocation. Unlike the previous ESP32-specific implementation, this tool: + +- ✅ **Works on ALL platforms** (UNO, ESP32, Teensy, STM32, etc.) +- ✅ **Pulls ALL symbols** without filtering or classification +- ✅ **No FastLED-specific filtering** - analyzes everything +- ✅ **Generic and extensible** for any platform with build_info.json + +## Quick Start + +### Analyze a specific platform + +```bash +cd ci +uv run symbol_analysis.py --board uno +uv run symbol_analysis.py --board esp32dev +``` + +### Run analysis on all available platforms + +```bash +uv run demo_symbol_analysis.py +``` + +### Use in GitHub Actions + +```bash +uv run symbol_analysis_runner.py --board uno --example Blink +``` + +## Files + +- **`symbol_analysis.py`** - Main generic symbol analysis tool +- **`symbol_analysis_runner.py`** - GitHub Actions wrapper +- **`demo_symbol_analysis.py`** - Demonstration script for multiple platforms +- **`SYMBOL_ANALYSIS.md`** - This documentation file + +## Example Output + +### UNO Platform Results + +``` +================================================================================ +UNO SYMBOL ANALYSIS REPORT +================================================================================ + +SUMMARY: + Total symbols: 51 + Total symbol size: 3767 bytes (3.7 KB) + +LARGEST SYMBOLS (all symbols, sorted by size): + 1. 1204 bytes - ClocklessController<...>::showPixels(...) + 2. 572 bytes - CFastLED::show(unsigned char) [clone .constprop.18] + 3. 460 bytes - main + 4. 204 bytes - CPixelLEDController<...>::show(...) + 5. 168 bytes - CPixelLEDController<...>::showColor(...) + +SYMBOL TYPE BREAKDOWN: + t: 19 symbols, 2710 bytes (2.6 KB) + T: 14 symbols, 904 bytes (0.9 KB) + b: 15 symbols, 89 bytes (0.1 KB) + d: 3 symbols, 64 bytes (0.1 KB) +``` + +### ESP32 Platform Results + +``` +================================================================================ +ESP32DEV SYMBOL ANALYSIS REPORT +================================================================================ + +SUMMARY: + Total symbols: 2503 + Total symbol size: 237092 bytes (231.5 KB) + +LARGEST SYMBOLS (all symbols, sorted by size): + 1. 12009 bytes - _vfprintf_r + 2. 11813 bytes - _svfprintf_r + 3. 8010 bytes - _vfiprintf_r + 4. 7854 bytes - _svfiprintf_r + 5. 4192 bytes - port_IntStack + +SYMBOL TYPE BREAKDOWN: + T: 1240 symbols, 156230 bytes (152.6 KB) + t: 371 symbols, 45841 bytes (44.8 KB) + d: 475 symbols, 13032 bytes (12.7 KB) + D: 60 symbols, 9421 bytes (9.2 KB) + W: 126 symbols, 5209 bytes (5.1 KB) +``` + +### ESP32S3 Platform Results + +``` +================================================================================ +ESP32S3 SYMBOL ANALYSIS REPORT +================================================================================ + +SUMMARY: + Total symbols: 2831 + Total symbol size: 260710 bytes (254.6 KB) + +LARGEST SYMBOLS (all symbols, sorted by size): + 1. 11309 bytes - _vfprintf_r + 2. 11118 bytes - _svfprintf_r + 3. 7337 bytes - _svfiprintf_r + 4. 7265 bytes - _vfiprintf_r + 5. 4192 bytes - port_IntStack + 6. 1164 bytes - printBeforeSetupInfo() + 7. 828 bytes - fl::RmtController5::loadPixelData(PixelIterator&) + +SYMBOL TYPE BREAKDOWN: + T: 1416 symbols, 172591 bytes (168.5 KB) + t: 429 symbols, 48854 bytes (47.7 KB) + d: 529 symbols, 14189 bytes (13.9 KB) + D: 72 symbols, 9831 bytes (9.6 KB) + W: 122 symbols, 5457 bytes (5.3 KB) +``` + +## Output Files + +The tool generates JSON files with complete symbol data: + +- `.build/{board}_symbol_analysis.json` - Complete analysis results + +### JSON Structure + +```json +{ + "summary": { + "board": "uno", + "total_symbols": 51, + "total_size": 3767, + "largest_symbols": [...], + "type_breakdown": {...}, + "dependencies": {...} + }, + "all_symbols_sorted_by_size": [ + { + "address": "00001240", + "size": 1204, + "type": "t", + "name": "_ZN19ClocklessController...", + "demangled_name": "ClocklessController<...>::showPixels(...)" + }, + ... + ], + "dependencies": { + "module.cpp.o": ["symbol1", "symbol2", ...] + } +} +``` + +## Supported Platforms + +The tool automatically detects and works with any platform that has: +- A `build_info.json` file in `.build/{platform}/` +- Standard toolchain tools (nm, c++filt) in the build info aliases + +Successfully tested platforms: +- ✅ **UNO** (AVR) - 51 symbols, 3.7 KB +- ✅ **ESP32DEV** (Xtensa) - 2503 symbols, 231.5 KB +- ✅ **ESP32S3** (Xtensa) - 2831 symbols, 254.6 KB +- ✅ **TEENSY31** (ARM Cortex-M4) - 238 symbols, 10.9 KB +- ✅ **TEENSYLC** (ARM Cortex-M0+) - 241 symbols, 9.1 KB +- ✅ **DIGIX** (ARM Cortex-M3) - 298 symbols, 17.9 KB +- ✅ **STM32** (ARM Cortex-M3) - 311 symbols, 15.6 KB + +## Key Improvements + +### From ESP32-specific to Generic + +**Before (ESP32 only):** +```python +# Hard-coded ESP32 board detection +esp32_boards = ["esp32dev", "esp32", "esp32s2", ...] + +# FastLED-specific filtering +if any(keyword in search_text for keyword in [ + "fastled", "cfastled", "crgb", "hsv", ...]): + fastled_symbols.append(symbol_info) +``` + +**After (Generic):** +```python +# Auto-detect any platform with build_info.json +for item in build_dir.iterdir(): + if item.is_dir() and (item / "build_info.json").exists(): + available_boards.append((build_info_file, item.name)) + +# No filtering - analyze ALL symbols +symbols.append(symbol_info) # Everything included +``` + +### Symbol Analysis Improvements + +1. **No Classification** - All symbols are included without filtering +2. **Complete Demangling** - All C++ symbols are properly demangled +3. **Platform Agnostic** - Works with any toolchain (AVR, ARM, Xtensa, etc.) +4. **Size-based Sorting** - All results sorted by symbol size for easy optimization targeting + +## GitHub Actions Integration + +The `symbol_analysis_runner.py` script provides GitHub Actions compatibility: + +```yaml +- name: Run Symbol Analysis + run: python ci/symbol_analysis_runner.py --board uno --example Blink +``` + +This runner: +- Validates build_info.json exists +- Handles errors gracefully with `--skip-on-failure` +- Provides proper exit codes for CI/CD + +## Migration from ESP32 Analysis + +If you were using the old ESP32-specific analysis: + +**Old command:** +```bash +cd ci/ci && python esp32_symbol_analysis.py +``` + +**New command:** +```bash +cd ci && uv run symbol_analysis.py --board esp32dev +``` + +The new tool provides all the same information plus: +- Support for more platforms +- ALL symbols instead of just FastLED ones +- Better error handling +- Cleaner output format + +## Advanced Usage + +### Analyzing Custom Platforms + +To add support for a new platform: + +1. Ensure the platform builds correctly with PlatformIO +2. Verify `build_info.json` is generated in `.build/{platform}/` +3. Run: `uv run symbol_analysis.py --board {platform}` + +### Batch Analysis + +```bash +# Analyze all available platforms +uv run demo_symbol_analysis.py + +# Analyze specific platforms in sequence +for board in uno esp32dev teensy31; do + uv run symbol_analysis.py --board $board +done +``` + +### Integration with Build Systems + +```python +# In your Python build script +from ci.ci.symbol_analysis import main as analyze_symbols +import sys + +# Override arguments +sys.argv = ["symbol_analysis.py", "--board", "uno"] +analyze_symbols() +``` + +## Troubleshooting + +### Common Issues + +1. **"build_info.json not found"** + - Solution: Compile the platform first: `uv run ci/ci-compile.py {board} --examples Blink` + +2. **"KeyError: 'board_name'"** + - Solution: The board name in the directory doesn't match the key in build_info.json + - Check: `cat .build/{board}/build_info.json | jq 'keys'` + +3. **"Tool not found"** + - Solution: The toolchain isn't installed or path is incorrect in build_info.json + +### Debug Mode + +To debug issues, run with verbose Python output: +```bash +uv run python -v symbol_analysis.py --board uno +``` + +## Contributing + +When adding new features: + +1. Maintain platform independence +2. Don't add filtering logic +3. Test on multiple platforms +4. Update this documentation +5. Add tests to verify functionality + +## Performance + +Platform analysis times (approximate): +- **UNO**: < 1 second (51 symbols) +- **Teensy**: < 2 seconds (238-241 symbols) +- **STM32**: < 3 seconds (311 symbols) +- **ESP32**: < 10 seconds (2503 symbols) + +The tool scales well with symbol count and provides progress feedback during demangling. diff --git a/v1.5/lib/FastLED/ci/__init__.py b/v1.5/lib/FastLED/ci/__init__.py new file mode 100644 index 0000000..86b4be4 --- /dev/null +++ b/v1.5/lib/FastLED/ci/__init__.py @@ -0,0 +1,13 @@ +"""FastLED CI tools and utilities + +Module-level initialization to ensure consistent runtime behavior across tools. +""" + +# Configure UTF-8 console output on Windows globally for CI tools +try: + from ci.util.console_utf8 import configure_utf8_console + + configure_utf8_console() +except Exception: + # Never fail import due to console configuration + pass diff --git a/v1.5/lib/FastLED/ci/adafruit_bridge/adafruit_bridge.ino.cpp b/v1.5/lib/FastLED/ci/adafruit_bridge/adafruit_bridge.ino.cpp new file mode 100644 index 0000000..6d25826 --- /dev/null +++ b/v1.5/lib/FastLED/ci/adafruit_bridge/adafruit_bridge.ino.cpp @@ -0,0 +1,45 @@ + + + +#define FASTLED_USE_ADAFRUIT_NEOPIXEL +#include "FastLED.h" + +// Test the non-template Adafruit bridge +#define DATA_PIN 2 +#define NUM_LEDS 144 + +// Test LED arrays with different RGB orders +CRGB leds_grb[NUM_LEDS]; +CRGB leds_rgb[NUM_LEDS]; +CRGB leds_bgr[NUM_LEDS]; + +void setup() { + Serial.begin(115200); + + // Test template controllers + FastLED.addLeds(leds_grb, NUM_LEDS); + FastLED.addLeds(leds_rgb, NUM_LEDS); + FastLED.addLeds(leds_bgr, NUM_LEDS); + + // Fill with test patterns + fill_rainbow(leds_grb, NUM_LEDS, 0, 255/NUM_LEDS); + fill_solid(leds_rgb, NUM_LEDS, CRGB::Red); + fill_solid(leds_bgr, NUM_LEDS, CRGB::Blue); + + FastLED.show(); + Serial.println("Adafruit bridge test initialized"); +} + +void loop() { + static uint8_t hue = 0; + + // Update colors + fill_rainbow(leds_grb, NUM_LEDS, hue, 255/NUM_LEDS); + fill_solid(leds_rgb, NUM_LEDS, CHSV(hue, 255, 255)); + fill_solid(leds_bgr, NUM_LEDS, CHSV(hue + 128, 255, 255)); + + FastLED.show(); + + hue += 2; + delay(50); +} diff --git a/v1.5/lib/FastLED/ci/adafruit_bridge/platformio.ini b/v1.5/lib/FastLED/ci/adafruit_bridge/platformio.ini new file mode 100644 index 0000000..cbdca78 --- /dev/null +++ b/v1.5/lib/FastLED/ci/adafruit_bridge/platformio.ini @@ -0,0 +1,13 @@ +[platformio] +src_dir = symlink://../../../src + +[env:dev] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip +board = esp32-s3-devkitc-1 +framework = arduino +lib_deps = + adafruit/Adafruit NeoPixel + +lib_ldf_mode=deep +build_flags=-DFASTLED_STUB_MAIN_INCLUDE_INO="../ci/adafruit_bridge/adafruit_bridge.ino.cpp" + -DFASTLED_USE_ADAFRUIT_NEOPIXEL=1 \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/boards.py b/v1.5/lib/FastLED/ci/boards.py new file mode 100644 index 0000000..68b37f1 --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards.py @@ -0,0 +1,769 @@ +# dataclasses + +import json +from copy import deepcopy +from dataclasses import dataclass +from typing import Any + + +# An open source version of the esp-idf 5.1 platform for the ESP32 that +# gives esp32 boards the same build environment as the Arduino 2.3.1+. + +# Set to a specific release, we may want to update this in the future. +ESP32_IDF_5_1_PIOARDUINO = "https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip" + +# TODO: Upgrade toolkit to 5.3 +ESP32_IDF_5_3_PIOARDUINO = "https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10/platform-espressif32.zip" +ESP32_IDF_5_4_PIOARDUINO = "https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip" +ESP32_IDF_5_5_PIOARDUINO = "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip" +ESP32_IDF_LATEST_PIOARDUINO = ESP32_IDF_5_5_PIOARDUINO + +ESP32_IDF_LATEST_PIOARDUINO = ( + "https://github.com/pioarduino/platform-espressif32.git#develop" +) +ESP32_IDF_4_4_LATEST = ( + "https://github.com/platformio/platform-espressif32/archive/refs/tags/v4.4.0.zip" +) +APOLLO3_2_2_0 = "https://github.com/nigelb/platform-apollo3blue" +# Top of trunk. +# ESP32_IDF_5_1_PIOARDUINO = "https://github.com/pioarduino/platform-espressif32" + +# Old fork that we were using +# ESP32_IDF_5_1_PIOARDUINO = "https://github.com/zackees/platform-espressif32#Arduino/IDF5" + +# ALL will be auto populated in the Board constructor whenever a +# board is defined. +ALL: list["Board"] = [] + + +class Auto: + pass + + +@dataclass +class Board: + board_name: str + real_board_name: str | None = None + platform: str | None = None + platform_needs_install: bool = False + use_pio_run: bool = ( + False # some platforms like esp32-c2-devkitm-1 will only work with pio run + ) + platform_packages: str | None = None + framework: str | None = None + board_build_mcu: str | None = None + board_build_core: str | None = None + board_build_filesystem_size: str | None = None + build_flags: list[str] | None = None # Reserved for future use. + build_unflags: list[str] | None = None # New: unflag options + defines: list[str] | None = None + customsdk: str | None = None + board_partitions: str | None = None # Reserved for future use. + no_board_spec: bool = ( + False # For platforms like 'native' that don't need a board specification + ) + add_board_to_all: bool = True + lib_compat_mode: str | None = ( + None # Library compatibility mode (e.g., 'off' for native platform) + ) + lib_ldf_mode: str | None = ( + None # Library Dependency Finder mode (e.g., 'chain+' for enhanced dependency finding) + ) + lib_ignore: list[str] | None = ( + None # Libraries to ignore during compilation (e.g., ['I2S'] for UNO R4 WiFi) + ) + + def __post_init__(self) -> None: + # Check if framework is set, warn and auto-set to arduino if missing (except for native/stub platforms) + if self.framework is None and not self._is_native_or_stub_platform(): + self.framework = "arduino" + + # Auto-detect and exclude problematic libraries based on environmental signals + self._auto_detect_library_exclusions() + + ALL.append(self) + + def _is_native_or_stub_platform(self) -> bool: + """Check if this is a native or stub platform that doesn't need a framework""" + # Check for native platform + if self.platform and ( + "native" in self.platform.lower() or "stub" in self.platform.lower() + ): + return True + + # Check for stub build flags + if self.build_flags: + for flag in self.build_flags: + if "FASTLED_STUB" in flag or "PLATFORM_NATIVE" in flag: + return True + + # Check no_board_spec flag (typically used for native platforms) + if self.no_board_spec: + return True + + return False + + def _auto_detect_library_exclusions(self) -> None: + """Automatically detect and exclude libraries known to be broken on this platform.""" + + # Check for I2S library problems based on environmental signals + if self._should_ignore_i2s_library(): + if not self.lib_ignore: + self.lib_ignore = [] + if "I2S" not in self.lib_ignore: + self.lib_ignore.append("I2S") + + def _should_ignore_i2s_library(self) -> bool: + """Detect if I2S library should be ignored based on platform characteristics.""" + + # Known problematic platforms - Renesas RA family + if self.platform == "renesas-ra": + return True + + # Known problematic board names + problematic_boards = ["uno_r4_wifi", "uno_r4_minima"] + if self.board_name in problematic_boards: + return True + + # Check for specific defines that indicate I2S problems + if self.defines: + problematic_defines = [ + "ARDUINO_UNOR4_WIFI", + "ARDUINO_UNOR4_MINIMA", + "ARDUINO_ARCH_RENESAS", + "ARDUINO_ARCH_RENESAS_UNO", + "_RENESAS_RA_", + "ARDUINO_FSP", + ] + for define in self.defines: + # Check if define contains any problematic patterns + if any(prob_define in define for prob_define in problematic_defines): + return True + + return False + + def clone(self) -> "Board": + out = Board( + board_name=self.board_name, + add_board_to_all=False, + ) + for field_name, field_info in self.__dataclass_fields__.items(): + field_value: Any = getattr(self, field_name) + # Create deep copy for mutable types to avoid shared references + if isinstance(field_value, (list, dict)): + field_value = deepcopy(field_value) # type: ignore[misc] + setattr(out, field_name, field_value) + return out + + def get_real_board_name(self) -> str: + return self.real_board_name if self.real_board_name else self.board_name + + def to_dictionary(self) -> dict[str, list[str]]: + out: dict[str, list[str]] = {} + if self.real_board_name: + out[self.board_name] = [f"board={self.real_board_name}"] + options = out.setdefault(self.board_name, []) + + if self.platform: + options.append(f"platform={self.platform}") + + if self.platform_needs_install: + options.append("platform_needs_install=true") + if self.platform_packages: + options.append(f"platform_packages={self.platform_packages}") + if self.framework: + options.append(f"framework={self.framework}") + if self.board_build_core: + options.append(f"board_build.core={self.board_build_core}") + if self.board_build_mcu: + options.append(f"board_build.mcu={self.board_build_mcu}") + if self.board_build_filesystem_size: + options.append( + f"board_build.filesystem_size={self.board_build_filesystem_size}" + ) + if self.defines: + for define in self.defines: + options.append(f"build_flags=-D{define}") + + # Handle build_unflags + if self.build_unflags: + for uf in self.build_unflags: + options.append(f"build_unflags={uf}") + + # Handle explicit build_flags (added for native host compilation and other special cases) + if self.build_flags: + for bf in self.build_flags: + options.append(f"build_flags={bf}") + + if self.customsdk: + options.append(f"custom_sdkconfig={self.customsdk}") + + # Add board-specific build cache directory pointing via symlink directive + # here = Path(__file__).parent + # project_root = here.parent.parent # Move from ci/util/ to project root + # cache_dir = project_root / ".pio_cache" / self.board_name + # absolute_cache_dir = cache_dir.resolve() + # options.append(f"build_cache_dir=symlink://{absolute_cache_dir}") + + return out + + def __repr__(self) -> str: + json_str = json.dumps(self.to_dictionary(), indent=4, sort_keys=True) + return json_str + + def __hash__(self) -> int: + data_str = self.__repr__() + return hash(data_str) + + def to_platformio_ini( + self, + additional_defines: list[str] | None = None, + additional_include_dirs: list[str] | None = None, + additional_libs: list[str] | None = None, + include_platformio_section: bool = False, + core_dir: str | None = None, + packages_dir: str | None = None, + project_root: str | None = None, + build_cache_dir: str | None = None, + extra_scripts: list[str] | None = None, + ) -> str: + """Return a `platformio.ini` snippet representing this board. + + The output is suitable for directly appending to a *platformio.ini* file + and follows the same semantics used by the PlatformIO CLI. Only + parameters understood by PlatformIO are emitted – internal helper + fields like ``platform_needs_install`` and ``use_pio_run`` are **not** + included because they are consumed exclusively by the build helpers in + the *ci/* folder and would be ignored (or flagged as errors) by + PlatformIO itself. + + Args: + example: Example name for dynamic build flags (currently unused) + additional_defines: Additional defines to merge with board defines + additional_include_dirs: Additional include directories to merge with build flags + include_platformio_section: Whether to include [platformio] section with core_dir/packages_dir + core_dir: PlatformIO core directory path + packages_dir: PlatformIO packages directory path + project_root: FastLED project root for lib_deps symlink + """ + lines: list[str] = [] + + # Optional [platformio] section + if include_platformio_section: + lines.append("[platformio]") + if build_cache_dir: + lines.append(f"build_cache_dir = {build_cache_dir}") + if core_dir: + lines.append(f"core_dir = {core_dir}") + if packages_dir: + lines.append(f"packages_dir = {packages_dir}") + lines.append("") + + # Section header + lines.append(f"[env:{self.board_name}]") + if extra_scripts: + lines.append(f"extra_scripts = {' '.join(extra_scripts)}") + + # Board identifier (skip for platforms that don't need board specification) + if not self.no_board_spec: + lines.append(f"board = {self.get_real_board_name()}") + + # Optional parameters ------------------------------------------------- + if self.platform: + lines.append(f"platform = {self.platform}") + + if self.platform_packages: + lines.append(f"platform_packages = {self.platform_packages}") + + if self.framework: + lines.append(f"framework = {self.framework}") + + if self.board_build_core: + lines.append(f"board_build.core = {self.board_build_core}") + + if self.board_build_mcu: + lines.append(f"board_build.mcu = {self.board_build_mcu}") + + if self.board_build_filesystem_size: + lines.append( + f"board_build.filesystem_size = {self.board_build_filesystem_size}" + ) + + if self.board_partitions: + lines.append(f"board_partitions = {self.board_partitions}") + + # Build-time flags and unflags --------------------------------------- + build_flags_elements: list[str] = [] + if self.defines: + build_flags_elements.extend(f"-D{define}" for define in self.defines) + + # Add additional defines + if additional_defines: + build_flags_elements.extend(f"-D{define}" for define in additional_defines) + + # Use static build flags + if self.build_flags: + build_flags_elements.extend(self.build_flags) + + # Add additional include directories + if additional_include_dirs: + build_flags_elements.extend( + f"-I{include_dir}" for include_dir in additional_include_dirs + ) + + if build_flags_elements: + # Put each build flag on a separate line for better readability and debugging + lines.append("build_flags =") + for flag in build_flags_elements: + lines.append(f" {flag}") + + if self.build_unflags: + # PlatformIO accepts multiple *build_unflags* separated by spaces. + # Emit a single line for readability. + lines.append(f"build_unflags = {' '.join(self.build_unflags)}") + + # Custom ESP-IDF sdkconfig override (ESP32-family boards) + if self.customsdk: + lines.append(f"custom_sdkconfig = {self.customsdk}") + + # Library compatibility mode (for platforms like native that need special handling) + if self.lib_compat_mode: + lines.append(f"lib_compat_mode = {self.lib_compat_mode}") + + # Library Dependency Finder mode (for enhanced dependency finding) + if self.lib_ldf_mode: + lines.append(f"lib_ldf_mode = {self.lib_ldf_mode}") + + # Libraries to ignore during compilation + if self.lib_ignore: + if len(self.lib_ignore) == 1: + lines.append(f"lib_ignore = {self.lib_ignore[0]}") + else: + lines.append("lib_ignore =") + for lib in self.lib_ignore: + lines.append(f" {lib}") + + # Add FastLED-specific configurations if project_root is provided + if project_root: + # Only add default lib_ldf_mode if board doesn't specify its own + current_config = "\n".join(lines) + if "lib_ldf_mode" not in current_config: + lines.append("lib_ldf_mode = chain") + lines.append("lib_archive = true") + + # Build lib_deps with additional libs only (FastLED is copied to lib/FastLED) + # PlatformIO supports multiple lib_deps entries on separate lines + if additional_libs: + # Format as multi-line lib_deps for proper PlatformIO parsing + if len(additional_libs) == 1: + lines.append(f"lib_deps = {additional_libs[0]}") + else: + lines.append("lib_deps =") + for entry in additional_libs: + lines.append(f" {entry}") + + return "\n".join(lines) + "\n" + + +# [env:sparkfun_xrp_controller] +# platform = https://github.com/maxgerhardt/platform-raspberrypi +# board = sparkfun_xrp_controller +# framework = arduino +# lib_deps = fastled/FastLED @ ^3.9.16 + + +WEBTARGET = Board( + board_name="web", +) + +# Native host compilation target using PlatformIO's "native" platform. +# This allows compiling FastLED for the host machine (Linux/macOS/Windows) +# which is useful for CI compile-tests and static analysis. We replicate +# the build flags present in ci/native/platformio.ini so that the same +# stub implementation and main-file inclusion are used. + +NATIVE = Board( + board_name="native", + platform="platformio/native", + no_board_spec=True, # Native platform doesn't need a board specification + lib_compat_mode="off", # Disable library compatibility checking for native platform + build_flags=[ + "-DFASTLED_STUB_IMPL", # Enable stub platform implementations + "-DFASTLED_USE_STUB_ARDUINO", # Enable Arduino stub implementations + "-DPLATFORM_NATIVE", # Enable native platform stub compilation + "-std=c++17", + "-I../../../src/platforms/stub", # Include path for Arduino.h and other stub headers (relative to project dir) + "-I../../../src", # Include path for FastLED.h and other source headers (relative to project dir) + ], +) + +DUE = Board( + board_name="due", + platform="atmelsam", +) + + +SPARKFUN_XRP_CONTROLLER_2350B = Board( + board_name="sparkfun_xrp_controller", + platform="https://github.com/maxgerhardt/platform-raspberrypi", + platform_needs_install=True, +) + +APOLLO3_RED_BOARD = Board( + board_name="apollo3_red", + real_board_name="SparkFun_RedBoard_Artemis_ATP", + platform=APOLLO3_2_2_0, + platform_packages="framework-arduinoapollo3@https://github.com/sparkfun/Arduino_Apollo3#v2.2.0", + platform_needs_install=True, +) + +APOLLO3_SPARKFUN_THING_PLUS_EXPLORABLE = Board( + board_name="apollo3_thing_explorable", + real_board_name="SparkFun_Thing_Plus_expLoRaBLE", + platform=APOLLO3_2_2_0, + platform_packages="framework-arduinoapollo3@https://github.com/sparkfun/Arduino_Apollo3#v2.2.0", + platform_needs_install=True, +) + +ESP32DEV = Board( + board_name="esp32dev", + platform=ESP32_IDF_5_3_PIOARDUINO, +) + +ESP32DEV_IDF3_3 = Board( + board_name="esp32dev_idf33", + real_board_name="esp32dev", + platform="espressif32@1.11.2", +) + +ESP32DEV_IDF4_4 = Board( + board_name="esp32dev_idf44", + real_board_name="esp32dev", + platform=ESP32_IDF_4_4_LATEST, +) + +GIGA_R1 = Board( + board_name="giga_r1", + platform="ststm32", + framework="arduino", + real_board_name="giga_r1_m7", +) + +# ESP01 = Board( +# board_name="esp01", +# platform=ESP32_IDF_5_1_PIOARDUINO, +# ) + +ESP32_C2_DEVKITM_1 = Board( + board_name="esp32c2", + real_board_name="esp32-c2-devkitm-1", + platform="https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip", + framework="arduino, espidf", +) + +ESP32_C3_DEVKITM_1 = Board( + board_name="esp32c3", + real_board_name="esp32-c3-devkitm-1", + platform=ESP32_IDF_5_3_PIOARDUINO, +) + +ESP32_C5_DEVKITC_1 = Board( + board_name="esp32c5", + real_board_name="esp32-c5-devkitc-1", + platform=ESP32_IDF_5_5_PIOARDUINO, +) + +ESP32_C6_DEVKITC_1 = Board( + board_name="esp32c6", + real_board_name="esp32-c6-devkitc-1", + platform=ESP32_IDF_5_3_PIOARDUINO, +) + +ESP32_S3_DEVKITC_1 = Board( + board_name="esp32s3", + real_board_name="esp32-s3-devkitc-1", + platform=ESP32_IDF_5_4_PIOARDUINO, + framework="arduino", + board_partitions="huge_app.csv", + build_unflags=["-DFASTLED_RMT5=0", "-DFASTLED_RMT5"], +) + +ESP32_S2_DEVKITM_1 = Board( + board_name="esp32s2", + real_board_name="lolin_s2_mini ", + board_build_mcu="esp32s2", + platform=ESP32_IDF_5_3_PIOARDUINO, +) + +ESP32_UPESY_WROOM = Board( + board_name="upesy_wroom", + real_board_name="upesy_wroom", + platform="espressif32", +) + +ESP32H2 = Board( + board_name="esp32h2", + real_board_name="esp32-h2-devkitm-1", + platform_needs_install=True, # Install platform package to get the boards + platform=ESP32_IDF_5_3_PIOARDUINO, +) + +ESP32_P4 = Board( + board_name="esp32p4", + real_board_name="esp32-p4-evboard", + platform_needs_install=True, # Install platform package to get the boards + platform="https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip", +) + +ADA_FEATHER_NRF52840_SENSE = Board( + board_name="adafruit_feather_nrf52840_sense", + platform="nordicnrf52", +) + +XIAOBLESENSE_ADAFRUIT_NRF52 = Board( + board_name="xiaoblesense_adafruit", + platform="https://github.com/maxgerhardt/platform-nordicnrf52", + platform_needs_install=True, # Install platform package to get the boards +) + +# Alias: handle common misspelling without the trailing 't' +XIAOBLESENSE_ADAFRUI_ALIAS = Board( + board_name="xiaoblesense_adafrui", # missing 't' + real_board_name="xiaoblesense_adafruit", # map to the correct board name + platform="https://github.com/maxgerhardt/platform-nordicnrf52", + platform_needs_install=True, +) + +XIAOBLESENSE_NRF52 = Board( + board_name="xiaoblesense", + real_board_name="xiaoble_adafruit", + platform="https://github.com/maxgerhardt/platform-nordicnrf52", + platform_needs_install=True, +) + +# Correct nRF52840 DK board definition +# The Nordic nRF52840 DK is directly supported by the default PlatformIO +# `nordicnrf52` platform under the board name `nrf52840_dk`, so we don't +# need a custom platform package or extra installation steps. Point the +# Board definition at the stock platform and use the canonical board name. +# This fixes compilation failures introduced during the build-system +# migration where the board was temporarily mapped to the XIAO variant. +NRF52840 = Board( + board_name="nrf52840_dk", + real_board_name="nrf52840_dk_adafruit", # Use Adafruit BSP variant which includes full Nordic SDK headers + platform="nordicnrf52", + framework="arduino", + platform_needs_install=False, + platform_packages="framework-arduinoadafruitnrf52@^1.10601.0", + defines=[ + "FASTLED_USE_COMPILE_TESTS=0", + ], + board_build_core="nRF5", # Ensure correct core directory +) + +RPI_PICO = Board( + board_name="rpipico", + platform="https://github.com/maxgerhardt/platform-raspberrypi.git", + platform_needs_install=True, # Install platform package to get the boards + platform_packages="framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git", + framework="arduino", + board_build_core="earlephilhower", + board_build_filesystem_size="0.5m", +) + +RPI_PICO2 = Board( + board_name="rpipico2", + real_board_name="rpipico", # Use the existing Pico board definition until PlatformIO adds native Pico 2 support + platform="https://github.com/maxgerhardt/platform-raspberrypi.git", + platform_needs_install=True, # Install platform package to get the boards + platform_packages="framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git", + framework="arduino", + board_build_core="earlephilhower", + board_build_filesystem_size="0.5m", +) + +BLUEPILL = Board( + board_name="bluepill", + real_board_name="bluepill_f103c8", + platform="ststm32", +) + +# maple_mini_b20 +MAPLE_MINI = Board( + board_name="maple_mini", + real_board_name="maple_mini_b20", + platform="ststm32", +) + +HY_TINYSTM103TB = Board( + board_name="hy_tinystm103tb", + platform="ststm32", +) + +ATTINY88 = Board( + board_name="attiny88", + platform="atmelavr", +) + +# ATtiny1604 +ATTINY1604 = Board( + board_name="ATtiny1604", + platform="atmelmegaavr", +) + +# attiny4313 +ATTINY4313 = Board( + board_name="attiny4313", + platform="atmelavr", +) + +ATTINY1616 = Board( + board_name="ATtiny1616", + platform="atmelmegaavr", +) + +UNO_R4_WIFI = Board( + board_name="uno_r4_wifi", + platform="renesas-ra", + # I2S library auto-excluded due to missing r_i2s_api.h header in Arduino Renesas framework +) + +NANO_EVERY = Board( + board_name="nano_every", + platform="atmelmegaavr", +) + +ESP32DEV_I2S = Board( + board_name="esp32dev_i2s", + real_board_name="esp32dev", + platform=ESP32_IDF_4_4_LATEST, +) + +ESP32S3_RMT51 = Board( + board_name="esp32rmt_51", + real_board_name="esp32-s3-devkitc-1", + platform_needs_install=True, + platform=ESP32_IDF_5_3_PIOARDUINO, + defines=[ + "FASTLED_RMT5=1", + ], +) + +# Teensy boards +TEENSY_LC = Board( + board_name="teensylc", + platform="teensy", + framework="arduino", +) + +TEENSY30 = Board( + board_name="teensy30", + platform="teensy", + framework="arduino", +) + +TEENSY31 = Board( + board_name="teensy31", + platform="teensy", + framework="arduino", +) + +TEENSY40 = Board( + board_name="teensy40", + platform="teensy", + framework="arduino", +) + +TEENSY41 = Board( + board_name="teensy41", + platform="teensy", + framework="arduino", +) + +# Basic Arduino boards +UNO = Board( + board_name="uno", + platform="atmelavr", + framework="arduino", +) + +YUN = Board( + board_name="yun", + platform="atmelavr", + framework="arduino", +) + +DIGIX = Board( + board_name="digix", + real_board_name="due", # Digix is Arduino Due compatible + platform="atmelsam", + framework="arduino", +) + +# ESP8266 boards +ESP01 = Board( + board_name="esp01", + platform="espressif8266", + framework="arduino", +) + +# ATtiny boards +ATTINY85 = Board( + board_name="attiny85", + platform="atmelavr", + framework="arduino", +) + +# Seeed XIAO ESP32S3 board – same platform, needs FASTLED_RMT5 macro removal +XIAO_ESP32S3 = Board( + board_name="seeed_xiao_esp32s3", + real_board_name="seeed_xiao_esp32s3", + platform=ESP32_IDF_5_4_PIOARDUINO, + board_partitions="huge_app.csv", + defines=None, + build_unflags=["-DFASTLED_RMT5=0", "-DFASTLED_RMT5"], +) + +# STM32F4 Black Pill board - addresses GitHub issue #726 +BLACKPILL = Board( + board_name="blackpill", + real_board_name="blackpill_f411ce", + platform="ststm32", +) + +# Silicon Labs MGM240S boards (Arduino Nano Matter, SparkFun Thing Plus Matter) +# Uses Silicon Labs EFM32 platform with Arduino framework support +# Based on EFR32MG24 SoC with ARM Cortex-M33 @ 78MHz +MGM240S = Board( + board_name="mgm240", + real_board_name="sparkfun_thingplusmatter", + platform="https://github.com/maxgerhardt/platform-siliconlabsefm32/archive/refs/heads/silabs-arduino.zip", + platform_needs_install=True, + framework="arduino", +) + + +def _make_board_map(boards: list[Board]) -> dict[str, Board]: + # make board map, but assert on duplicate board names + board_map: dict[str, Board] = {} + for board in boards: + assert board.board_name not in board_map, ( + f"Duplicate board name: {board.board_name}" + ) + board_map[board.board_name] = board + return board_map + + +_BOARD_MAP: dict[str, Board] = _make_board_map(ALL) + + +def create_board(board_name: str, no_project_options: bool = False) -> Board: + board: Board + if no_project_options: + board = Board(board_name=board_name, add_board_to_all=False) + if board_name not in _BOARD_MAP: + # empty board without any special overrides, assume platformio will know what to do with it. + board = Board(board_name=board_name, add_board_to_all=False) + else: + board = _BOARD_MAP[board_name] + return board.clone() diff --git a/v1.5/lib/FastLED/ci/boards/bluepill.json b/v1.5/lib/FastLED/ci/boards/bluepill.json new file mode 100644 index 0000000..a6a1799 --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/bluepill.json @@ -0,0 +1,47 @@ +{ + "build": { + "core": "maple", + "cpu": "cortex-m3", + "extra_flags": "-DSTM32F103C8 -DSTM32F1 -DARDUINO_GENERIC_STM32F103C8 -DARDUINO_ARCH_STM32", + "f_cpu": "72000000L", + "hwids": [ + [ + "0x1EAF", + "0x0003" + ] + ], + "ldscript": "jtag_c8.ld", + "mcu": "stm32f103c8", + "product_line": "STM32F103xx", + "variant": "STM32F103C8" + }, + "connectivity": [ + "can" + ], + "debug": { + "jlink_device": "STM32F103C8", + "openocd_board": "stm32f103c8_blue_pill", + "openocd_target": "stm32f1x", + "svd_path": "STM32F103.svd" + }, + "frameworks": [ + "arduino", + "stm32cube", + "libopencm3" + ], + "name": "Blue Pill STM32F103C8", + "upload": { + "maximum_ram_size": 20480, + "maximum_size": 65536, + "protocol": "stlink", + "protocols": [ + "stlink", + "dfu", + "serial", + "blackmagic", + "jlink" + ] + }, + "url": "https://stm32-base.org/boards/STM32F103C8T6-Blue-Pill", + "vendor": "Generic" +} diff --git a/v1.5/lib/FastLED/ci/boards/esp32-c2-devkitm-1.json b/v1.5/lib/FastLED/ci/boards/esp32-c2-devkitm-1.json new file mode 100644 index 0000000..dcf2ca4 --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/esp32-c2-devkitm-1.json @@ -0,0 +1,35 @@ +{ + "build": { + "core": "esp32", + "f_cpu": "120000000L", + "f_flash": "60000000L", + "flash_mode": "qio", + "mcu": "esp32c2", + "variant": "esp32c2" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "openocd_target": "esp32c2.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Espressif ESP32-C2-DevKitM-1", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 278528, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "espidf": { + "custom_sdkconfig": [ + "CONFIG_IDF_TARGET=\"esp32c2\"" + ] + }, + "url": "https://docs.espressif.com/projects/espressif-esp-dev-kits/en/latest/esp8684/esp8684-devkitm-1/user_guide.html", + "vendor": "Espressif" +} diff --git a/v1.5/lib/FastLED/ci/boards/esp32-c6-devkitc-1.json b/v1.5/lib/FastLED/ci/boards/esp32-c6-devkitc-1.json new file mode 100644 index 0000000..a5732a1 --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/esp32-c6-devkitc-1.json @@ -0,0 +1,31 @@ +{ + "build": { + "core": "esp32", + "f_cpu": "160000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "mcu": "esp32c6", + "variant": "esp32c6" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "openocd_target": "esp32c6.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Espressif ESP32-C6-DevKitC-1", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://docs.espressif.com/projects/espressif-esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/index.html", + "vendor": "Espressif" +} diff --git a/v1.5/lib/FastLED/ci/boards/esp32-h2-devkitm-1.json b/v1.5/lib/FastLED/ci/boards/esp32-h2-devkitm-1.json new file mode 100644 index 0000000..e5c912c --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/esp32-h2-devkitm-1.json @@ -0,0 +1,31 @@ +{ + "build": { + "core": "esp32", + "f_cpu": "96000000L", + "f_flash": "64000000L", + "f_image": "48000000L", + "flash_mode": "qio", + "mcu": "esp32h2", + "variant": "esp32h2" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "openocd_target": "esp32h2.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Espressif ESP32-H2-DevKit", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://docs.espressif.com/projects/espressif-esp-dev-kits/en/latest/esp32h2/esp32-h2-devkitm-1/index.html", + "vendor": "Espressif" +} diff --git a/v1.5/lib/FastLED/ci/boards/giga.json b/v1.5/lib/FastLED/ci/boards/giga.json new file mode 100644 index 0000000..6d05c1d --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/giga.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "linker_script.ld", + "flash_layout": "100_0" + }, + "extra_flags": "-DARDUINO_GIGA -DARDUINO_GIGA_PINS -DGIGA_PINS -DGIGA -DCM4_BINARY_START=0x60000000 -DCM4_BINARY_END=0x60040000 -DCM4_RAM_END=0x60080000", + "core": "arduino", + "cpu": "cortex-m7", + "f_cpu": "480000000L", + "mcu": "stm32h747xih6", + "variant": "GIGA", + "product_line": "STM32H747xx", + "hwids": [ + [ + "0x2341", + "0x0266" + ], + [ + "0x2341", + "0x0366" + ], + [ + "0x2341", + "0x0466" + ] + ] + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "jlink_device": "STM32H747XI_M7", + "openocd_target": "stm32h7x_dual_bank" + }, + "frameworks": [ + "arduino" + ], + "name": "Arduino Giga R1 Wifi", + "upload": { + "_maximum_ram_size": 294248, + "_maximum_size": 1048576, + "maximum_ram_size": 523624, + "maximum_size": 1966080, + "protocol": "dfu", + "protocols": [ + "cmsis-dap", + "dfu", + "jlink", + "stlink", + "mbed" + ], + "require_upload_port": true, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "offset_address": "0x08100000" + }, + "url": "https://docs.arduino.cc/hardware/giga-r1-wifi", + "vendor": "Arduino" +} \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/boards/nrf52840_dk.json b/v1.5/lib/FastLED/ci/boards/nrf52840_dk.json new file mode 100644 index 0000000..5cba666 --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/nrf52840_dk.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino":{ + "ldscript": "nrf52_xxaa.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52_DK", + "f_cpu": "64000000L", + "mcu": "nrf52840", + "variant": "nRF52DK", + "zephyr": { + "variant": "nrf52840dk_nrf52840" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "default_tools": [ + "jlink" + ], + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "cmsis-dap", + "jlink" + ], + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino", + "mbed", + "zephyr" + ], + "name": "Nordic nRF52840-DK", + "upload": { + "maximum_ram_size": 262144, + "maximum_size": 1048576, + "protocol": "jlink", + "protocols": [ + "jlink", + "nrfjprog", + "stlink", + "blackmagic", + "cmsis-dap", + "mbed" + ] + }, + "url": "https://os.mbed.com/platforms/Nordic-nRF52840-DK/", + "vendor": "Nordic" +} diff --git a/v1.5/lib/FastLED/ci/boards/rpipico.json b/v1.5/lib/FastLED/ci/boards/rpipico.json new file mode 100644 index 0000000..5264e4d --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/rpipico.json @@ -0,0 +1,56 @@ +{ + "build": { + "arduino": { + "earlephilhower": { + "boot2_source": "boot2_w25q080_2_padded_checksum.S", + "usb_vid": "0x2E8A", + "usb_pid": "0x000A" + } + }, + "core": "earlephilhower", + "cpu": "cortex-m0plus", + "extra_flags": "-DARDUINO_RASPBERRY_PI_PICO -DARDUINO_ARCH_RP2040 -DUSBD_MAX_POWER_MA=250 ", + "f_cpu": "133000000L", + "hwids": [ + [ + "0x2E8A", + "0x00C0" + ], + [ + "0x2E8A", + "0x000A" + ] + ], + "mcu": "rp2040", + "variant": "rpipico" + }, + "debug": { + "jlink_device": "RP2040_M0_0", + "openocd_target": "rp2040.cfg", + "svd_path": "rp2040.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Pico", + "upload": { + "maximum_ram_size": 262144, + "maximum_size": 2097152, + "require_upload_port": true, + "native_usb": true, + "use_1200bps_touch": true, + "wait_for_upload_port": false, + "protocol": "picotool", + "protocols": [ + "blackmagic", + "cmsis-dap", + "jlink", + "raspberrypi-swd", + "picotool", + "picoprobe", + "pico-debug" + ] + }, + "url": "https://www.raspberrypi.org/products/raspberry-pi-pico/", + "vendor": "Raspberry Pi" +} \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/boards/rpipico2.json b/v1.5/lib/FastLED/ci/boards/rpipico2.json new file mode 100644 index 0000000..7c5d4d4 --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/rpipico2.json @@ -0,0 +1,56 @@ +{ + "build": { + "arduino": { + "earlephilhower": { + "boot2_source": "boot2_generic_03h_2_padded_checksum.S", + "usb_vid": "0x2E8A", + "usb_pid": "0x000B" + } + }, + "core": "earlephilhower", + "cpu": "cortex-m33", + "extra_flags": "-D ARDUINO_RASPBERRY_PI_PICO_2 -DARDUINO_ARCH_RP2350 -DUSBD_MAX_POWER_MA=250", + "f_cpu": "133000000L", + "hwids": [ + [ + "0x2E8A", + "0x00C0" + ], + [ + "0x2E8A", + "0x000B" + ] + ], + "mcu": "rp2350", + "variant": "rpipico2" + }, + "debug": { + "jlink_device": "RP2350_0", + "openocd_target": "rp2350.cfg", + "svd_path": "rp2350.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Pico 2", + "upload": { + "maximum_ram_size": 524288, + "maximum_size": 2097152, + "require_upload_port": true, + "native_usb": true, + "use_1200bps_touch": true, + "wait_for_upload_port": false, + "protocol": "picotool", + "protocols": [ + "blackmagic", + "cmsis-dap", + "jlink", + "raspberrypi-swd", + "picotool", + "picoprobe", + "pico-debug" + ] + }, + "url": "https://www.raspberrypi.org/products/raspberry-pi-pico/", + "vendor": "Raspberry Pi" +} diff --git a/v1.5/lib/FastLED/ci/boards/rpipicow.json b/v1.5/lib/FastLED/ci/boards/rpipicow.json new file mode 100644 index 0000000..3e1e9f6 --- /dev/null +++ b/v1.5/lib/FastLED/ci/boards/rpipicow.json @@ -0,0 +1,56 @@ +{ + "build": { + "arduino": { + "earlephilhower": { + "boot2_source": "boot2_w25q080_2_padded_checksum.S", + "usb_vid": "0x2E8A", + "usb_pid": "0xF00A" + } + }, + "core": "earlephilhower", + "cpu": "cortex-m0plus", + "extra_flags": "-DARDUINO_RASPBERRY_PI_PICO_W -DARDUINO_ARCH_RP2040 -DUSBD_MAX_POWER_MA=250 ", + "f_cpu": "133000000L", + "hwids": [ + [ + "0x2E8A", + "0x00C0" + ], + [ + "0x2E8A", + "0xF00A" + ] + ], + "mcu": "rp2040", + "variant": "rpipicow" + }, + "debug": { + "jlink_device": "RP2040_M0_0", + "openocd_target": "rp2040.cfg", + "svd_path": "rp2040.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Pico W", + "upload": { + "maximum_ram_size": 262144, + "maximum_size": 2097152, + "require_upload_port": true, + "native_usb": true, + "use_1200bps_touch": true, + "wait_for_upload_port": false, + "protocol": "picotool", + "protocols": [ + "blackmagic", + "cmsis-dap", + "jlink", + "raspberrypi-swd", + "picotool", + "picoprobe", + "pico-debug" + ] + }, + "url": "https://www.raspberrypi.org/products/raspberry-pi-pico/", + "vendor": "Raspberry Pi" +} \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/build_api.py b/v1.5/lib/FastLED/ci/build_api.py new file mode 100644 index 0000000..bbd45ac --- /dev/null +++ b/v1.5/lib/FastLED/ci/build_api.py @@ -0,0 +1,643 @@ +#!/usr/bin/env python3 +""" +FastLED Unified Build API + +This module provides a unified build system for both unit tests and examples, +centralizing PCH and library building while providing a simple, elegant API. + +Key Features: +- Unified API for both unit tests and examples +- Automatic PCH header building and reuse +- Automatic libfastled.a building and reuse +- Organized build directories under .build/ +- Efficient multiple target building +- Simple, elegant interface + +Usage: + # Unit test building + unit_builder = BuildAPI( + config_file="ci/build_unit.toml", + build_dir=".build/unit", + build_type=BuildType.UNIT_TEST + ) + unit_builder.build_targets(["test_json.cpp", "test_color.cpp"]) + + # Example building + example_builder = BuildAPI( + config_file="ci/build_example.toml", + build_dir=".build/examples", + build_type=BuildType.EXAMPLE + ) + example_builder.build_targets(["Blink.ino", "DemoReel100.ino"]) +""" + +import os +import shutil +import subprocess +import tempfile +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple + +from ci.ci.fingerprint_cache import FingerprintCache +from ci.compiler.clang_compiler import ( + BuildFlags, + Compiler, + CompilerOptions, + LibarchiveOptions, +) +from ci.compiler.test_example_compilation import get_fastled_core_sources +from ci.util.paths import PROJECT_ROOT +from ci.util.running_process import subprocess_run + + +class BuildType(Enum): + """Build type enumeration""" + + UNIT_TEST = "unit_test" + EXAMPLE = "example" + + +@dataclass +class BuildTarget: + """Represents a single build target (test file or example)""" + + source_file: Path + name: str + output_dir: Path + + def get_output_executable(self) -> Path: + """Get the expected output executable path""" + if self.source_file.suffix == ".cpp": + # Unit test: test_name -> test_name.exe + base_name = self.source_file.stem + return self.output_dir / f"{base_name}.exe" + else: + # Example: Blink.ino -> Blink.exe + base_name = self.source_file.stem + return self.output_dir / f"{base_name}.exe" + + +@dataclass +class BuildResult: + """Result of a build operation""" + + target: BuildTarget + success: bool + message: str + build_time: float + + +class BuildAPI: + """ + Unified build API for both unit tests and examples. + + This class centralizes all build operations including PCH building, + library creation, and target compilation. It provides a simple, + elegant interface while handling the complexity internally. + """ + + def __init__( + self, + build_flags_toml: str | Path, + build_dir: str | Path, + build_type: BuildType, + use_pch: bool = True, + parallel: bool = True, + clean: bool = False, + ): + """ + Initialize the build API. + + Args: + build_flags_toml: Path to build_flags.toml file (build_unit.toml or build_example.toml) + build_dir: Directory for build outputs (e.g., .build/unit or .build/examples) + build_type: Type of build (UNIT_TEST or EXAMPLE) + use_pch: Whether to use precompiled headers + parallel: Whether to enable parallel compilation + clean: Whether to force a clean build + """ + self.build_flags_toml = Path(build_flags_toml) + self.build_dir = Path(build_dir) + self.build_type = build_type + self.use_pch = use_pch + self.parallel = parallel + self.clean = clean + + # Ensure build directory exists + self.build_dir.mkdir(parents=True, exist_ok=True) + + # Set up subdirectories for organization + self.artifacts_dir = self.build_dir / "artifacts" # PCH, lib files + self.targets_dir = self.build_dir / "targets" # Individual target builds + self.cache_dir = self.build_dir / "cache" # Fingerprint caches + + for directory in [self.artifacts_dir, self.targets_dir, self.cache_dir]: + directory.mkdir(parents=True, exist_ok=True) + + # Initialize internal state + self._compiler: Optional[Compiler] = None + self._pch_file: Optional[Path] = None + self._library_file: Optional[Path] = None + self._fingerprint_cache: Optional[FingerprintCache] = None + + # Parse build flags from TOML file + self._parse_build_flags() + + # Create compiler from TOML configuration + self._create_compiler() + + def _parse_build_flags(self) -> None: + """Parse build flags from TOML file""" + if not self.build_flags_toml.exists(): + raise RuntimeError( + f"Build flags TOML file not found: {self.build_flags_toml}" + ) + + print(f"[BUILD API] Parsing build flags from {self.build_flags_toml}") + + # Parse build flags from TOML + self.build_flags = BuildFlags.from_toml_file( + self.build_flags_toml, + quick_build=True, # Use quick build mode for faster iteration + strict_mode=False, + ) + + # Initialize fingerprint cache + cache_file = self.cache_dir / "fingerprint_cache.json" + self._fingerprint_cache = FingerprintCache(cache_file) + + def _create_compiler(self) -> None: + """Create compiler from TOML configuration ONLY - no inline settings allowed""" + from ci.compiler.clang_compiler import create_compiler_options_from_toml + + # ALL settings must come from the TOML file - NO hardcoded/inline settings allowed + print(f"[BUILD API] Creating compiler from TOML configuration") + + # Set up PCH output path + pch_output_path: Optional[str] = None + if self.use_pch: + pch_output_path = str( + self.artifacts_dir / f"{self.build_type.value}_pch.hpp.pch" + ) + + # Use the proper function to create CompilerOptions from TOML - NO inline settings + self.compiler_settings = create_compiler_options_from_toml( + toml_path=self.build_flags_toml, + include_path=str(PROJECT_ROOT / "src"), # Base include path only + quick_build=True, # Use quick build mode for faster iteration + strict_mode=False, + # Pass PCH and parallel settings from constructor + use_pch=self.use_pch, + pch_output_path=pch_output_path, + parallel=self.parallel, + ) + + # Create compiler instance with TOML-loaded settings + self._compiler = Compiler(self.compiler_settings, self.build_flags) + + def _ensure_pch_built(self) -> bool: + """ + Ensure PCH header is built and ready. + + Returns: + bool: True if PCH is ready, False otherwise + """ + if not self.use_pch or not self._compiler: + return True + + print(f"[BUILD API] Building PCH header for {self.build_type.value}") + + # Check if we need to rebuild PCH + if not self.clean and self._pch_file and self._pch_file.exists(): + # TODO: Add proper dependency checking here + print(f"[BUILD API] Reusing existing PCH: {self._pch_file}") + return True + + # Build PCH + success = self._compiler.create_pch_file() + if success and self.compiler_settings.pch_output_path: + self._pch_file = Path(self.compiler_settings.pch_output_path) + print(f"[BUILD API] PCH built successfully: {self._pch_file}") + else: + print(f"[BUILD API] PCH build failed") + + return success + + def _ensure_library_built(self) -> bool: + """ + Ensure libfastled.a is built and ready. + + Returns: + bool: True if library is ready, False otherwise + """ + if not self._compiler: + return False + + lib_name = f"libfastled_{self.build_type.value}.a" + self._library_file = self.artifacts_dir / lib_name + + print(f"[BUILD API] Building FastLED library for {self.build_type.value}") + + # Check if we need to rebuild library + if not self.clean and self._library_file.exists(): + # TODO: Add proper dependency checking here + print(f"[BUILD API] Reusing existing library: {self._library_file}") + return True + + # Prefer UNITY chunk build for faster library creation + try: + unity_dir = self.artifacts_dir / "unity" + unity_dir.mkdir(parents=True, exist_ok=True) + + # Collect FastLED core sources (consistent with example compilation) + all_sources = [ + p for p in get_fastled_core_sources() if p.name != "stub_main.cpp" + ] + if not all_sources: + print("[BUILD API] No FastLED sources found for unity build") + return False + + # Deterministic sort relative to project root + project_root = PROJECT_ROOT + + def sort_key(p: Path) -> str: + try: + rel = p.relative_to(project_root) + except Exception: + rel = p + return rel.as_posix() + + all_sources = sorted(all_sources, key=sort_key) + + # Partition into chunks (1..4 typical) + total = len(all_sources) + max_chunks = min(4, max(1, (os.cpu_count() or 2) // 2)) + chunks = max_chunks if total >= max_chunks else max(1, total) + + # Prepare compile options (PCH off for unity) + compile_opts = CompilerOptions( + include_path=self._compiler.settings.include_path, + compiler=self._compiler.settings.compiler, + defines=self._compiler.settings.defines, + std_version=self._compiler.settings.std_version, + compiler_args=self._compiler.settings.compiler_args, + use_pch=False, + additional_flags=["-c"], + parallel=self.parallel, + ) + + # Compute chunk sizes + base = total // chunks + rem = total % chunks + start = 0 + unity_objects: list[Path] = [] + + for i in range(chunks): + size = base + (1 if i < rem else 0) + end = start + size + group = all_sources[start:end] + start = end + + if not group: + continue + + unity_cpp = unity_dir / f"unity{i + 1}.cpp" + # Use synchronous path to simplify error handling here + result = self._compiler._compile_unity_sync( + compile_opts, + group, + unity_output_path=unity_cpp, + ) + if not result.ok: + print(f"[BUILD API] Unity chunk {i + 1} failed: {result.stderr}") + return False + unity_objects.append(unity_cpp.with_suffix(".o")) + + if not unity_objects: + print("[BUILD API] No unity objects produced") + return False + + # Archive unity objects into the final library + archive_result = self._compiler.create_archive( + unity_objects, self._library_file, LibarchiveOptions() + ).result() + if not archive_result.ok: + print(f"[BUILD API] Archive creation failed: {archive_result.stderr}") + return False + + print( + f"[BUILD API] Library built successfully (UNITY): {self._library_file}" + ) + return True + except Exception as e: + print(f"[BUILD API] UNITY library build failed with exception: {e}") + return False + + def _create_static_library( + self, object_files: List[Path], output_lib: Path + ) -> bool: + """ + Create static library from object files. + + Args: + object_files: List of object files to archive + output_lib: Output library file path + + Returns: + bool: True if successful, False otherwise + """ + if not object_files: + return False + + # Use archiver from build flags + archiver_cmd = self.build_flags.tools.archiver + archive_flags = self.build_flags.archive.flags + + # Build archiver command + cmd = ( + archiver_cmd + + [archive_flags] + + [str(output_lib)] + + [str(f) for f in object_files] + ) + + print(f"[BUILD API] Creating static library: {' '.join(cmd)}") + + # Execute archiver command using stdout-pumping wrapper + try: + _completed = subprocess_run( + command=cmd, + cwd=None, + check=True, + timeout=900, + enable_stack_trace=False, + ) + return True + except subprocess.CalledProcessError as e: + print(f"[BUILD API] Archiver failed: {e}") + print(f"[BUILD API] Archiver stdout: {e.stdout}") + print(f"[BUILD API] Archiver stderr: {e.stderr}") + return False + + def build_targets(self, target_files: Sequence[str | Path]) -> List[BuildResult]: + """ + Build multiple targets efficiently. + + Args: + target_files: List of source files to build + + Returns: + List[BuildResult]: Results for each target + """ + print( + f"[BUILD API] Building {len(target_files)} targets for {self.build_type.value}" + ) + + # Ensure shared artifacts are built first + if not self._ensure_pch_built(): + return [ + BuildResult( + target=BuildTarget(Path(f), Path(f).stem, self.targets_dir), + success=False, + message="PCH build failed", + build_time=0.0, + ) + for f in target_files + ] + + if not self._ensure_library_built(): + return [ + BuildResult( + target=BuildTarget(Path(f), Path(f).stem, self.targets_dir), + success=False, + message="Library build failed", + build_time=0.0, + ) + for f in target_files + ] + + # Build individual targets + results: List[BuildResult] = [] + for target_file in target_files: + result = self._build_single_target(Path(target_file)) + results.append(result) + + return results + + def _build_single_target(self, target_file: Path) -> BuildResult: + """ + Build a single target. + + Args: + target_file: Source file to build + + Returns: + BuildResult: Result of the build + """ + import time + + start_time = time.time() + + # Create build target + target_dir = self.targets_dir / target_file.stem + target_dir.mkdir(parents=True, exist_ok=True) + + target = BuildTarget( + source_file=target_file, name=target_file.stem, output_dir=target_dir + ) + + print(f"[BUILD API] Building target: {target.name}") + + try: + # Compile and link the target + success = self._compile_and_link_target(target) + + build_time = time.time() - start_time + message = "Build successful" if success else "Build failed" + + return BuildResult( + target=target, success=success, message=message, build_time=build_time + ) + + except Exception as e: + build_time = time.time() - start_time + return BuildResult( + target=target, + success=False, + message=f"Build error: {e}", + build_time=build_time, + ) + + def _compile_and_link_target(self, target: BuildTarget) -> bool: + """ + Compile and link a single target. + + Args: + target: Target to build + + Returns: + bool: True if successful, False otherwise + """ + if not self._compiler: + return False + + # Compile target source file + obj_file = target.output_dir / f"{target.name}.o" + if target.source_file.suffix == ".cpp": + success = self._compiler.compile_cpp_file(target.source_file, obj_file) + else: + # For .ino files + success = self._compiler.compile_ino_file(target.source_file, obj_file) + + if not success: + print(f"[BUILD API] Failed to compile {target.source_file}") + return False + + # Link with library to create executable + exe_file = target.get_output_executable() + success = self._link_executable([obj_file], exe_file) + + if not success: + print(f"[BUILD API] Failed to link {exe_file}") + return False + + print(f"[BUILD API] Successfully built {exe_file}") + return True + + def _link_executable(self, object_files: List[Path], output_exe: Path) -> bool: + """ + Link object files into an executable. + + Args: + object_files: Object files to link + output_exe: Output executable path + + Returns: + bool: True if successful, False otherwise + """ + if not self._library_file or not self._library_file.exists(): + print(f"[BUILD API] Library not available for linking") + return False + + # Build linker command + linker_cmd = self.build_flags.tools.linker + link_flags = self.build_flags.link_flags + + cmd = ( + linker_cmd + + link_flags + + [str(f) for f in object_files] + + [str(self._library_file)] + + ["-o", str(output_exe)] + ) + cmd_str = subprocess.list2cmdline(cmd) + + print(f"[BUILD API] Linking: {cmd_str}") + + # Execute linker using stdout-pumping wrapper + try: + _completed = subprocess_run( + command=cmd, + cwd=None, + check=True, + timeout=900, + enable_stack_trace=False, + ) + return True + except subprocess.CalledProcessError as e: + print(f"[BUILD API] Linker failed: {e}") + print(f"[BUILD API] Linker stdout: {e.stdout}") + print(f"[BUILD API] Linker stderr: {e.stderr}") + return False + + def get_build_info(self) -> Dict[str, Any]: + """Get information about the current build configuration""" + return { + "build_flags_toml": str(self.build_flags_toml), + "build_dir": str(self.build_dir), + "build_type": self.build_type.value, + "use_pch": self.use_pch, + "parallel": self.parallel, + "pch_file": str(self._pch_file) if self._pch_file else None, + "library_file": str(self._library_file) if self._library_file else None, + } + + def clean_build_artifacts(self) -> None: + """Clean all build artifacts""" + print(f"[BUILD API] Cleaning build artifacts in {self.build_dir}") + + if self.build_dir.exists(): + shutil.rmtree(self.build_dir) + self.build_dir.mkdir(parents=True, exist_ok=True) + + # Recreate subdirectories + for directory in [self.artifacts_dir, self.targets_dir, self.cache_dir]: + directory.mkdir(parents=True, exist_ok=True) + + # Reset internal state + self._pch_file = None + self._library_file = None + if self._fingerprint_cache: + # Clear the cache by resetting it + cache_file = self.cache_dir / "fingerprint_cache.json" + self._fingerprint_cache = FingerprintCache(cache_file) + + +def create_unit_test_builder( + build_dir: str | Path = ".build/unit", + use_pch: bool = True, + parallel: bool = True, + clean: bool = False, +) -> BuildAPI: + """ + Create a BuildAPI instance for unit tests. + + Args: + build_dir: Build directory for unit test outputs + use_pch: Whether to use precompiled headers + parallel: Whether to enable parallel compilation + clean: Whether to force a clean build + + Returns: + BuildAPI: Configured build API for unit tests + """ + build_flags_toml = PROJECT_ROOT / "ci" / "build_unit.toml" + return BuildAPI( + build_flags_toml=build_flags_toml, + build_dir=build_dir, + build_type=BuildType.UNIT_TEST, + use_pch=use_pch, + parallel=parallel, + clean=clean, + ) + + +def create_example_builder( + build_dir: str | Path = ".build/examples", + use_pch: bool = True, + parallel: bool = True, + clean: bool = False, +) -> BuildAPI: + """ + Create a BuildAPI instance for examples. + + Args: + build_dir: Build directory for example outputs + use_pch: Whether to use precompiled headers + parallel: Whether to enable parallel compilation + clean: Whether to force a clean build + + Returns: + BuildAPI: Configured build API for examples + """ + build_flags_toml = PROJECT_ROOT / "ci" / "build_example.toml" + return BuildAPI( + build_flags_toml=build_flags_toml, + build_dir=build_dir, + build_type=BuildType.EXAMPLE, + use_pch=use_pch, + parallel=parallel, + clean=clean, + ) diff --git a/v1.5/lib/FastLED/ci/build_commands.toml b/v1.5/lib/FastLED/ci/build_commands.toml new file mode 100644 index 0000000..58e3360 --- /dev/null +++ b/v1.5/lib/FastLED/ci/build_commands.toml @@ -0,0 +1,32 @@ +[all] +# Universal defines and compiler flags for compile-commands generation +defines = [ + "STUB_PLATFORM", + "__EMSCRIPTEN__" +] +compiler_flags = [ + "-std=gnu++17", + "-fpermissive", + "-Wall", + "-Wextra" +] +# Include flags are passed verbatim; use -I and -isystem as needed +include_flags = [ + "-Isrc", + "-isystemsrc/platforms/stub" +] + +[tools] +# Tool configuration required by BuildFlags.parse +cpp_compiler = ["uv", "run", "python", "-m", "ziglang", "c++"] +linker = ["uv", "run", "python", "-m", "ziglang", "c++"] +archiver = ["uv", "run", "python", "-m", "ziglang", "ar"] +c_compiler = ["uv", "run", "python", "-m", "ziglang", "cc"] +objcopy = ["uv", "run", "python", "-m", "ziglang", "objcopy"] +nm = ["uv", "run", "python", "-m", "ziglang", "nm"] +strip = ["uv", "run", "python", "-m", "ziglang", "strip"] +ranlib = ["uv", "run", "python", "-m", "ziglang", "ranlib"] + +[archive] +# Required by BuildFlags.parse; not used for compile-commands generation +flags = "rcsD" diff --git a/v1.5/lib/FastLED/ci/build_example.toml b/v1.5/lib/FastLED/ci/build_example.toml new file mode 100644 index 0000000..853a6b5 --- /dev/null +++ b/v1.5/lib/FastLED/ci/build_example.toml @@ -0,0 +1,181 @@ +# ================================================================================================ +# FastLED Example Build Flags Configuration +# ================================================================================================ +# +# This file centralizes ALL compilation flags used by example compilation to ensure +# they remain synchronized and compatible across the entire build system. +# +# Examples use the STUB platform for Arduino-compatible functionality without hardware +# dependencies, but do NOT need unit test specific flags like FASTLED_FORCE_NAMESPACE. + +[tools] +# Build tool configuration - Full compiler command with arguments +# CRITICAL: Must use 'uv run python' for subprocess execution (subprocess doesn't inherit uv environment) +cpp_compiler = ["uv", "run", "python", "-m", "ziglang", "c++"] +linker = ["uv", "run", "python", "-m", "ziglang", "c++"] # Separate linker config for consistency +archiver = ["uv", "run", "python", "-m", "ziglang", "ar"] # Ziglang archiver for unified toolchain +c_compiler = ["uv", "run", "python", "-m", "ziglang", "cc"] +objcopy = ["uv", "run", "python", "-m", "ziglang", "objcopy"] +nm = ["uv", "run", "python", "-m", "ziglang", "nm"] +strip = ["uv", "run", "python", "-m", "ziglang", "strip"] +ranlib = ["uv", "run", "python", "-m", "ziglang", "ranlib"] + +[all] +# Universal compilation flags (compiler command handled separately) +compiler_flags = [ + "-std=gnu++17", + "-fpermissive", + "-Wall", + "-Wextra", + "-Wno-deprecated-register", # Suppress 'register' keyword warnings for C++17 compatibility + "-Wno-backslash-newline-escape", # Suppress backslash newline escape warnings + "-fno-exceptions", + "-fno-rtti", +] + +defines = [ + "STUB_PLATFORM", + # NOTE: Examples do NOT get FASTLED_FORCE_NAMESPACE=1 - they use global namespace +] + +include_flags = [ + "-I.", + "-Isrc", +] + +[windows] +# ================================================================================================ +# WINDOWS-SPECIFIC BUILD FLAGS +# ================================================================================================ +# Comprehensive Windows build configuration using Zig's bundled Clang with GNU target + +# C compiler flags for Windows +cc_flags = [ + "--target=x86_64-windows-gnu", # Explicit target for MSYS2/MinGW compatibility + "-fuse-ld=lld", # Use Zig's bundled LLD linker + "-std=c17", # C17 standard + "-Wall", + "-Wextra", +] + +# C++ compiler flags for Windows +cpp_flags = [ + "--target=x86_64-windows-gnu", # Explicit target for MSYS2/MinGW compatibility + "-fuse-ld=lld-link", # Use lld-link (Windows MSVC-compatible linker) + "-std=c++17", # C++17 standard + "-Wall", + "-Wextra", + "-fno-exceptions", + "-fno-rtti", + + # Fix Arduino INPUT macro conflict with Windows headers + "-DNOMINMAX", # Prevent Windows min/max macros + "-DWIN32_LEAN_AND_MEAN", # Reduce Windows header conflicts +] + +# Linker flags for Windows +link_flags = [ + "-mconsole", # Console application + + # Prevent automatic library inclusion to avoid CRT collisions + "-nodefaultlibs", # Don't automatically include default libraries + + # Fix CRT collision between Zig/Clang and MSYS2/MinGW runtimes + # This prevents "atexit was replaced" errors from conflicting CRT implementations + # "-rtlib=compiler-rt", # Removed - was causing conflicts + "-unwindlib=libunwind", # Use Clang's libunwind (avoid MinGW unwind) + "-nostdlib++", # Don't auto-link standard C++ library + "-lc++", # Manually link libc++ (Clang's C++ standard library) + + "-lkernel32", # Windows kernel functions + "-luser32", # Windows user interface + "-lgdi32", # Graphics device interface + "-ladvapi32", # Advanced Windows API +] + +[linking.base] +# Base linking flags (Unix-like systems) +flags = [ + "-pthread", # POSIX threads (not used on Windows) +] + +[build_modes.quick] +flags = [ + "-O1", + "-g0", + "-fno-inline-functions", + "-fno-vectorize", + "-fno-unroll-loops", + "-fno-strict-aliasing", +] + +link_flags = [] + +[strict_mode] +flags = [ + "-Werror", + "-Wextra", + "-Wconversion", + "-Wsign-conversion", + "-Werror=unused-variable", # Force error on unused variables + "-Werror=unused-parameter", # Force error on unused parameters + "-Werror=unused-function", # Force error on unused functions + "-Werror=unused-but-set-variable", # Force error on variables that are set but never used + "-Wuninitialized", + "-Wdouble-promotion", + "-Wformat=2", + "-Wcast-align", + "-Wcast-qual", + "-Werror=return-type", +] + +[stub_platform] +# ================================================================================================ +# STUB PLATFORM DEFINES FOR EXAMPLE COMPILATION +# ================================================================================================ +# These defines are used for compiling examples on the STUB platform +# which provides Arduino-compatible functionality without hardware dependencies. + +defines = [ + "STUB_PLATFORM", # Enable STUB platform + "ARDUINO=10808", # Arduino version compatibility + "FASTLED_USE_STUB_ARDUINO", # Use stub Arduino implementation + "FASTLED_STUB_IMPL", # Enable STUB platform Arduino function implementations + "SKETCH_HAS_LOTS_OF_MEMORY=1", # Enable memory-intensive features for STUB platform + "FASTLED_HAS_ENGINE_EVENTS=1", # Enable EngineEvents for UI components +] + +# Include paths required for stub platform compilation +include_paths = [ + "src/platforms/stub", # Arduino.h stub for examples +] + +# Additional compiler arguments for stub platform +compiler_args = [ + # No additional args currently needed - all flags come from [all] and [build_modes.quick] sections +] + +[archive] +# ================================================================================================ +# ARCHIVE CREATION FLAGS +# ================================================================================================ +# Flags for creating static libraries (.a files) with deterministic builds. +# Using Zig's unified toolchain (ziglang ar) which supports deterministic builds on all platforms. + +flags = "rcsD" # r=insert, c=create if needed, s=write symbol table, D=deterministic (zero timestamps) + +[linking.unix] +# ================================================================================================ +# UNIX-SPECIFIC LINKING FLAGS +# ================================================================================================ +# Linking flags specific to Unix-like systems (Linux, macOS). + +flags = [ + # Core Libraries + "-pthread", # POSIX threads support + "-lm", # Math library + "-ldl", # Dynamic loading library + + # Platform-specific libraries (added as needed) + # Note: Additional platform-specific flags can be added here +] diff --git a/v1.5/lib/FastLED/ci/build_example_usage.py b/v1.5/lib/FastLED/ci/build_example_usage.py new file mode 100644 index 0000000..f435f52 --- /dev/null +++ b/v1.5/lib/FastLED/ci/build_example_usage.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +""" +FastLED Unified Build API - Example Usage + +This script demonstrates how to use the new unified build API to replace +the old scattered build logic for both unit tests and examples. + +Before (scattered logic): +- PCH building logic in multiple places +- libfastled.a building in callers +- Build flags mixed between unit tests and examples +- Manual directory management + +After (unified API): +- Single API for both unit tests and examples +- Automatic PCH and library building +- Clean separation of build configurations +- Organized build directory structure +""" + +from pathlib import Path + +from ci.build_api import BuildType, create_example_builder, create_unit_test_builder + + +def demo_unit_test_building(): + """Demonstrate unit test building with the unified API""" + print("=" * 60) + print("UNIT TEST BUILDING DEMO") + print("=" * 60) + + # Create unit test builder - automatically uses ci/build_unit.toml + unit_builder = create_unit_test_builder( + build_dir=".build/unit", use_pch=True, parallel=True, clean=False + ) + + # Get build info + info = unit_builder.get_build_info() + print(f"Build configuration: {info}") + + # Build multiple unit test targets efficiently + # The API automatically handles: + # - PCH building (once, reused by all targets) + # - libfastled.a building (once, reused by all targets) + # - Individual target compilation and linking + test_files = [ + Path("tests/test_json.cpp"), + Path("tests/test_color.cpp"), + Path("tests/test_math.cpp"), + ] + + print(f"\nBuilding {len(test_files)} unit test targets...") + results = unit_builder.build_targets(test_files) + + # Report results + for result in results: + status = "SUCCESS" if result.success else "FAILED" + print(f" {result.target.name}: {status} ({result.build_time:.2f}s)") + if not result.success: + print(f" Error: {result.message}") + + print(f"\nBuild artifacts organized in: {unit_builder.build_dir}") + print(f" - PCH file: {unit_builder.artifacts_dir}") + print(f" - Library: {unit_builder.artifacts_dir}") + print(f" - Targets: {unit_builder.targets_dir}") + print(f" - Cache: {unit_builder.cache_dir}") + + +def demo_example_building(): + """Demonstrate example building with the unified API""" + print("\n" + "=" * 60) + print("EXAMPLE BUILDING DEMO") + print("=" * 60) + + # Create example builder - automatically uses ci/build_example.toml + example_builder = create_example_builder( + build_dir=".build/examples", use_pch=True, parallel=True, clean=False + ) + + # Get build info + info = example_builder.get_build_info() + print(f"Build configuration: {info}") + + # Build multiple example targets efficiently + # The API automatically handles: + # - PCH building (once, reused by all targets) + # - libfastled.a building (once, with example-specific flags) + # - Individual target compilation and linking + example_files = [ + Path("examples/Blink/Blink.ino"), + Path("examples/DemoReel100/DemoReel100.ino"), + Path("examples/ColorPalette/ColorPalette.ino"), + ] + + print(f"\nBuilding {len(example_files)} example targets...") + results = example_builder.build_targets(example_files) + + # Report results + for result in results: + status = "SUCCESS" if result.success else "FAILED" + print(f" {result.target.name}: {status} ({result.build_time:.2f}s)") + if not result.success: + print(f" Error: {result.message}") + + print(f"\nBuild artifacts organized in: {example_builder.build_dir}") + print(f" - PCH file: {example_builder.artifacts_dir}") + print(f" - Library: {example_builder.artifacts_dir}") + print(f" - Targets: {example_builder.targets_dir}") + print(f" - Cache: {example_builder.cache_dir}") + + +def demo_build_configuration_differences(): + """Demonstrate the differences between unit test and example configurations""" + print("\n" + "=" * 60) + print("BUILD CONFIGURATION DIFFERENCES") + print("=" * 60) + + unit_builder = create_unit_test_builder() + example_builder = create_example_builder() + + unit_info = unit_builder.get_build_info() + example_info = example_builder.get_build_info() + + print("Unit Test Configuration:") + print(f" - Build flags TOML: {unit_info['build_flags_toml']}") + print(f" - Build directory: {unit_info['build_dir']}") + print(f" - Build type: {unit_info['build_type']}") + print(f" - Includes FASTLED_FORCE_NAMESPACE=1") + print(f" - Includes FASTLED_TESTING=1") + + print("\nExample Configuration:") + print(f" - Build flags TOML: {example_info['build_flags_toml']}") + print(f" - Build directory: {example_info['build_dir']}") + print(f" - Build type: {example_info['build_type']}") + print(f" - Uses global namespace (no FASTLED_FORCE_NAMESPACE)") + print(f" - Arduino compatibility defines") + + +def demo_clean_builds(): + """Demonstrate clean build functionality""" + print("\n" + "=" * 60) + print("CLEAN BUILD DEMO") + print("=" * 60) + + # Create builders with clean=True to force rebuilding of all artifacts + unit_builder = create_unit_test_builder(clean=True) + example_builder = create_example_builder(clean=True) + + print("Unit test builder:") + print(f" - Will clean and rebuild all artifacts in {unit_builder.build_dir}") + + print("Example builder:") + print(f" - Will clean and rebuild all artifacts in {example_builder.build_dir}") + + # Demonstrate manual cleaning + print("\nManual cleaning:") + unit_builder.clean_build_artifacts() + print(f" - Cleaned unit test artifacts") + + example_builder.clean_build_artifacts() + print(f" - Cleaned example artifacts") + + +def demo_advanced_usage(): + """Demonstrate advanced usage patterns""" + print("\n" + "=" * 60) + print("ADVANCED USAGE DEMO") + print("=" * 60) + + # Custom build directory structure + unit_builder = create_unit_test_builder( + build_dir=".build/my_custom_unit_tests", + use_pch=False, # Disable PCH for debugging + parallel=False, # Disable parallel for single-threaded debugging + ) + + print("Custom configuration:") + print(f" - Custom build directory: {unit_builder.build_dir}") + print(f" - PCH disabled for easier debugging") + print(f" - Parallel compilation disabled for sequential output") + + # Show the organized directory structure + print(f"\nOrganized directory structure:") + print(f" {unit_builder.build_dir}/") + print(f" ├── artifacts/ # PCH files and libfastled.a") + print(f" ├── targets/ # Individual target builds") + print(f" │ ├── test_json/") + print(f" │ │ ├── test_json.o") + print(f" │ │ └── test_json.exe") + print(f" │ └── test_color/") + print(f" │ ├── test_color.o") + print(f" │ └── test_color.exe") + print(f" └── cache/ # Fingerprint caches") + + +if __name__ == "__main__": + print("FastLED Unified Build API - Example Usage") + print("This demonstrates the new unified build system") + + # Run all demos (commented out to avoid actual compilation during demo) + + # Uncomment these to run actual builds: + # demo_unit_test_building() + # demo_example_building() + + # These can run without actual building: + demo_build_configuration_differences() + demo_clean_builds() + demo_advanced_usage() + + print("\n" + "=" * 60) + print("DEMO COMPLETE") + print("=" * 60) + print("The unified build API provides:") + print("✓ Single API for both unit tests and examples") + print("✓ Automatic PCH and library building") + print("✓ Clean separation of build configurations") + print("✓ Organized build directory structure") + print("✓ Efficient multiple target building") + print("✓ Simple, elegant interface") diff --git a/v1.5/lib/FastLED/ci/build_unit.toml b/v1.5/lib/FastLED/ci/build_unit.toml new file mode 100644 index 0000000..6448121 --- /dev/null +++ b/v1.5/lib/FastLED/ci/build_unit.toml @@ -0,0 +1,276 @@ +# ================================================================================================ +# FastLED Unit Test Build Flags Configuration +# ================================================================================================ +# +# This file centralizes ALL compilation flags used by unit test compilation to ensure +# they remain synchronized and compatible across the entire build system. +# +# Unit tests require special flags like FASTLED_FORCE_NAMESPACE=1 to put all symbols +# in the fl:: namespace that the tests expect. + +[tools] +# Build tool configuration - Full compiler command with arguments +# CRITICAL: Must use 'uv run python' for subprocess execution (subprocess doesn't inherit uv environment) +cpp_compiler = ["uv", "run", "python", "-m", "ziglang", "c++"] +linker = ["uv", "run", "python", "-m", "ziglang", "c++"] # Separate linker config for consistency +archiver = ["uv", "run", "python", "-m", "ziglang", "ar"] # Ziglang archiver for unified toolchain +c_compiler = ["uv", "run", "python", "-m", "ziglang", "cc"] +objcopy = ["uv", "run", "python", "-m", "ziglang", "objcopy"] +nm = ["uv", "run", "python", "-m", "ziglang", "nm"] +strip = ["uv", "run", "python", "-m", "ziglang", "strip"] +ranlib = ["uv", "run", "python", "-m", "ziglang", "ranlib"] + +[all] +# Universal compilation flags (compiler command handled separately) +compiler_flags = [ + "-std=gnu++17", + "-fpermissive", + "-Wall", + "-Wextra", + "-Wno-deprecated-register", # Suppress 'register' keyword warnings for C++17 compatibility + "-Wno-backslash-newline-escape", # Suppress backslash newline escape warnings + "-fno-exceptions", + "-fno-rtti", +] + +defines = [ + "STUB_PLATFORM", + "FASTLED_UNIT_TEST=1", + "FASTLED_FORCE_NAMESPACE=1", # CRITICAL: Required for unit tests to access fl:: namespace + "FASTLED_TESTING=1", # Enable test-specific functionality like MockTimeProvider +] + +include_flags = [ + "-I.", + "-Isrc", + "-Itests", +] + +[windows] +# ================================================================================================ +# WINDOWS-SPECIFIC BUILD FLAGS +# ================================================================================================ +# Comprehensive Windows build configuration using Zig's bundled Clang with GNU target + +# C compiler flags for Windows +cc_flags = [ + "--target=x86_64-windows-gnu", # Explicit target for MSYS2/MinGW compatibility + "-fuse-ld=lld", # Use Zig's bundled LLD linker + "-std=c17", # C17 standard + "-Wall", + "-Wextra", +] + +# C++ compiler flags for Windows +cpp_flags = [ + "--target=x86_64-windows-gnu", # Explicit target for MSYS2/MinGW compatibility + "-fuse-ld=lld-link", # Use lld-link (Windows MSVC-compatible linker) + "-std=c++17", # C++17 standard + "-Wall", + "-Wextra", + "-fno-exceptions", + "-fno-rtti", + + # Fix Arduino INPUT macro conflict with Windows headers + "-DNOMINMAX", # Prevent Windows min/max macros + "-DWIN32_LEAN_AND_MEAN", # Reduce Windows header conflicts +] + +# Linker flags for Windows +link_flags = [ + "-mconsole", # Console application + + # Prevent automatic library inclusion to avoid CRT collisions + "-nodefaultlibs", # Don't automatically include default libraries + + # Fix CRT collision between Zig/Clang and MSYS2/MinGW runtimes + # This prevents "atexit was replaced" errors from conflicting CRT implementations + # "-rtlib=compiler-rt", # Removed - was causing conflicts + "-unwindlib=libunwind", # Use Clang's libunwind (avoid MinGW unwind) + "-nostdlib++", # Don't auto-link standard C++ library + "-lc++", # Manually link libc++ (Clang's C++ standard library) + + "-lkernel32", # Windows kernel functions + "-luser32", # Windows user interface + "-lgdi32", # Graphics device interface + "-ladvapi32", # Advanced Windows API +] + +[linking.base] +# Base linking flags (Unix-like systems) +flags = [ + "-pthread", # POSIX threads (not used on Windows) +] + +[linux] +# Linux-specific build configuration – libunwind disabled (fallback to execinfo) +link_flags = [ +] + +defines = [ +] + +[build_modes.quick] +flags = [ + "-O1", + "-fno-inline-functions", + "-fno-vectorize", + "-fno-unroll-loops", + "-fno-strict-aliasing", + "-fno-omit-frame-pointer", # Preserve stack frames for debugging +] + +link_flags = [] + +[build_modes.debug_asan] +# AddressSanitizer and UndefinedBehaviorSanitizer for debugging memory issues +flags = [ + "-fsanitize=address", # AddressSanitizer for memory error detection + "-fsanitize=undefined", # UndefinedBehaviorSanitizer for undefined behavior + "-O1", # Light optimization for performance + "-fno-omit-frame-pointer", # Preserve stack frames for better debugging + "-fno-optimize-sibling-calls", # Better stack traces +] + +link_flags = [ + "-fsanitize=address", + "-fsanitize=undefined", +] + +[strict_mode] +flags = [ + "-Werror", + "-Wextra", + "-Wconversion", + "-Wsign-conversion", + "-Werror=unused-variable", # Force error on unused variables + "-Werror=unused-parameter", # Force error on unused parameters + "-Werror=unused-function", # Force error on unused functions + "-Werror=unused-but-set-variable", # Force error on variables that are set but never used + "-Wuninitialized", + "-Wdouble-promotion", + "-Wformat=2", + "-Wcast-align", + "-Wcast-qual", + "-Werror=return-type", +] + +[test] +# Test-specific preprocessor defines +defines = [ + # FastLED Test Configuration + "-DFASTLED_UNIT_TEST=1", # Identify as unit test build + "-DFASTLED_FORCE_NAMESPACE=1", # Force namespace usage to avoid conflicts + "-DFASTLED_USE_PROGMEM=0", # Disable PROGMEM usage in tests + "-DSTUB_PLATFORM", # Use stub platform for testing + "-DARDUINO=10808", # Arduino version compatibility + "-DFASTLED_USE_STUB_ARDUINO", # Use stub Arduino implementation + "-DSKETCH_HAS_LOTS_OF_MEMORY=1", # Assume lots of memory available + "-DFASTLED_STUB_IMPL", # Use stub implementations + "-DFASTLED_USE_JSON_UI=1", # Enable JSON UI features + "-DFASTLED_TESTING", # Enable testing features + "-DFASTLED_NO_AUTO_NAMESPACE", # Disable automatic namespace + "-DFASTLED_NO_PINMAP", # Disable pin mapping + "-DHAS_HARDWARE_PIN_SUPPORT", # Enable hardware pin support + "-DFASTLED_DEBUG_LEVEL=1", # Set debug level + "-DFASTLED_NO_ATEXIT=1", # Disable atexit to fix Windows lld-link errors + "-DDOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS", # Doctest configuration + "-DENABLE_CRASH_HANDLER", # Enable global crash handler in tests +] + +# Test-specific compiler flags +compiler_flags = [ + # Language Standard and Compatibility + "-std=c++17", # Base C++17 standard + "-std=gnu++17", # Use GNU C++17 standard with extensions + "-fpermissive", # Allow some non-conforming code patterns + + # Warning Configuration + "-Wall", # Enable all warnings + "-Wextra", # Enable extra warnings + "-Werror=return-type", # Error on missing return statements + + # Debug and Crash Handling + "-fno-omit-frame-pointer", # Preserve stack frames for debugging + + # AddressSanitizer and Memory Debugging + "-fsanitize=address", # AddressSanitizer for memory error detection + "-fsanitize=undefined", # UndefinedBehaviorSanitizer for undefined behavior + + # Thread Safety and Runtime Behavior + "-fno-threadsafe-statics", # Disable thread-safe static initialization + "-fno-exceptions", # Disable C++ exceptions + "-fno-rtti", # Disable RTTI + + # PCH Support + "-fpch-instantiate-templates", # Required for proper PCH template handling +] + +# Test-specific include flags +include_flags = [ + "-I.", # Current directory + "-Isrc", # FastLED source directory + "-Itests", # Test directory +] + +# Test-specific linking flags for AddressSanitizer and libunwind +link_flags = [ + "-fsanitize=address", # Link AddressSanitizer runtime + "-fsanitize=undefined", # Link UndefinedBehaviorSanitizer runtime + "-lunwind", # Link libunwind for stack traces + "-lunwind-x86_64", # Platform-specific libunwind functions +] + +[archive] +# ================================================================================================ +# ARCHIVE CREATION FLAGS +# ================================================================================================ +# Flags for creating static libraries (.a files) with deterministic builds. +# Using Zig's unified toolchain (ziglang ar) which supports deterministic builds on all platforms. + +flags = "rcsD" # r=insert, c=create if needed, s=write symbol table, D=deterministic (zero timestamps) + +[stub_platform] +# ================================================================================================ +# STUB PLATFORM DEFINES FOR UNIT TEST COMPILATION +# ================================================================================================ +# These defines are used when unit tests need to compile example code on the STUB platform +# which provides Arduino-compatible functionality without hardware dependencies. + +defines = [ + "STUB_PLATFORM", # Enable STUB platform + "ARDUINO=10808", # Arduino version compatibility + "FASTLED_USE_STUB_ARDUINO", # Use stub Arduino implementation + "FASTLED_STUB_IMPL", # Enable STUB platform Arduino function implementations + "SKETCH_HAS_LOTS_OF_MEMORY=1", # Enable memory-intensive features for STUB platform + "FASTLED_HAS_ENGINE_EVENTS=1", # Enable EngineEvents for UI components + "FASTLED_UNIT_TEST=1", # Identify as unit test build + "FASTLED_FORCE_NAMESPACE=1", # Force namespace usage for unit tests + "FASTLED_TESTING=1", # Enable testing features +] + +# Include paths required for stub platform compilation in unit tests +include_paths = [ + "src/platforms/stub", # Arduino.h stub for examples +] + +# Additional compiler arguments for stub platform +compiler_args = [ + # No additional args currently needed - all flags come from [all] and other sections +] + +[linking.unix] +# ================================================================================================ +# UNIX-SPECIFIC LINKING FLAGS +# ================================================================================================ +# Linking flags specific to Unix-like systems (Linux, macOS). + +flags = [ + # Core Libraries + "-pthread", # POSIX threads support + "-lm", # Math library + "-ldl", # Dynamic loading library + + # Platform-specific libraries (added as needed) + # Note: Additional platform-specific flags can be added here +] diff --git a/v1.5/lib/FastLED/ci/check-js-types.py b/v1.5/lib/FastLED/ci/check-js-types.py new file mode 100644 index 0000000..cf4185d --- /dev/null +++ b/v1.5/lib/FastLED/ci/check-js-types.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +TypeScript checking for FastLED JavaScript/WASM compiler modules. + +This script runs TypeScript compiler with JSDoc checking enabled +to catch type errors in JavaScript files that would be caught +at compile time in TypeScript. +""" + +import subprocess +import sys +from pathlib import Path + + +def check_js_types(): + """Run TypeScript checking on JavaScript files.""" + wasm_compiler_dir = ( + Path(__file__).parent.parent / "src" / "platforms" / "wasm" / "compiler" + ) + + if not wasm_compiler_dir.exists(): + print(f"❌ WASM compiler directory not found: {wasm_compiler_dir}") + return 1 + + jsconfig_path = wasm_compiler_dir / "jsconfig.json" + if not jsconfig_path.exists(): + print(f"❌ jsconfig.json not found: {jsconfig_path}") + return 1 + + print(f"🔍 Checking JavaScript types in: {wasm_compiler_dir}") + + try: + # Run TypeScript compiler with noEmit to only check types + result = subprocess.run( + ["npx", "tsc", "--noEmit", "--project", str(jsconfig_path)], + cwd=wasm_compiler_dir, + capture_output=True, + text=True, + ) + + if result.returncode == 0: + print("✅ JavaScript type checking passed!") + return 0 + else: + print("❌ JavaScript type checking failed:") + print(result.stdout) + print(result.stderr) + return 1 + + except FileNotFoundError: + print( + "❌ TypeScript (tsc) not found. Please install: npm install -g typescript" + ) + return 1 + except Exception as e: + print(f"❌ Error running type check: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(check_js_types()) diff --git a/v1.5/lib/FastLED/ci/ci-check-compiled-size.py b/v1.5/lib/FastLED/ci/ci-check-compiled-size.py new file mode 100644 index 0000000..b777e67 --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-check-compiled-size.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Any, List, Match, Optional + + +HERE = Path(__file__).resolve().parent +PROJECT_ROOT = HERE.parent + +IS_GITHUB = "GITHUB_ACTIONS" in os.environ + + +def run_command( + cmd_list: List[str], + capture_output: bool = True, + shell: bool = False, + check: Optional[bool] = None, +) -> Optional[str]: + """Run a command and return its output.""" + actual_check = check if check is not None else False + cmd = cmd_list if not shell else subprocess.list2cmdline(cmd_list) + + result: subprocess.CompletedProcess[str] = subprocess.run( + cmd, capture_output=capture_output, text=True, shell=shell, check=actual_check + ) + + if not capture_output: + return None + + stdout: str = result.stdout + stdout = stdout.strip() + return stdout + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Check compiled program size for a board" + ) + parser.add_argument("board", help="Board name") + parser.add_argument( + "--max-size", type=int, required=False, help="Maximum allowed size" + ) + parser.add_argument( + "--no-build", + action="store_true", + help="Skip compilation and check existing build", + ) + parser.add_argument( + "--example", + default="Blink", + help="Example to compile (default: Blink)", + ) + + # Parse known args first + args, unknown = parser.parse_known_args() + + # Add remaining arguments as extra_args + args.extra_args = unknown + + return args + + +def main(): + os.chdir(str(PROJECT_ROOT)) + args = parse_args() + + if not args.no_build: + cmd_list = [ + "uv", + "run", + "python", + "-m", + "ci.ci-compile", + args.board, + "--examples", + args.example, + ] + args.extra_args + try: + run_command(cmd_list, shell=True, capture_output=IS_GITHUB, check=True) + except subprocess.CalledProcessError: + run_command(cmd_list, shell=True, capture_output=False, check=True) + + output = run_command( + ["uv", "run", "-m", "ci.compiled_size", "--board", args.board], + capture_output=True, + ) + size_match = re.search(r": *(\d+)", output) # type: ignore + + if not size_match: + print("Error: Unable to extract size from output") + print(f"Output: {output}") + sys.exit(1) + + size_str = size_match.group(1) # type: ignore + if not size_str: + print("Error: Size group is empty") + sys.exit(1) + size = int(size_str) # type: ignore + + if args.max_size is not None and args.max_size > 0: + max_size = args.max_size + if size > max_size: + print(f"{args.board} size {size} is greater than max size {max_size}") + print("::error::Compiled size exceeds maximum allowed size") + sys.exit(1) + else: + print(f"{args.board} size {size} is within the limit of {max_size}") + else: + if not args.max_size: + print("Warning: No max size specified") + elif args.max_size <= 0: + print("Warning: max size was <= 0 so no check was performed") + print(f"{args.board} size: {size}") + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/ci-compile b/v1.5/lib/FastLED/ci/ci-compile new file mode 100644 index 0000000..e36be1a --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-compile @@ -0,0 +1,5 @@ +#!/bin/bash + +# cd to the directory of the script +cd "$(dirname "$0")" +uv run python ci-compile.py "$@" diff --git a/v1.5/lib/FastLED/ci/ci-compile-concurrent-backup.py b/v1.5/lib/FastLED/ci/ci-compile-concurrent-backup.py new file mode 100644 index 0000000..f967a30 --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-compile-concurrent-backup.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +""" +BACKUP: Original concurrent build system - REPLACED by pio ci approach + +This file is a backup of the original ci-compile.py that used concurrent builds. +It has been replaced by a simpler pio ci approach in the main ci-compile.py. + +The original system used ThreadPoolExecutor to run multiple board compilations +in parallel with build directory creation and artifact recycling. + +The new system uses pio ci command directly for each board/example combination, +which is simpler and more straightforward. + +This backup is kept for reference and potential future needs. +""" + +import argparse +import concurrent.futures +import io +import multiprocessing +import os +import subprocess +import sys +import time +import warnings +from pathlib import Path +from typing import List, Set + +from ci.boards import Board, create_board # type: ignore +from ci.util.concurrent_run import ConcurrentRunArgs, concurrent_run +from ci.util.locked_print import locked_print + + +HERE = Path(__file__).parent.resolve() + +LIBS = ["src"] +EXTRA_LIBS = [ + "https://github.com/me-no-dev/ESPAsyncWebServer.git", + "ArduinoOTA", + "SD", + "FS", + "ESPmDNS", + "WiFi", + "WebSockets", +] +BUILD_FLAGS = ["-Wl,-Map,firmware.map", "-fopt-info-all=optimization_report.txt"] + +# Default boards to compile for. You can use boards not defined here but +# if the board isn't part of the officially supported platformio boards then +# you will need to add the board to the ~/.platformio/platforms directory. +# prior to running this script. This happens automatically as of 2024-08-20 +# with the github workflow scripts. +DEFAULT_BOARDS_NAMES = [ + "apollo3_red", + "apollo3_thing_explorable", + "web", # work in progress + "uno", # Build is faster if this is first, because it's used for global init. + "esp32dev", + "esp01", # ESP8266 + "esp32c3", + "attiny85", + "ATtiny1616", + "esp32c6", + "esp32s3", + "esp32p4", + "yun", + "digix", + "teensylc", + "teensy30", + "teensy31", + "teensy41", + "adafruit_feather_nrf52840_sense", + "xiaoblesense_adafruit", + "rpipico", + "rpipico2", + "uno_r4_wifi", + "esp32rmt_51", + "esp32dev_idf44", + "bluepill", + "esp32rmt_51", + "giga_r1", + "sparkfun_xrp_controller", +] + +OTHER_BOARDS_NAMES = [ + "nano_every", + "esp32-c2-devkitm-1", +] + +# Examples to compile. +DEFAULT_EXAMPLES = [ + "Animartrix", + "Apa102", + "Apa102HD", + "Apa102HDOverride", + "Audio", + "Blink", + "Blur", + "Chromancer", + "ColorPalette", + "ColorTemperature", + "Corkscrew", + "Cylon", + "DemoReel100", + # "Downscale", + "FestivalStick", + "FirstLight", + "Fire2012", + "Multiple/MultipleStripsInOneArray", + "Multiple/ArrayOfLedArrays", + "Noise", + "NoisePlayground", + "NoisePlusPalette", + # "LuminescentGrand", + "Pacifica", + "Pride2015", + "RGBCalibrate", + "RGBSetDemo", + "RGBW", + "Overclock", + "RGBWEmulated", + "TwinkleFox", + "XYMatrix", + "FireMatrix", + "FireCylinder", + "FxGfx2Video", + "FxSdCard", + "FxCylon", + "FxDemoReel100", + "FxTwinkleFox", + "FxFire2012", + "FxNoisePlusPalette", + "FxPacifica", + "FxEngine", + "WS2816", +] + +EXTRA_EXAMPLES: dict[Board, list[str]] = { + # ESP32DEV: ["EspI2SDemo"], + # ESP32_S3_DEVKITC_1: ["EspS3I2SDemo"], +} + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Compile FastLED examples for various boards." + ) + # parser.add_argument( + # "--boards", type=str, help="Comma-separated list of boards to compile for" + # ) + # needs to be a positional argument instead + parser.add_argument( + "boards", + type=str, + help="Comma-separated list of boards to compile for", + nargs="?", + ) + parser.add_argument( + "positional_examples", + type=str, + help="Examples to compile (positional arguments after board name)", + nargs="*", + ) + parser.add_argument( + "--examples", type=str, help="Comma-separated list of examples to compile" + ) + parser.add_argument( + "--exclude-examples", type=str, help="Examples that should be excluded" + ) + parser.add_argument( + "--skip-init", action="store_true", help="Skip the initialization step" + ) + parser.add_argument( + "--defines", type=str, help="Comma-separated list of compiler definitions" + ) + parser.add_argument("--customsdk", type=str, help="custom_sdkconfig project option") + parser.add_argument( + "--extra-packages", + type=str, + help="Comma-separated list of extra packages to install", + ) + parser.add_argument( + "--add-extra-esp32-libs", + action="store_true", + help="Add extra libraries to the libraries list to check against compiler errors.", + ) + parser.add_argument( + "--build-dir", type=str, help="Override the default build directory" + ) + parser.add_argument( + "--no-project-options", + action="store_true", + help="Don't use custom project options", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Enable interactive mode to choose a board", + ) + # Passed by the github action to disable interactive mode. + parser.add_argument( + "--no-interactive", action="store_true", help="Disable interactive mode" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose output" + ) + parser.add_argument( + "--supported-boards", + action="store_true", + help="Print the list of supported boards and exit", + ) + parser.add_argument( + "--symbols", + action="store_true", + help="Run symbol analysis on compiled output", + ) + parser.add_argument( + "--allsrc", + action="store_true", + help="Enable all-source build (adds FASTLED_ALL_SRC=1 define)", + ) + parser.add_argument( + "--no-allsrc", + action="store_true", + help="Disable all-source build (adds FASTLED_ALL_SRC=0 define)", + ) + try: + args = parser.parse_intermixed_args() + unknown = [] + except SystemExit: + # If parse_intermixed_args fails, fall back to parse_known_args + args, unknown = parser.parse_known_args() + + # Handle unknown arguments intelligently - treat non-flag arguments as examples + unknown_examples = [] + unknown_flags = [] + for arg in unknown: + if arg.startswith("-"): + unknown_flags.append(arg) + else: + unknown_examples.append(arg) + + # Add unknown examples to positional_examples + if unknown_examples: + if not hasattr(args, "positional_examples") or args.positional_examples is None: + args.positional_examples = [] + args.positional_examples.extend(unknown_examples) + + # Only warn about actual unknown flags, not examples + if unknown_flags: + warnings.warn(f"Unknown arguments: {unknown_flags}") + + # Check for FASTLED_CI_NO_INTERACTIVE environment variable + # This allows test.py and other scripts to force non-interactive mode + if os.environ.get("FASTLED_CI_NO_INTERACTIVE") == "true": + args.interactive = False + args.no_interactive = True + + # if --interactive and --no-interative are both passed, --no-interactive takes precedence. + if args.interactive and args.no_interactive: + warnings.warn( + "Both --interactive and --no-interactive were passed, --no-interactive takes precedence." + ) + args.interactive = False + + # Validate that --allsrc and --no-allsrc are not both specified + if args.allsrc and args.no_allsrc: + warnings.warn( + "Both --allsrc and --no-allsrc were passed, this is contradictory. Please specify only one." + ) + sys.exit(1) # Exit with error + + return args + + +def remove_duplicates(items: List[str]) -> List[str]: + seen: Set[str] = set() + out: List[str] = [] + for item in items: + if item not in seen: + seen.add(item) + out.append(item) + return out + + +def choose_board_interactively(boards: List[str]) -> List[str]: + print("Available boards:") + boards = remove_duplicates(sorted(boards)) + for i, board in enumerate(boards): + print(f"[{i}]: {board}") + print("[all]: All boards") + out: List[str] = [] + while True: + try: + # choice = int(input("Enter the number of the board(s) you want to compile to: ")) + input_str = input( + "Enter the number of the board(s) you want to compile to, or it's name(s): " + ) + if "all" in input_str: + return boards + for board in input_str.split(","): + if board == "": + continue + if not board.isdigit(): + out.append(board) # Assume it's a board name. + else: + index = int(board) # Find the board from the index. + if 0 <= index < len(boards): + out.append(boards[index]) + else: + warnings.warn(f"invalid board index: {index}, skipping") + if not out: + print("Please try again.") + continue + return out + except ValueError: + print("Invalid input. Please enter a number.") + + +def resolve_example_path(example: str) -> Path: + example_path = HERE.parent / "examples" / example + if not example_path.exists(): + raise FileNotFoundError(f"Example '{example}' not found at '{example_path}'") + return example_path + + +def create_concurrent_run_args(args: argparse.Namespace) -> ConcurrentRunArgs: + skip_init = args.skip_init + if args.interactive: + boards = choose_board_interactively(DEFAULT_BOARDS_NAMES + OTHER_BOARDS_NAMES) + else: + boards = args.boards.split(",") if args.boards else DEFAULT_BOARDS_NAMES + projects: list[Board] = [] + for board in boards: + projects.append(create_board(board, no_project_options=args.no_project_options)) + extra_examples: dict[Board, list[Path]] = {} + # Handle both positional and named examples + if args.positional_examples: + # Convert positional examples, handling both "examples/Blink" and "Blink" formats + examples = [] + for example in args.positional_examples: + # Remove "examples/" prefix if present + if example.startswith("examples/"): + example = example[len("examples/") :] + examples.append(example) + elif args.examples: + examples = args.examples.split(",") + else: + examples = DEFAULT_EXAMPLES + # Only add extra examples when using defaults + for b, _examples in EXTRA_EXAMPLES.items(): + resolved_examples = [resolve_example_path(example) for example in _examples] + extra_examples[b] = resolved_examples + examples_paths = [resolve_example_path(example) for example in examples] + # now process example exclusions. + if args.exclude_examples: + exclude_examples = args.exclude_examples.split(",") + examples_paths = [ + example + for example in examples_paths + if example.name not in exclude_examples + ] + for exclude in exclude_examples: + examples.remove(exclude) + customsdk = args.customsdk + defines: list[str] = [] + if args.defines: + defines.extend(args.defines.split(",")) + # Add FASTLED_ALL_SRC define when --allsrc or --no-allsrc flag is specified + if args.allsrc: + defines.append("FASTLED_ALL_SRC=1") + elif args.no_allsrc: + defines.append("FASTLED_ALL_SRC=0") + extra_packages: list[str] = [] + if args.extra_packages: + extra_packages.extend(args.extra_packages.split(",")) + build_dir = args.build_dir + extra_scripts = "pre:lib/ci/util-flags.py" + verbose = args.verbose + + out: ConcurrentRunArgs = ConcurrentRunArgs( + projects=projects, + examples=examples_paths, + skip_init=skip_init, + defines=defines, + customsdk=customsdk, + extra_packages=extra_packages, + libs=LIBS, + build_dir=build_dir, + extra_scripts=extra_scripts, + cwd=str(HERE.parent), + board_dir=(HERE / "boards").absolute().as_posix(), + build_flags=BUILD_FLAGS, + verbose=verbose, + extra_examples=extra_examples, + symbols=args.symbols, + ) + return out + + +def main() -> int: + """Main function.""" + args = parse_args() + if args.supported_boards: + print(",".join(DEFAULT_BOARDS_NAMES)) + return 0 + if args.add_extra_esp32_libs: + LIBS.extend(EXTRA_LIBS) + + # Set the working directory to the script's parent directory. + run_args = create_concurrent_run_args(args) + start_time = time.time() + rtn = concurrent_run(args=run_args) + time_taken = time.strftime("%Mm:%Ss", time.gmtime(time.time() - start_time)) + locked_print(f"Compilation finished in {time_taken}.") + return rtn + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(1) diff --git a/v1.5/lib/FastLED/ci/ci-compile-native.py b/v1.5/lib/FastLED/ci/ci-compile-native.py new file mode 100644 index 0000000..014126a --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-compile-native.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import sys + + +def main(): + # Change to the directory of the script + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + # Change to the 'native' directory and run 'pio run' + os.chdir("native") + result = subprocess.run(["pio", "run"], check=True) + + # Exit with the same status as the pio run command + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/ci-compile-pio-ci-working.py b/v1.5/lib/FastLED/ci/ci-compile-pio-ci-working.py new file mode 100644 index 0000000..f858fd0 --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-compile-pio-ci-working.py @@ -0,0 +1,596 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +""" +Runs the compilation process for examples on boards using pio ci command. +This replaces the previous concurrent build system with a simpler pio ci approach. +""" + +import argparse +import os +import subprocess +import sys +import time +import warnings +from pathlib import Path +from typing import List, Set + +from ci.boards import Board, create_board # type: ignore +from ci.util.locked_print import locked_print + + +HERE = Path(__file__).parent.resolve() + +# Default boards to compile for +DEFAULT_BOARDS_NAMES = [ + "apollo3_red", + "apollo3_thing_explorable", + "web", # work in progress + "uno", # Build is faster if this is first, because it's used for global init. + "esp32dev", + "esp01", # ESP8266 + "esp32c3", + "attiny85", + "ATtiny1616", + "esp32c6", + "esp32s3", + "esp32p4", + "yun", + "digix", + "teensylc", + "teensy30", + "teensy31", + "teensy41", + "adafruit_feather_nrf52840_sense", + "xiaoblesense_adafruit", + "rpipico", + "rpipico2", + "uno_r4_wifi", + "esp32rmt_51", + "esp32dev_idf44", + "bluepill", + "esp32rmt_51", + "giga_r1", + "sparkfun_xrp_controller", +] + +OTHER_BOARDS_NAMES = [ + "nano_every", + "esp32-c2-devkitm-1", +] + +# Examples to compile. +DEFAULT_EXAMPLES = [ + "Animartrix", + "Apa102", + "Apa102HD", + "Apa102HDOverride", + "Audio", + "Blink", + "Blur", + "Chromancer", + "ColorPalette", + "ColorTemperature", + "Corkscrew", + "Cylon", + "DemoReel100", + # "Downscale", + "FestivalStick", + "FirstLight", + "Fire2012", + "Multiple/MultipleStripsInOneArray", + "Multiple/ArrayOfLedArrays", + "Noise", + "NoisePlayground", + "NoisePlusPalette", + # "LuminescentGrand", + "Pacifica", + "Pride2015", + "RGBCalibrate", + "RGBSetDemo", + "RGBW", + "Overclock", + "RGBWEmulated", + "TwinkleFox", + "XYMatrix", + "FireMatrix", + "FireCylinder", + "FxGfx2Video", + "FxSdCard", + "FxCylon", + "FxDemoReel100", + "FxTwinkleFox", + "FxFire2012", + "FxNoisePlusPalette", + "FxPacifica", + "FxEngine", + "WS2816", +] + +EXTRA_EXAMPLES: dict[Board, list[str]] = { + # ESP32DEV: ["EspI2SDemo"], + # ESP32_S3_DEVKITC_1: ["EspS3I2SDemo"], +} + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Compile FastLED examples for various boards using pio ci.util." + ) + parser.add_argument( + "boards", + type=str, + help="Comma-separated list of boards to compile for", + nargs="?", + ) + parser.add_argument( + "positional_examples", + type=str, + help="Examples to compile (positional arguments after board name)", + nargs="*", + ) + parser.add_argument( + "--examples", type=str, help="Comma-separated list of examples to compile" + ) + parser.add_argument( + "--exclude-examples", type=str, help="Examples that should be excluded" + ) + parser.add_argument( + "--defines", type=str, help="Comma-separated list of compiler definitions" + ) + parser.add_argument("--customsdk", type=str, help="custom_sdkconfig project option") + parser.add_argument( + "--extra-packages", + type=str, + help="Comma-separated list of extra packages to install", + ) + parser.add_argument( + "--build-dir", type=str, help="Override the default build directory" + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Enable interactive mode to choose a board", + ) + parser.add_argument( + "--no-interactive", action="store_true", help="Disable interactive mode" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose output" + ) + parser.add_argument( + "--supported-boards", + action="store_true", + help="Print the list of supported boards and exit", + ) + parser.add_argument( + "--symbols", + action="store_true", + help="Run symbol analysis on compiled output", + ) + parser.add_argument( + "--allsrc", + action="store_true", + help="Enable all-source build (adds FASTLED_ALL_SRC=1 define)", + ) + parser.add_argument( + "--no-allsrc", + action="store_true", + help="Disable all-source build (adds FASTLED_ALL_SRC=0 define)", + ) + try: + args = parser.parse_intermixed_args() + unknown = [] + except SystemExit: + # If parse_intermixed_args fails, fall back to parse_known_args + args, unknown = parser.parse_known_args() + + # Handle unknown arguments intelligently - treat non-flag arguments as examples + unknown_examples = [] + unknown_flags = [] + for arg in unknown: + if arg.startswith("-"): + unknown_flags.append(arg) + else: + unknown_examples.append(arg) + + # Add unknown examples to positional_examples + if unknown_examples: + if not hasattr(args, "positional_examples") or args.positional_examples is None: + args.positional_examples = [] + args.positional_examples.extend(unknown_examples) + + # Only warn about actual unknown flags, not examples + if unknown_flags: + warnings.warn(f"Unknown arguments: {unknown_flags}") + + # Check for FASTLED_CI_NO_INTERACTIVE environment variable + if os.environ.get("FASTLED_CI_NO_INTERACTIVE") == "true": + args.interactive = False + args.no_interactive = True + + # if --interactive and --no-interative are both passed, --no-interactive takes precedence. + if args.interactive and args.no_interactive: + warnings.warn( + "Both --interactive and --no-interactive were passed, --no-interactive takes precedence." + ) + args.interactive = False + + # Validate that --allsrc and --no-allsrc are not both specified + if args.allsrc and args.no_allsrc: + warnings.warn( + "Both --allsrc and --no-allsrc were passed, this is contradictory. Please specify only one." + ) + sys.exit(1) + + return args + + +def remove_duplicates(items: List[str]) -> List[str]: + seen: Set[str] = set() + out: List[str] = [] + for item in items: + if item not in seen: + seen.add(item) + out.append(item) + return out + + +def choose_board_interactively(boards: List[str]) -> List[str]: + print("Available boards:") + boards = remove_duplicates(sorted(boards)) + for i, board in enumerate(boards): + print(f"[{i}]: {board}") + print("[all]: All boards") + out: list[str] = [] + while True: + try: + input_str = input( + "Enter the number of the board(s) you want to compile to, or it's name(s): " + ) + if "all" in input_str: + return boards + for board in input_str.split(","): + if board == "": + continue + if not board.isdigit(): + out.append(board) # Assume it's a board name. + else: + index = int(board) # Find the board from the index. + if 0 <= index < len(boards): + out.append(boards[index]) + else: + warnings.warn(f"invalid board index: {index}, skipping") + if not out: + print("Please try again.") + continue + return out + except ValueError: + print("Invalid input. Please enter a number.") + + +def resolve_example_path(example: str) -> Path: + example_path = HERE.parent / "examples" / example + if not example_path.exists(): + raise FileNotFoundError(f"Example '{example}' not found at '{example_path}'") + return example_path + + +def compile_with_pio_ci( + board: Board, + example_paths: list[Path], + build_dir: str | None, + defines: list[str], + verbose: bool, +) -> tuple[bool, str]: + """Compile examples for a board using pio ci command.""" + + # Skip web boards + if board.board_name == "web": + locked_print(f"Skipping web target for board {board.board_name}") + return True, "" + + board_name = board.board_name + real_board_name = board.get_real_board_name() + + # Set up build directory + if build_dir: + board_build_dir = Path(build_dir) / board_name + else: + board_build_dir = Path(".build") / board_name + + board_build_dir.mkdir(parents=True, exist_ok=True) + + locked_print(f"*** Compiling examples for board {board_name} using pio ci ***") + + errors: list[str] = [] + + for example_path in example_paths: + locked_print( + f"*** Building example {example_path.name} for board {board_name} ***" + ) + + # Find the .ino file in the example directory + ino_files = list(example_path.glob("*.ino")) + if not ino_files: + error_msg = f"No .ino file found in {example_path}" + locked_print(f"ERROR: {error_msg}") + errors.append(error_msg) + continue + + # Use the first .ino file found + ino_file = ino_files[0] + + # Build pio ci command + cmd_list = [ + "pio", + "ci", + str(ino_file), + "--board", + real_board_name, + "--lib", + "src", # FastLED source directory + "--keep-build-dir", + "--build-dir", + str(board_build_dir / example_path.name), + ] + + # Add platform-specific options + if board.platform: + cmd_list.extend(["--project-option", f"platform={board.platform}"]) + + if board.platform_packages: + cmd_list.extend( + ["--project-option", f"platform_packages={board.platform_packages}"] + ) + + if board.framework: + cmd_list.extend(["--project-option", f"framework={board.framework}"]) + + if board.board_build_core: + cmd_list.extend( + ["--project-option", f"board_build.core={board.board_build_core}"] + ) + + if board.board_build_filesystem_size: + cmd_list.extend( + [ + "--project-option", + f"board_build.filesystem_size={board.board_build_filesystem_size}", + ] + ) + + # Add defines + all_defines = defines.copy() + if board.defines: + all_defines.extend(board.defines) + + if all_defines: + build_flags = " ".join(f"-D{define}" for define in all_defines) + cmd_list.extend(["--project-option", f"build_flags={build_flags}"]) + + # Add custom SDK config if specified + if board.customsdk: + cmd_list.extend(["--project-option", f"custom_sdkconfig={board.customsdk}"]) + + # Add verbose flag if requested + if verbose: + cmd_list.append("--verbose") + + # Execute the command + cmd_str = subprocess.list2cmdline(cmd_list) + locked_print(f"Running command: {cmd_str}") + + start_time = time.time() + + try: + result = subprocess.run( + cmd_list, + capture_output=True, + text=True, + cwd=str(HERE.parent), + timeout=300, # 5 minute timeout per example + ) + + elapsed_time = time.time() - start_time + + if result.returncode == 0: + locked_print( + f"*** Successfully built {example_path.name} for {board_name} in {elapsed_time:.2f}s ***" + ) + if verbose and result.stdout: + locked_print(f"Build output:\n{result.stdout}") + else: + error_msg = f"Failed to build {example_path.name} for {board_name}" + locked_print(f"ERROR: {error_msg}") + locked_print(f"Command: {cmd_str}") + if result.stdout: + locked_print(f"STDOUT:\n{result.stdout}") + if result.stderr: + locked_print(f"STDERR:\n{result.stderr}") + errors.append(f"{error_msg}: {result.stderr}") + + except subprocess.TimeoutExpired: + error_msg = f"Timeout building {example_path.name} for {board_name}" + locked_print(f"ERROR: {error_msg}") + errors.append(error_msg) + except Exception as e: + error_msg = f"Exception building {example_path.name} for {board_name}: {e}" + locked_print(f"ERROR: {error_msg}") + errors.append(error_msg) + + if errors: + return False, "\n".join(errors) + + return True, f"Successfully compiled all examples for {board_name}" + + +def run_symbol_analysis(boards: list[Board]) -> None: + """Run symbol analysis on compiled outputs if requested.""" + locked_print("\nRunning symbol analysis on compiled outputs...") + + for board in boards: + if board.board_name == "web": + continue + + try: + locked_print(f"Running symbol analysis for board: {board.board_name}") + + cmd = [ + "uv", + "run", + "ci/util/symbol_analysis.py", + "--board", + board.board_name, + ] + + result = subprocess.run( + cmd, capture_output=True, text=True, cwd=str(HERE.parent) + ) + + if result.returncode != 0: + locked_print( + f"ERROR: Symbol analysis failed for board {board.board_name}: {result.stderr}" + ) + else: + locked_print(f"Symbol analysis completed for board: {board.board_name}") + if result.stdout: + print(result.stdout) + + except Exception as e: + locked_print( + f"ERROR: Exception during symbol analysis for board {board.board_name}: {e}" + ) + + +def main() -> int: + """Main function.""" + args = parse_args() + + if args.supported_boards: + print(",".join(DEFAULT_BOARDS_NAMES)) + return 0 + + # Determine which boards to compile for + if args.interactive: + boards_names = choose_board_interactively( + DEFAULT_BOARDS_NAMES + OTHER_BOARDS_NAMES + ) + else: + boards_names = args.boards.split(",") if args.boards else DEFAULT_BOARDS_NAMES + + # Get board objects + boards: list[Board] = [] + for board_name in boards_names: + try: + board = create_board(board_name, no_project_options=False) + boards.append(board) + except Exception as e: + locked_print(f"ERROR: Failed to get board '{board_name}': {e}") + return 1 + + # Determine which examples to compile + if args.positional_examples: + # Convert positional examples, handling both "examples/Blink" and "Blink" formats + examples = [] + for example in args.positional_examples: + # Remove "examples/" prefix if present + if example.startswith("examples/"): + example = example[len("examples/") :] + examples.append(example) + elif args.examples: + examples = args.examples.split(",") + else: + examples = DEFAULT_EXAMPLES + + # Process example exclusions + if args.exclude_examples: + exclude_examples = args.exclude_examples.split(",") + examples = [ex for ex in examples if ex not in exclude_examples] + + # Resolve example paths + example_paths: list[Path] = [] + for example in examples: + try: + example_path = resolve_example_path(example) + example_paths.append(example_path) + except FileNotFoundError as e: + locked_print(f"ERROR: {e}") + return 1 + + # Add extra examples for specific boards + extra_examples: dict[Board, list[Path]] = {} + for board in boards: + if board in EXTRA_EXAMPLES: + board_examples = [] + for example in EXTRA_EXAMPLES[board]: + try: + board_examples.append(resolve_example_path(example)) + except FileNotFoundError as e: + locked_print(f"WARNING: {e}") + if board_examples: + extra_examples[board] = board_examples + + # Set up defines + defines: list[str] = [] + if args.defines: + defines.extend(args.defines.split(",")) + + # Add FASTLED_ALL_SRC define when --allsrc or --no-allsrc flag is specified + if args.allsrc: + defines.append("FASTLED_ALL_SRC=1") + elif args.no_allsrc: + defines.append("FASTLED_ALL_SRC=0") + + # Start compilation + start_time = time.time() + locked_print( + f"Starting compilation for {len(boards)} boards with {len(example_paths)} examples" + ) + + compilation_errors: List[str] = [] + + # Compile for each board + for board in boards: + board_examples = example_paths.copy() + if board in extra_examples: + board_examples.extend(extra_examples[board]) + + success, message = compile_with_pio_ci( + board=board, + example_paths=board_examples, + build_dir=args.build_dir, + defines=defines, + verbose=args.verbose, + ) + + if not success: + compilation_errors.append(f"Board {board.board_name}: {message}") + locked_print(f"ERROR: Compilation failed for board {board.board_name}") + # Continue with other boards instead of stopping + + # Run symbol analysis if requested + if args.symbols: + run_symbol_analysis(boards) + + # Report results + elapsed_time = time.time() - start_time + time_str = time.strftime("%Mm:%Ss", time.gmtime(elapsed_time)) + + if compilation_errors: + locked_print( + f"\nCompilation finished in {time_str} with {len(compilation_errors)} error(s):" + ) + for error in compilation_errors: + locked_print(f" - {error}") + return 1 + else: + locked_print(f"\nAll compilations completed successfully in {time_str}") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + locked_print("\nInterrupted by user") + sys.exit(1) diff --git a/v1.5/lib/FastLED/ci/ci-compile.py b/v1.5/lib/FastLED/ci/ci-compile.py new file mode 100644 index 0000000..9bee5ab --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-compile.py @@ -0,0 +1,657 @@ +#!/usr/bin/env python3 +""" +FastLED Example Compiler + +Streamlined compiler that uses the PioCompiler to build FastLED examples for various boards. +This replaces the previous complex compilation system with a simpler approach using the Pio compiler. +""" + +import argparse +import os +import sys +import threading +import time +from concurrent.futures import Future, as_completed +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, cast + +from typeguard import typechecked + +from ci.boards import ALL, Board, create_board +from ci.compiler.compiler import CacheType, SketchResult +from ci.compiler.pio import PioCompiler + + +def green_text(text: str) -> str: + """Return text in green color.""" + return f"\033[32m{text}\033[0m" + + +def red_text(text: str) -> str: + """Return text in red color.""" + return f"\033[31m{text}\033[0m" + + +def get_default_boards() -> List[str]: + """Get all board names from the ALL boards list, with preferred boards first.""" + # These are the boards we want to compile first (preferred order) + # Order matters: UNO first because it's used for global init and builds faster + preferred_board_names = [ + "uno", # Build is faster if this is first, because it's used for global init. + "esp32dev", + "esp01", # ESP8266 + "esp32c3", + "esp32c6", + "esp32s3", + "teensylc", + "teensy31", + "teensy41", + "digix", + "rpipico", + "rpipico2", + ] + + # Get all available board names from the ALL list + available_board_names = {board.board_name for board in ALL} + + # Start with preferred boards that exist, warn about missing ones + default_boards: List[str] = [] + for board_name in preferred_board_names: + if board_name in available_board_names: + default_boards.append(board_name) + else: + print( + f"WARNING: Preferred board '{board_name}' not found in available boards" + ) + + # Add all remaining boards (sorted for consistency) + remaining_boards = sorted(available_board_names - set(default_boards)) + default_boards.extend(remaining_boards) + + return default_boards + + +def get_all_examples() -> List[str]: + """Get all available example names from the examples directory.""" + project_root = Path(__file__).parent.parent.resolve() + examples_dir = project_root / "examples" + + if not examples_dir.exists(): + return [] + + examples: List[str] = [] + for item in examples_dir.iterdir(): + if item.is_dir(): + # Check if it contains a .ino file with the same name + ino_file = item / f"{item.name}.ino" + if ino_file.exists(): + examples.append(item.name) + + # Sort for consistent ordering + examples.sort() + return examples + + +def parse_args(args: Optional[list[str]] = None) -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Compile FastLED examples for various boards using PioCompiler" + ) + parser.add_argument( + "boards", + type=str, + help="Comma-separated list of boards to compile for", + nargs="?", + ) + parser.add_argument( + "positional_examples", + type=str, + help="Examples to compile (positional arguments after board name)", + nargs="*", + ) + parser.add_argument( + "--examples", type=str, help="Comma-separated list of examples to compile" + ) + parser.add_argument( + "--exclude-examples", type=str, help="Examples that should be excluded" + ) + parser.add_argument( + "--defines", type=str, help="Comma-separated list of compiler definitions" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose output" + ) + parser.add_argument( + "--no-cache", + action="store_true", + help="Disable sccache for faster compilation (default is already disabled)", + ) + parser.add_argument( + "--enable-cache", + action="store_true", + help="Enable sccache for faster compilation", + ) + parser.add_argument( + "--cache", + action="store_true", + help="(Deprecated) Enable sccache for faster compilation - use --enable-cache instead", + ) + parser.add_argument( + "--supported-boards", + action="store_true", + help="Print the list of supported boards and exit", + ) + parser.add_argument( + "--no-interactive", + action="store_true", + help="Disables interactive mode (deprecated)", + ) + parser.add_argument( + "--log-failures", + type=str, + help="Directory to write per-example failure logs (created on first failure)", + ) + parser.add_argument( + "--global-cache", + type=str, + help="Override global PlatformIO cache directory path (for testing)", + ) + parser.add_argument( + "-o", + "--out", + type=str, + help="Output path for build artifact. Requires exactly one sketch. " + "If path ends with '/', treated as directory. If has suffix, treated as file. " + "Use '-o .' to save in current directory with sketch name.", + ) + + try: + parsed_args = parser.parse_intermixed_args(args) + unknown = [] + except SystemExit: + # If parse_intermixed_args fails, fall back to parse_known_args + parsed_args, unknown = parser.parse_known_args(args) + + # Handle unknown arguments intelligently - treat non-flag arguments as examples + unknown_examples: List[str] = [] + for arg in unknown: + if not arg.startswith("-"): + unknown_examples.append(arg) + + # Add unknown examples to positional_examples + if unknown_examples: + if ( + not hasattr(parsed_args, "positional_examples") + or parsed_args.positional_examples is None + ): + parsed_args.positional_examples = [] + # Type assertion to help the type checker - argparse returns Any but we know it's List[str] + positional_examples: List[str] = cast( + List[str], getattr(parsed_args, "positional_examples", []) + ) + positional_examples.extend(unknown_examples) + parsed_args.positional_examples = positional_examples + + return parsed_args + + +def resolve_example_path(example: str) -> str: + """Resolve example name to path, ensuring it exists.""" + project_root = Path(__file__).parent.parent.resolve() + examples_dir = project_root / "examples" + + # Handle both "Blink" and "examples/Blink" formats + if example.startswith("examples/"): + example = example[len("examples/") :] + + example_path = examples_dir / example + if not example_path.exists(): + raise FileNotFoundError(f"Example not found: {example_path}") + + return example + + +@typechecked +@dataclass +class BoardCompilationResult: + """Aggregated result for compiling a set of examples on a single board.""" + + ok: bool + sketch_results: List[SketchResult] + + +def compile_board_examples( + board: Board, + examples: List[str], + defines: List[str], + verbose: bool, + enable_cache: bool, + global_cache_dir: Optional[Path] = None, +) -> BoardCompilationResult: + """Compile examples for a single board using PioCompiler.""" + + # Resolve global cache directory immediately for display + resolved_cache_dir = None + if global_cache_dir is not None: + # User specified a path - use it exactly as provided + resolved_cache_dir = global_cache_dir.resolve() + else: + # Default path ends with 'global_cache' + resolved_cache_dir = Path.home() / ".fastled" / "global_cache" + + print(f"\n{'=' * 60}") + print(f"COMPILING BOARD: {board.board_name}") + print(f"EXAMPLES: {', '.join(examples)}") + print(f"GLOBAL CACHE: {resolved_cache_dir}") + + # Show other cache directories + from ci.compiler.pio import FastLEDPaths + + paths = FastLEDPaths(board.board_name) + + print(f"BUILD CACHE: {paths.build_cache_dir}") + print(f"CORE DIR: {paths.core_dir}") + print(f"PACKAGES DIR: {paths.packages_dir}") + + print(f"{'=' * 60}") + + try: + # Determine cache type based on flag and board frameworks + frameworks = [f.strip().lower() for f in (board.framework or "").split(",")] + mixed_frameworks = "arduino" in frameworks and "espidf" in frameworks + cache_type = ( + CacheType.SCCACHE + if enable_cache and not mixed_frameworks + else CacheType.NO_CACHE + ) + + # Create PioCompiler instance + compiler = PioCompiler( + board=board, + verbose=verbose, + global_cache_dir=resolved_cache_dir, + additional_defines=defines, + cache_type=cache_type, + ) + + # Build all examples + futures: List[Future[SketchResult]] = compiler.build(examples) + + # Wait for completion and collect results + results: List[SketchResult] = [] + + for future in futures: + try: + result = future.result() + results.append(result) + # SUCCESS/FAILED messages are printed by worker threads + except KeyboardInterrupt: + print("Keyboard interrupt detected, cancelling builds") + compiler.cancel_all() + import _thread + + for future in futures: + future.cancel() + + _thread.interrupt_main() + raise + except Exception as e: + # Represent unexpected exception as a failed SketchResult for consistency + from pathlib import Path as _Path + + results.append( + SketchResult( + success=False, + output=f"Build exception: {str(e)}", + build_dir=_Path("."), + example="", + ) + ) + print(f"EXCEPTION during build: {e}") + # Cleanup + compiler.cancel_all() + + # Show compiler statistics after all builds complete + try: + stats = compiler.get_cache_stats() + if stats: + print("\n" + "=" * 60) + print("COMPILER STATISTICS") + print("=" * 60) + print(stats) + print("=" * 60) + except Exception as e: + print(f"Warning: Could not retrieve compiler statistics: {e}") + + any_failures = False + for r in results: + if not r.success: + any_failures = True + break + return BoardCompilationResult(ok=not any_failures, sketch_results=results) + except KeyboardInterrupt: + print("Keyboard interrupt detected, cancelling builds") + import _thread + + _thread.interrupt_main() + raise + except Exception as e: + # Compiler could not be set up; return a single failed result to carry message + from pathlib import Path as _Path + + return BoardCompilationResult( + ok=False, + sketch_results=[ + SketchResult( + success=False, + output=f"Compiler setup failed: {str(e)}", + build_dir=_Path("."), + example="", + ) + ], + ) + + +def get_board_artifact_extension(board: Board) -> str: + """Get the primary artifact extension for a board.""" + # ESP32/ESP8266 boards always produce .bin files + if board.board_name.startswith("esp"): + return ".bin" + + # Most Arduino-based boards produce .hex files + if board.framework and "arduino" in board.framework.lower(): + return ".hex" + + # Default to .hex for most microcontroller boards + return ".hex" + + +def validate_output_path( + output_path: str, sketch_name: str, board: Board +) -> tuple[bool, str, str]: + """Validate output path and return (is_valid, resolved_path, error_message). + + Args: + output_path: The user-specified output path + sketch_name: Name of the sketch being built + board: Board configuration + + Returns: + Tuple of (is_valid, resolved_output_path, error_message) + """ + import os + + expected_ext = get_board_artifact_extension(board) + + # Handle special case: -o . + if output_path == ".": + resolved_path = f"{sketch_name}{expected_ext}" + return True, resolved_path, "" + + # If path ends with /, it's a directory + if output_path.endswith("/") or output_path.endswith("\\"): + resolved_path = os.path.join(output_path, f"{sketch_name}{expected_ext}") + return True, resolved_path, "" + + # If path has an extension, it's a file - validate the extension + if "." in os.path.basename(output_path): + _, ext = os.path.splitext(output_path) + if ext != expected_ext: + return ( + False, + "", + f"Output file extension '{ext}' doesn't match expected '{expected_ext}' for board '{board.board_name}'", + ) + return True, output_path, "" + + # Path doesn't end with / and has no extension - treat as directory + resolved_path = os.path.join(output_path, f"{sketch_name}{expected_ext}") + return True, resolved_path, "" + + +def copy_build_artifact( + build_dir: Path, board: Board, sketch_name: str, output_path: str +) -> bool: + """Copy the build artifact to the specified output path. + + Args: + build_dir: Build directory path + board: Board configuration + sketch_name: Name of the sketch + output_path: Target output path + + Returns: + True if successful, False otherwise + """ + import os + import shutil + + expected_ext = get_board_artifact_extension(board) + + # Find the source artifact + # PlatformIO builds are in .build/pio/{board}/.pio/build/{board}/firmware.{ext} + artifact_dir = build_dir / ".pio" / "build" / board.board_name + source_artifact = artifact_dir / f"firmware{expected_ext}" + + if not source_artifact.exists(): + print(f"ERROR: Build artifact not found: {source_artifact}") + return False + + # Ensure output directory exists + output_path_obj = Path(output_path) + output_path_obj.parent.mkdir(parents=True, exist_ok=True) + + try: + print(f"Copying {source_artifact} to {output_path}") + shutil.copy2(source_artifact, output_path) + print(f"✅ Build artifact saved to: {output_path}") + return True + except Exception as e: + print(f"ERROR: Failed to copy build artifact: {e}") + return False + + +def main() -> int: + """Main function.""" + args = parse_args() + if args.verbose: + os.environ["VERBOSE"] = "1" + + if args.supported_boards: + print(",".join(get_default_boards())) + return 0 + + # Determine which boards to compile for + if args.boards: + boards_names = args.boards.split(",") + else: + boards_names = get_default_boards() + + # Get board objects + boards: List[Board] = [] + for board_name in boards_names: + try: + board = create_board(board_name, no_project_options=False) + boards.append(board) + except Exception as e: + print(f"ERROR: Failed to get board '{board_name}': {e}") + return 1 + + # Determine which examples to compile + if args.positional_examples: + # Convert positional examples, handling both "examples/Blink" and "Blink" formats + examples: List[str] = [] + for example in args.positional_examples: + # Remove "examples/" prefix if present + if example.startswith("examples/"): + example = example[len("examples/") :] + examples.append(example) + elif args.examples: + examples = args.examples.split(",") + else: + # Compile all available examples since builds are fast now! + examples = get_all_examples() + + # Process example exclusions + if args.exclude_examples: + exclude_examples = args.exclude_examples.split(",") + examples = [ex for ex in examples if ex not in exclude_examples] + + # Validate examples exist + for example in examples: + try: + resolve_example_path(example) + except FileNotFoundError as e: + print(f"ERROR: {e}") + return 1 + + # Validate -o/--out option requirements + if args.out: + if len(examples) != 1: + print( + f"ERROR: -o/--out option requires exactly one sketch, got {len(examples)}: {examples}" + ) + return 1 + + if len(boards) != 1: + print( + f"ERROR: -o/--out option requires exactly one board, got {len(boards)}: {[b.board_name for b in boards]}" + ) + return 1 + + # Validate the output path + sketch_name = examples[0] + board = boards[0] + is_valid, resolved_output_path, error_msg = validate_output_path( + args.out, sketch_name, board + ) + if not is_valid: + print(f"ERROR: {error_msg}") + return 1 + + # Set up defines + defines: List[str] = [] + if args.defines: + defines.extend(args.defines.split(",")) + + # Start compilation + start_time = time.time() + print( + f"Starting compilation for {len(boards)} boards with {len(examples)} examples" + ) + + compilation_errors: List[str] = [] + failed_example_names: List[str] = [] + failure_logs_dir: Optional[Path] = ( + Path(args.log_failures) if getattr(args, "log_failures", None) else None + ) + + # Compile for each board + for board in boards: + # Parse global cache directory if provided + global_cache_dir = None + if args.global_cache: + global_cache_dir = Path(args.global_cache) + + result = compile_board_examples( + board=board, + examples=examples, + defines=defines, + verbose=args.verbose, + enable_cache=(args.enable_cache or args.cache) and not args.no_cache, + global_cache_dir=global_cache_dir, + ) + + if not result.ok: + # Record board-level error + compilation_errors.append(f"Board {board.board_name} failed") + print(red_text(f"ERROR: Compilation failed for board {board.board_name}")) + # Print each failing sketch's stdout and collect names for summary + for sketch in result.sketch_results: + if not sketch.success: + if sketch.example and sketch.example not in failed_example_names: + failed_example_names.append(sketch.example) + # Write per-example failure logs when requested + if failure_logs_dir is not None: + try: + failure_logs_dir.mkdir(parents=True, exist_ok=True) + safe_name = f"{sketch.example}.log" + log_path = failure_logs_dir / safe_name + with log_path.open( + "a", encoding="utf-8", errors="ignore" + ) as f: + f.write(sketch.output) + f.write("\n") + except KeyboardInterrupt: + print("Keyboard interrupt detected, cancelling builds") + import _thread + + _thread.interrupt_main() + raise + except Exception as e: + print( + f"Warning: Could not write failure log for {sketch.example}: {e}" + ) + print(f"\n{'-' * 60}") + print(f"Sketch: {sketch.example}") + print(f"{'-' * 60}") + # Print the collected output for this sketch + print(sketch.output) + # Continue with other boards instead of stopping + else: + # Compilation succeeded - handle -o/--out option if specified + if args.out: + sketch_name = examples[0] # We already validated there's exactly one + is_valid, resolved_output_path, _ = validate_output_path( + args.out, sketch_name, board + ) + if is_valid: + # Find the build directory for this board + project_root = Path(__file__).parent.parent.resolve() + build_dir = project_root / ".build" / "pio" / board.board_name + + if not copy_build_artifact( + build_dir, board, sketch_name, resolved_output_path + ): + compilation_errors.append( + f"Failed to copy artifact for {board.board_name}" + ) + print( + red_text( + f"ERROR: Failed to copy build artifact for {board.board_name}" + ) + ) + return 1 + + # Report results + elapsed_time = time.time() - start_time + time_str = time.strftime("%Mm:%Ss", time.gmtime(elapsed_time)) + + if compilation_errors: + print( + f"\nCompilation finished in {time_str} with {len(compilation_errors)} error(s):" + ) + for error in compilation_errors: + print(f" - {error}") + if failed_example_names: + print("") + print(red_text(f"ERROR! There were {len(failed_example_names)} failures:")) + # Sort for stable output + for name in sorted(failed_example_names): + # print(f" {name}") + # same but in red + print(red_text(f" {name}")) + print("") + return 1 + else: + print(f"\nAll compilations completed successfully in {time_str}") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\nInterrupted by user") + sys.exit(1) diff --git a/v1.5/lib/FastLED/ci/ci-compile.save.py b/v1.5/lib/FastLED/ci/ci-compile.save.py new file mode 100644 index 0000000..153c653 --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-compile.save.py @@ -0,0 +1,1032 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +""" +Runs the compilation process for examples on boards using pio ci command. +This replaces the previous concurrent build system with a simpler pio ci approach. +""" + +import argparse +import os +import shutil +import subprocess +import sys +import time +import warnings +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +# Import from the local ci directory +from ci.boards import Board, create_board # type: ignore +from ci.util.create_build_dir import insert_tool_aliases +from ci.util.locked_print import locked_print + + +HERE = Path(__file__).parent.resolve() + +# Default boards to compile for +DEFAULT_BOARDS_NAMES = [ + "apollo3_red", + "apollo3_thing_explorable", + "web", # work in progress + "uno", # Build is faster if this is first, because it's used for global init. + "esp32dev", + "esp01", # ESP8266 + "esp32c3", + "attiny85", + "ATtiny1616", + "esp32c6", + "esp32s3", + "esp32p4", + "yun", + "digix", + "teensylc", + "teensy30", + "teensy31", + "teensy41", + "adafruit_feather_nrf52840_sense", + "xiaoblesense_adafruit", + "rpipico", + "rpipico2", + "uno_r4_wifi", + "esp32rmt_51", + "esp32dev_idf44", + "bluepill", + "esp32rmt_51", + "giga_r1", + "sparkfun_xrp_controller", +] + +OTHER_BOARDS_NAMES = [ + "nano_every", + "esp32-c2-devkitm-1", +] + +# Examples to compile. +DEFAULT_EXAMPLES = [ + "Animartrix", + "Apa102", + "Apa102HD", + "Apa102HDOverride", + "Audio", + "Blink", + "Blur", + "Chromancer", + "ColorPalette", + "ColorTemperature", + "Corkscrew", + "Cylon", + "DemoReel100", + # "Downscale", + "FestivalStick", + "FirstLight", + "Fire2012", + "Multiple/MultipleStripsInOneArray", + "Multiple/ArrayOfLedArrays", + "Noise", + "NoisePlayground", + "NoisePlusPalette", + # "LuminescentGrand", + "Pacifica", + "Pride2015", + "RGBCalibrate", + "RGBSetDemo", + "RGBW", + "Overclock", + "RGBWEmulated", + "TwinkleFox", + "XYMatrix", + "FireMatrix", + "FireCylinder", + "FxGfx2Video", + "FxSdCard", + "FxCylon", + "FxDemoReel100", + "FxTwinkleFox", + "FxFire2012", + "FxNoisePlusPalette", + "FxPacifica", + "FxEngine", + "WS2816", +] + +EXTRA_EXAMPLES: dict[Board, list[str]] = { + # ESP32DEV: ["EspI2SDemo"], + # ESP32_S3_DEVKITC_1: ["EspS3I2SDemo"], +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Compile FastLED examples for various boards using pio ci.util." + ) + parser.add_argument( + "boards", + type=str, + help="Comma-separated list of boards to compile for", + nargs="?", + ) + parser.add_argument( + "positional_examples", + type=str, + help="Examples to compile (positional arguments after board name)", + nargs="*", + ) + parser.add_argument( + "--examples", type=str, help="Comma-separated list of examples to compile" + ) + parser.add_argument( + "--exclude-examples", type=str, help="Examples that should be excluded" + ) + parser.add_argument( + "--defines", type=str, help="Comma-separated list of compiler definitions" + ) + parser.add_argument("--customsdk", type=str, help="custom_sdkconfig project option") + parser.add_argument( + "--extra-packages", + type=str, + help="Comma-separated list of extra packages to install", + ) + parser.add_argument( + "--build-dir", type=str, help="Override the default build directory" + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Enable interactive mode to choose a board", + ) + parser.add_argument( + "--no-interactive", action="store_true", help="Disable interactive mode" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose output" + ) + parser.add_argument( + "--supported-boards", + action="store_true", + help="Print the list of supported boards and exit", + ) + parser.add_argument( + "--symbols", + action="store_true", + help="Run symbol analysis on compiled output", + ) + parser.add_argument( + "--allsrc", + action="store_true", + help="Enable all-source build (adds FASTLED_ALL_SRC=1 define)", + ) + parser.add_argument( + "--no-allsrc", + action="store_true", + help="Disable all-source build (adds FASTLED_ALL_SRC=0 define)", + ) + try: + args = parser.parse_intermixed_args() + unknown = [] + except SystemExit: + # If parse_intermixed_args fails, fall back to parse_known_args + args, unknown = parser.parse_known_args() + + # Handle unknown arguments intelligently - treat non-flag arguments as examples + unknown_examples: list[str] = [] + unknown_flags: list[str] = [] + for arg in unknown: + if arg.startswith("-"): + unknown_flags.append(arg) + else: + unknown_examples.append(arg) + + # Add unknown examples to positional_examples + if unknown_examples: + if not hasattr(args, "positional_examples") or args.positional_examples is None: + args.positional_examples = [] + # Type assertion to help the type checker - argparse returns Any but we know it's list[str] + positional_examples: List[str] = args.positional_examples or [] + positional_examples.extend(unknown_examples) + + # Only warn about actual unknown flags, not examples + if unknown_flags: + warnings.warn(f"Unknown arguments: {unknown_flags}") + + # Check for FASTLED_CI_NO_INTERACTIVE environment variable + if os.environ.get("FASTLED_CI_NO_INTERACTIVE") == "true": + args.interactive = False + args.no_interactive = True + + # if --interactive and --no-interative are both passed, --no-interactive takes precedence. + if args.interactive and args.no_interactive: + warnings.warn( + "Both --interactive and --no-interactive were passed, --no-interactive takes precedence." + ) + args.interactive = False + + # Validate that --allsrc and --no-allsrc are not both specified + if args.allsrc and args.no_allsrc: + warnings.warn( + "Both --allsrc and --no-allsrc were passed, this is contradictory. Please specify only one." + ) + sys.exit(1) + + return args + + +def remove_duplicates(items: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for item in items: + if item not in seen: + seen.add(item) + out.append(item) + return out + + +def choose_board_interactively(boards: list[str]) -> list[str]: + print("Available boards:") + boards = remove_duplicates(sorted(boards)) + for i, board in enumerate(boards): + print(f"[{i}]: {board}") + print("[all]: All boards") + out: list[str] = [] + while True: + try: + input_str = input( + "Enter the number of the board(s) you want to compile to, or it's name(s): " + ) + if "all" in input_str: + return boards + for board in input_str.split(","): + if board == "": + continue + if not board.isdigit(): + out.append(board) # Assume it's a board name. + else: + index = int(board) # Find the board from the index. + if 0 <= index < len(boards): + out.append(boards[index]) + else: + warnings.warn(f"invalid board index: {index}, skipping") + if not out: + print("Please try again.") + continue + return out + except ValueError: + print("Invalid input. Please enter a number.") + + +def resolve_example_path(example: str) -> Path: + example_path = HERE.parent / "examples" / example + if not example_path.exists(): + raise FileNotFoundError(f"Example '{example}' not found at '{example_path}'") + return example_path + + +def generate_build_info( + board: Board, board_build_dir: Path, defines: list[str] +) -> bool: + """Generate build_info.json file for the board using pio project metadata.""" + import json + import tempfile + + board_name = board.board_name + real_board_name = board.get_real_board_name() + + # Create a temporary project to get metadata + with tempfile.TemporaryDirectory() as temp_dir: + temp_project = Path(temp_dir) / "temp_project" + temp_project.mkdir(parents=True, exist_ok=True) + + # Initialize a temporary project + cmd_list: List[str] = [ + "pio", + "project", + "init", + "--project-dir", + str(temp_project), + "--board", + real_board_name, + ] + + # Add platform-specific options + if board.platform: + cmd_list.append(f"--project-option=platform={board.platform}") + if board.platform_packages: + cmd_list.append( + f"--project-option=platform_packages={board.platform_packages}" + ) + if board.framework: + cmd_list.append(f"--project-option=framework={board.framework}") + if board.board_build_core: + cmd_list.append( + f"--project-option=board_build.core={board.board_build_core}" + ) + if board.board_build_filesystem_size: + cmd_list.append( + f"--project-option=board_build.filesystem_size={board.board_build_filesystem_size}" + ) + + # Add defines + all_defines = defines.copy() + if board.defines: + all_defines.extend(board.defines) + if all_defines: + build_flags_str = " ".join(f"-D{define}" for define in all_defines) + cmd_list.append(f"--project-option=build_flags={build_flags_str}") + + if board.customsdk: + cmd_list.append(f"--project-option=custom_sdkconfig={board.customsdk}") + + try: + print(f"Initializing temp project for {board_name} with cmd: {cmd_list}") + # Initialize the project + result = subprocess.run( + cmd_list, + capture_output=True, + text=True, + cwd=str(temp_project), + timeout=60, + ) + + if result.returncode != 0: + locked_print( + f"Warning: Failed to initialize temp project for {board_name}: {result.stderr}" + ) + return False + + # Get metadata + metadata_cmd = ["pio", "project", "metadata", "--json-output"] + metadata_result = subprocess.run( + metadata_cmd, + capture_output=True, + text=True, + cwd=str(temp_project), + timeout=60, + ) + + if metadata_result.returncode != 0: + locked_print( + f"Warning: Failed to get metadata for {board_name}: {metadata_result.stderr}" + ) + return False + + # Parse and save the metadata + try: + data = json.loads(metadata_result.stdout) + + # Add tool aliases (from create_build_dir.py) + + insert_tool_aliases(data) + + # Save to build_info.json + build_info_path = board_build_dir / "build_info.json" + with open(build_info_path, "w") as f: + json.dump(data, f, indent=4, sort_keys=True) + + locked_print(f"Generated build_info.json for {board_name}") + return True + + except json.JSONDecodeError as e: + locked_print( + f"Warning: Failed to parse metadata JSON for {board_name}: {e}" + ) + return False + + except subprocess.TimeoutExpired: + locked_print( + f"Warning: Timeout generating build_info.json for {board_name}" + ) + return False + except Exception as e: + import traceback + + traceback.print_exc() + warnings.warn( + f"Warning: Exception generating build_info.json for {board_name}: {e}" + ) + return False + + +def setup_library_for_windows(board_build_dir: Path, example_path: Path) -> Path: + """Set up the FastLED library for Windows builds by copying files.""" + lib_dir = ( + board_build_dir / example_path.name / ".pio" / "libdeps" / "uno" / "FastLED" + ) + lib_dir.mkdir(parents=True, exist_ok=True) + + # Copy the library files + src_dir = HERE.parent + if lib_dir.exists(): + shutil.rmtree(lib_dir) + shutil.copytree( + src_dir, + lib_dir, + ignore=shutil.ignore_patterns(".git", ".pio", "__pycache__", ".vscode"), + ) + + return lib_dir + + +def compile_with_pio_ci( + board: Board, + example_paths: list[Path], + build_dir: str | None, + defines: list[str], + verbose: bool, +) -> tuple[bool, str]: + """Compile examples for a board using pio ci command.""" + + # Skip web boards + if board.board_name == "web": + locked_print(f"Skipping web target for board {board.board_name}") + return True, "" + + board_name = board.board_name + real_board_name = board.get_real_board_name() + + # Set up build directory in .build/fled/examples/{board_name} + if build_dir: + board_build_dir = Path(build_dir) / board_name + else: + board_build_dir = Path(".build") / "fled" / "examples" / board_name + + board_build_dir.mkdir(parents=True, exist_ok=True) + + # Generate build_info.json for this board + generate_build_info(board, board_build_dir, defines) + + locked_print(f"*** Compiling examples for board {board_name} using pio ci ***") + + errors: List[str] = [] + + for example_path in example_paths: + locked_print( + f"*** Building example {example_path.name} for board {board_name} ***" + ) + + # Find the .ino file in the example directory + ino_files = list(example_path.glob("*.ino")) + if not ino_files: + error_msg = f"No .ino file found in {example_path}" + locked_print(f"ERROR: {error_msg}") + errors.append(error_msg) + continue + + # Use the first .ino file found + ino_file = ino_files[0] + + # Find all .cpp files in the example directory (in addition to .ino file) + cpp_files = list(example_path.glob("*.cpp")) + + # Collect all source files to compile (.ino + .cpp) + source_files = [ino_file] + cpp_files + + if cpp_files: + locked_print( + f" Found {len(cpp_files)} additional .cpp file(s) to compile: {[f.name for f in cpp_files]}" + ) + + # Get absolute path to FastLED library using platform's natural path format + fastled_path = str(HERE.parent.absolute()) + lib_option = f"lib_deps=symlink://{fastled_path}" + + # Set up board-specific build cache directory with absolute path via symlink directive + # cache_dir = HERE.parent / ".pio_cache" / board.board_name + # absolute_cache_dir = cache_dir.resolve() + # cache_option = f"build_cache_dir=filelink://{absolute_cache_dir}" + + cache_option = None + + # NEW: Ensure stale board_build.core settings are removed from previous builds. + existing_ini_path = board_build_dir / example_path.name / "platformio.ini" + if existing_ini_path.exists(): + try: + with open(existing_ini_path, "r", encoding="utf-8") as _f: + ini_lines = _f.readlines() + new_lines = [ + ln + for ln in ini_lines + if not ln.strip().startswith("board_build.core") + ] + if len(new_lines) != len(ini_lines): + with open(existing_ini_path, "w", encoding="utf-8") as _f: + _f.writelines(new_lines) + except Exception as _: + # Non-fatal cleanup failure – continue with compilation + pass + + # Build pio ci command + cmd_list: List[str] = [ + "pio", + "ci", + str(ino_file), + "--board", + real_board_name, + "--keep-build-dir", + "--build-dir", + str(board_build_dir / example_path.name), + "--project-option", + lib_option, + "--project-option", + "lib_extra_dirs=/workspace", + ] + + if cache_option is not None: + cmd_list.append("--project-option") + cmd_list.append(cache_option) + + # Check for additional source directories in the example and collect them + example_include_dirs: List[str] = [] + example_src_dirs: List[str] = [] + + # Always show some level of scanning info + has_subdirs = any( + subdir.is_dir() + and subdir.name not in [".git", "__pycache__", ".pio", ".vscode"] + for subdir in example_path.iterdir() + ) + + if has_subdirs or verbose: + locked_print(f"Scanning {example_path.name} for additional sources...") + + # First, check for header files in the example root directory itself + # (e.g., defs.h files that need to be accessible to .ino files) + root_header_files = [ + f + for f in example_path.iterdir() + if f.is_file() and f.suffix in [".h", ".hpp"] + ] + + if root_header_files: + example_include_dirs.append(str(example_path)) + locked_print( + f" Found {len(root_header_files)} header file(s) in root directory" + ) + if verbose: + for header in root_header_files: + locked_print(f" -> {header.name}") + + # Then check subdirectories + for subdir in example_path.iterdir(): + if subdir.is_dir() and subdir.name not in [ + ".git", + "__pycache__", + ".pio", + ".vscode", + ]: + # Check if this directory contains source files + header_files = [ + f + for f in subdir.rglob("*") + if f.is_file() and f.suffix in [".h", ".hpp"] + ] + source_files = [ + f + for f in subdir.rglob("*") + if f.is_file() and f.suffix in [".cpp", ".c"] + ] + + if header_files: + example_include_dirs.append(str(subdir)) + locked_print( + f" Found {len(header_files)} header file(s) in: {subdir.name}" + ) + if verbose: + for header in header_files: + locked_print(f" -> {header.relative_to(example_path)}") + + if source_files: + example_src_dirs.append(str(subdir)) + locked_print( + f" Found {len(source_files)} source file(s) in: {subdir.name}" + ) + if verbose: + for source in source_files: + locked_print(f" -> {source.relative_to(example_path)}") + + # Show summary if we found additional sources + if example_include_dirs or example_src_dirs: + locked_print( + f" Added {len(example_include_dirs)} include dir(s), {len(example_src_dirs)} source dir(s)" + ) + + # Add platform-specific options + if board.platform: + cmd_list.extend(["--project-option", f"platform={board.platform}"]) + + if board.platform_packages: + cmd_list.extend( + ["--project-option", f"platform_packages={board.platform_packages}"] + ) + + if board.framework: + cmd_list.extend(["--project-option", f"framework={board.framework}"]) + + if board.board_build_core: + cmd_list.extend( + ["--project-option", f"board_build.core={board.board_build_core}"] + ) + + if board.board_build_filesystem_size: + cmd_list.extend( + [ + "--project-option", + f"board_build.filesystem_size={board.board_build_filesystem_size}", + ] + ) + + # Add defines and include paths + all_defines = defines.copy() + if board.defines: + all_defines.extend(board.defines) + + build_flags_list: List[str] = [] + + # Add optimization report flag for all builds (generates optimization_report.txt) + # The report will be created in the build directory where GCC runs + build_flags_list.append("-fopt-info-all=optimization_report.txt") + + # Add defines as build flags + if all_defines: + build_flags_list.extend(f"-D{define}" for define in all_defines) + + # Add example include directories as build flags + if example_include_dirs: + build_flags_list.extend( + f"-I{include_dir}" for include_dir in example_include_dirs + ) + + # Add build flags and unflags directly using project options + if build_flags_list: + build_flags_str = " ".join(build_flags_list) + cmd_list.extend(["--project-option", f"build_flags={build_flags_str}"]) + + # Show custom defines (excluding the optimization report flag) + custom_defines: List[str] = [ + flag + for flag in build_flags_list + if flag.startswith("-D") and "FASTLED" in flag + ] + if custom_defines or verbose: + if custom_defines: + locked_print(f"Custom defines: {' '.join(custom_defines)}") + if verbose: + locked_print(f"All build flags: {build_flags_str}") + + # Add build_unflags if provided + # build_unflags no longer used + + # Add example source directories as libraries + for src_dir in example_src_dirs: + cmd_list.extend(["--lib", src_dir]) + + # Add example include directories as libraries (for header files like defs.h) + for include_dir in example_include_dirs: + # Only add as --lib if it's not already added as a source dir + if include_dir not in example_src_dirs: + cmd_list.extend(["--lib", include_dir]) + + # Copy header files to build src directory preserving subdirectory structure + # This ensures that .ino files can find header files using relative paths like "shared/defs.h" + build_src_dir = board_build_dir / example_path.name / "src" + for include_dir in example_include_dirs: + include_path = Path(include_dir) + if str(include_dir) == str(example_path): # Root directory headers + # Copy .h/.hpp files to build src directory + for header_file in include_path.glob("*.h"): + build_src_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(header_file, build_src_dir) + if verbose: + locked_print( + f" Copied header file: {header_file.name} -> {build_src_dir}" + ) + for header_file in include_path.glob("*.hpp"): + build_src_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(header_file, build_src_dir) + if verbose: + locked_print( + f" Copied header file: {header_file.name} -> {build_src_dir}" + ) + else: # Subdirectory headers - preserve directory structure + # Get the relative path from example root to this include directory + try: + rel_path = include_path.relative_to(example_path) + target_subdir = build_src_dir / rel_path + + # Copy all header files from this subdirectory + header_files = list(include_path.glob("*.h")) + list( + include_path.glob("*.hpp") + ) + if header_files: + target_subdir.mkdir(parents=True, exist_ok=True) + for header_file in header_files: + shutil.copy2(header_file, target_subdir) + if verbose: + locked_print( + f" Copied header file: {rel_path}/{header_file.name} -> {target_subdir}" + ) + except ValueError: + # Include directory is not relative to example path, skip + pass + + # Add custom SDK config if specified + if board.customsdk: + cmd_list.extend(["--project-option", f"custom_sdkconfig={board.customsdk}"]) + locked_print(f"Using custom SDK config: {board.customsdk}") + + # Only add verbose flag to pio ci when explicitly requested + if verbose: + cmd_list.append("--verbose") + + # Execute the command + cmd_str = subprocess.list2cmdline(cmd_list) + locked_print(f"Building {example_path.name} for {board_name}...") + if verbose: + locked_print(f"Command: {cmd_str}") + + start_time = time.time() + + try: + # Launch subprocess.Popen and capture output line by line with timestamps + result = subprocess.Popen( + cmd_list, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + cwd=str(HERE.parent), + ) + + # Capture output lines in real-time with timestamp buffer + stdout_lines: List[str] = [] + timestamped_lines: List[str] = [] + + if result.stdout: + for line in iter(result.stdout.readline, ""): + if line: + line_stripped = line.rstrip() + stdout_lines.append(line_stripped) + + # Add elapsed time since build started with 2 decimal places + elapsed_time = time.time() - start_time + timestamp = f"{elapsed_time:.2f}" + timestamped_line = f"{timestamp} {line_stripped}" + timestamped_lines.append(timestamped_line) + + if verbose: + # In verbose mode, show each line immediately with timestamp + locked_print(timestamped_line) + else: + # In normal mode, show only essential build steps (one per line) + # Be more specific to avoid showing long compiler command lines + line_lower = line_stripped.lower() + show_line = False + + # Show actual source file compilation (but not compiler commands) + if "compiling .pio" in line_lower: + show_line = True + # Show linking step + elif ( + line_stripped.startswith("Linking") + or "linking" in line_lower + ): + show_line = True + # Show memory usage + elif line_stripped.startswith( + "RAM:" + ) or line_stripped.startswith("Flash:"): + show_line = True + # Show build results + elif any( + result in line_stripped + for result in ["SUCCESS", "FAILED"] + ): + show_line = True + # Show errors and warnings (but avoid long command lines) + elif ( + "error:" in line_lower or "warning:" in line_lower + ) and not line_stripped.startswith("avr-"): + show_line = True + # Show "Building in release mode" but not compiler commands + elif ( + line_stripped == "Building in release mode" + or line_stripped == "Building in debug mode" + ): + show_line = True + + if show_line: + locked_print(timestamped_line) + + # Wait for process to complete + result.wait() + + stdout = "\n".join(stdout_lines) + stderr = "" + returncode = result.returncode + + elapsed_time = time.time() - start_time + + if returncode == 0: + locked_print( + f"*** Successfully built {example_path.name} for {board_name} in {elapsed_time:.2f}s ***" + ) + # Print location of generated platformio.ini for this build + platformio_ini_path = ( + board_build_dir / example_path.name / "platformio.ini" + ) + locked_print(f"Writing to platformio.ini {platformio_ini_path}") + if verbose and stdout: + locked_print(f"Final build summary:\n{stdout}") + else: + error_msg = f"Failed to build {example_path.name} for {board_name}" + locked_print(f"ERROR: {error_msg}") + if verbose: + locked_print(f"Command: {cmd_str}") + if stdout: + locked_print(f"STDOUT:\n{stdout}") + if stderr: + locked_print(f"STDERR:\n{stderr}") + errors.append(f"{error_msg}: {stderr}") + + except subprocess.TimeoutExpired: + error_msg = f"Timeout building {example_path.name} for {board_name}" + locked_print(f"ERROR: {error_msg}") + errors.append(error_msg) + except Exception as e: + error_msg = f"Exception building {example_path.name} for {board_name}: {e}" + locked_print(f"ERROR: {error_msg}") + errors.append(error_msg) + + if errors: + return False, "\n".join(errors) + + return True, f"Successfully compiled all examples for {board_name}" + + +def run_symbol_analysis(boards: list[Board]) -> None: + """Run symbol analysis on compiled outputs if requested.""" + locked_print("\nRunning symbol analysis on compiled outputs...") + + for board in boards: + if board.board_name == "web": + continue + + try: + locked_print(f"Running symbol analysis for board: {board.board_name}") + + cmd = [ + "uv", + "run", + "ci/util/symbol_analysis.py", + "--board", + board.board_name, + ] + + result = subprocess.run( + cmd, capture_output=True, text=True, cwd=str(HERE.parent) + ) + + if result.returncode != 0: + locked_print( + f"ERROR: Symbol analysis failed for board {board.board_name}: {result.stderr}" + ) + else: + locked_print(f"Symbol analysis completed for board: {board.board_name}") + if result.stdout: + print(result.stdout) + + except Exception as e: + locked_print( + f"ERROR: Exception during symbol analysis for board {board.board_name}: {e}" + ) + + +def main() -> int: + """Main function.""" + args = parse_args() + + if args.supported_boards: + print(",".join(DEFAULT_BOARDS_NAMES)) + return 0 + + # Determine which boards to compile for + if args.interactive: + boards_names = choose_board_interactively( + DEFAULT_BOARDS_NAMES + OTHER_BOARDS_NAMES + ) + else: + boards_names = args.boards.split(",") if args.boards else DEFAULT_BOARDS_NAMES + + # Get board objects + boards: list[Board] = [] + for board_name in boards_names: + try: + board = create_board(board_name, no_project_options=False) + boards.append(board) + except Exception as e: + locked_print(f"ERROR: Failed to get board '{board_name}': {e}") + return 1 + + # Determine which examples to compile + if args.positional_examples: + # Convert positional examples, handling both "examples/Blink" and "Blink" formats + examples = [] + for example in args.positional_examples: + # Remove "examples/" prefix if present + if example.startswith("examples/"): + example = example[len("examples/") :] + examples.append(example) + elif args.examples: + examples = args.examples.split(",") + else: + examples = DEFAULT_EXAMPLES + + # Process example exclusions + if args.exclude_examples: + exclude_examples = args.exclude_examples.split(",") + examples = [ex for ex in examples if ex not in exclude_examples] + + # Resolve example paths + example_paths: list[Path] = [] + for example in examples: + try: + example_path = resolve_example_path(example) + example_paths.append(example_path) + except FileNotFoundError as e: + locked_print(f"ERROR: {e}") + return 1 + + # Add extra examples for specific boards + extra_examples: dict[Board, list[Path]] = {} + for board in boards: + if board in EXTRA_EXAMPLES: + board_examples = [] + for example in EXTRA_EXAMPLES[board]: + try: + board_examples.append(resolve_example_path(example)) + except FileNotFoundError as e: + locked_print(f"WARNING: {e}") + if board_examples: + extra_examples[board] = board_examples + + # Set up defines + defines: list[str] = [] + if args.defines: + defines.extend(args.defines.split(",")) + + # Add FASTLED_ALL_SRC define when --allsrc or --no-allsrc flag is specified + if args.allsrc: + defines.append("FASTLED_ALL_SRC=1") + elif args.no_allsrc: + defines.append("FASTLED_ALL_SRC=0") + + # Start compilation + start_time = time.time() + locked_print( + f"Starting compilation for {len(boards)} boards with {len(example_paths)} examples" + ) + + compilation_errors: List[str] = [] + + # Compile for each board + for board in boards: + board_examples = example_paths.copy() + if board in extra_examples: + board_examples.extend(extra_examples[board]) + + success, message = compile_with_pio_ci( + board=board, + example_paths=board_examples, + build_dir=args.build_dir, + defines=defines, + verbose=args.verbose, + ) + + if not success: + compilation_errors.append(f"Board {board.board_name}: {message}") + locked_print(f"ERROR: Compilation failed for board {board.board_name}") + # Continue with other boards instead of stopping + + # Run symbol analysis if requested + if args.symbols: + run_symbol_analysis(boards) + + # Report results + elapsed_time = time.time() - start_time + time_str = time.strftime("%Mm:%Ss", time.gmtime(elapsed_time)) + + if compilation_errors: + locked_print( + f"\nCompilation finished in {time_str} with {len(compilation_errors)} error(s):" + ) + for error in compilation_errors: + locked_print(f" - {error}") + return 1 + else: + locked_print(f"\nAll compilations completed successfully in {time_str}") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + locked_print("\nInterrupted by user") + sys.exit(1) diff --git a/v1.5/lib/FastLED/ci/ci-cppcheck.py b/v1.5/lib/FastLED/ci/ci-cppcheck.py new file mode 100644 index 0000000..cb6c536 --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-cppcheck.py @@ -0,0 +1,134 @@ +import argparse +import os +import subprocess +import sys +from pathlib import Path + + +MINIMUM_REPORT_SEVERTIY = "medium" +MINIMUM_FAIL_SEVERTIY = "high" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run cppcheck on the project") + parser.add_argument("board", nargs="?", help="Board to check, optional") + return parser.parse_args() + + +def find_platformio_project_dir(build_root: Path, board_name: str) -> Path | None: + """Find a directory containing platformio.ini file for the specified board. + + Handles both old and new build directory structures: + Old: .build/uno/Blink/platformio.ini + New: .build/fled/examples/uno/Blink/platformio.ini + + Returns the directory containing platformio.ini, or None if not found. + """ + if not build_root.exists(): + return None + + # Search patterns for different build structures + search_patterns = [ + # New structure: .build/fled/examples/{board}/**/*.ini + build_root / "fled" / "examples" / board_name, + # Old structure: .build/{board}/**/*.ini + build_root / board_name, + # Fallback: search all subdirectories for board name + ] + + # Try each search pattern + for base_dir in search_patterns: + if not base_dir.exists(): + continue + + # Look for platformio.ini in this directory and its subdirectories + project_dir = find_platformio_ini_in_tree(base_dir) + if project_dir: + return project_dir + + # Last resort: search the entire build directory for board-related platformio.ini + for platformio_ini in build_root.rglob("platformio.ini"): + # Check if this platformio.ini is for our board by looking at the path + if board_name in str(platformio_ini): + print(f"Found platformio.ini in {platformio_ini.parent}") + return platformio_ini.parent + + return None + + +def find_platformio_ini_in_tree(base_dir: Path) -> Path | None: + """Recursively search for platformio.ini in a directory tree.""" + if not base_dir.exists(): + return None + + # Check direct subdirectories first (most common case) + for subdir in base_dir.iterdir(): + if subdir.is_dir(): + platformio_ini = subdir / "platformio.ini" + if platformio_ini.exists(): + print(f"Found platformio.ini in {subdir}") + return subdir + + # Check if there's a platformio.ini directly in the base directory + if (base_dir / "platformio.ini").exists(): + print(f"Found platformio.ini directly in {base_dir}") + return base_dir + + return None + + +def main() -> int: + args = parse_args() + here = Path(__file__).parent + project_root = here.parent + build = project_root / ".build" + + if not build.exists(): + print(f"Build directory {build} not found") + return 1 + + if args.board: + # Search for the specified board + project_dir = find_platformio_project_dir(build, args.board) + if not project_dir: + print(f"No platformio.ini found for board '{args.board}'") + print("This usually means the board hasn't been compiled yet.") + print(f"Try running: uv run ci/ci-compile.py {args.board} --examples Blink") + return 1 + else: + # Auto-detect available board by searching for any platformio.ini + project_dir = None + + # Try to find any platformio.ini file in the build directory + for platformio_ini in build.rglob("platformio.ini"): + project_dir = platformio_ini.parent + print(f"Auto-detected project directory: {project_dir}") + break + + if not project_dir: + print(f"No platformio.ini files found in {build}") + print("This usually means no boards have been compiled yet.") + print("Try running: uv run ci/ci-compile.py uno --examples Blink") + return 1 + + print(f"Running pio check in {project_dir}") + os.chdir(str(project_dir)) + + # Run pio check command + cp = subprocess.run( + [ + "pio", + "check", + "--skip-packages", + "--src-filters=+", + f"--severity={MINIMUM_REPORT_SEVERTIY}", + f"--fail-on-defect={MINIMUM_FAIL_SEVERTIY}", + "--flags", + "--inline-suppr", + ], + ) + return cp.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/ci-flags.py b/v1.5/lib/FastLED/ci/ci-flags.py new file mode 100644 index 0000000..5759a85 --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-flags.py @@ -0,0 +1,6 @@ +# flake8: noqa +# ruff: skip + +Import("env") # type: ignore + +env.Append(CXXFLAGS=["-Wno-register"]) # type: ignore diff --git a/v1.5/lib/FastLED/ci/ci-iwyu.py b/v1.5/lib/FastLED/ci/ci-iwyu.py new file mode 100644 index 0000000..8665f23 --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci-iwyu.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false + +import argparse +import os +import subprocess +import sys +from pathlib import Path +from typing import List + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run include-what-you-use on the project" + ) + parser.add_argument("board", nargs="?", help="Board to check, optional") + parser.add_argument( + "--fix", + action="store_true", + help="Automatically apply suggested fixes using fix_includes", + ) + parser.add_argument( + "--mapping-file", + action="append", + help="Additional mapping file to use (can be specified multiple times)", + ) + parser.add_argument( + "--max-line-length", + type=int, + default=100, + help="Maximum line length for suggestions (default: 100)", + ) + parser.add_argument("--verbose", action="store_true", help="Enable verbose output") + return parser.parse_args() + + +def find_platformio_project_dir(board_dir: Path) -> Path | None: + """Find a directory containing platformio.ini file in the board's build directory. + + With the new pio ci build system, the structure is: + .build/uno/Blink/platformio.ini + .build/uno/SomeExample/platformio.ini + + We need to find one of these example directories that has a platformio.ini file. + """ + if not board_dir.exists(): + return None + + # Look for subdirectories containing platformio.ini + for subdir in board_dir.iterdir(): + if subdir.is_dir(): + platformio_ini = subdir / "platformio.ini" + if platformio_ini.exists(): + print(f"Found platformio.ini in {subdir}") + return subdir + + # Fallback: check if there's a platformio.ini directly in the board directory (old system) + if (board_dir / "platformio.ini").exists(): + print(f"Found platformio.ini directly in {board_dir}") + return board_dir + + return None + + +def check_iwyu_available() -> bool: + """Check if include-what-you-use is available in the system""" + try: + result = subprocess.run( + ["include-what-you-use", "--version"], + capture_output=True, + text=True, + timeout=10, + ) + return result.returncode == 0 + except ( + subprocess.CalledProcessError, + FileNotFoundError, + subprocess.TimeoutExpired, + ): + return False + + +def run_iwyu_on_cpp_tests(args: argparse.Namespace) -> int: + """Run IWYU on the C++ test suite using CMake integration""" + here = Path(__file__).parent + project_root = here.parent + + print("Running include-what-you-use on C++ test suite...") + + # Use the existing test infrastructure with --check flag + cmd = [ + "uv", + "run", + "test.py", + "--cpp", + "--check", # This enables IWYU via CMake + "--clang", + "--no-interactive", + ] + + if args.verbose: + cmd.append("--verbose") + + try: + result = subprocess.run(cmd, cwd=project_root) + return result.returncode + except subprocess.CalledProcessError as e: + print(f"IWYU analysis failed with return code {e.returncode}") + return e.returncode + + +def run_iwyu_on_platformio_project(project_dir: Path, args: argparse.Namespace) -> int: + """Run IWYU on a PlatformIO project""" + print(f"Running include-what-you-use in {project_dir}") + os.chdir(str(project_dir)) + + # Build mapping file arguments + mapping_args: List[str] = [] + + # Add FastLED mapping files if they exist + project_root = project_dir + while project_root.parent != project_root: # Find project root + if (project_root / "ci" / "iwyu").exists(): + break + project_root = project_root.parent + + fastled_mapping = project_root / "ci" / "iwyu" / "fastled.imp" + stdlib_mapping = project_root / "ci" / "iwyu" / "stdlib.imp" + + if fastled_mapping.exists(): + mapping_args.extend(["--mapping_file", str(fastled_mapping)]) + + if stdlib_mapping.exists(): + mapping_args.extend(["--mapping_file", str(stdlib_mapping)]) + + # Add user-specified mapping files + if args.mapping_file: + for mapping in args.mapping_file: + mapping_args.extend(["--mapping_file", mapping]) + + # Build IWYU command + iwyu_cmd = [ + "include-what-you-use", + f"--max_line_length={args.max_line_length}", + "--quoted_includes_first", + "--no_comments", + ] + + if args.verbose: + iwyu_cmd.append("--verbose=3") + else: + iwyu_cmd.append("--verbose=1") + + iwyu_cmd.extend(mapping_args) + + # Run through PlatformIO's check system with IWYU + pio_cmd = [ + "pio", + "check", + "--skip-packages", + "--src-filters=+", + "--tool=include-what-you-use", + "--flags", + ] + iwyu_cmd[1:] # Skip the include-what-you-use binary name + + try: + result = subprocess.run(pio_cmd) + return result.returncode + except subprocess.CalledProcessError as e: + print(f"PlatformIO IWYU check failed with return code {e.returncode}") + return e.returncode + + +def apply_iwyu_fixes(source_dir: Path) -> int: + """Apply IWYU fixes using fix_includes tool""" + try: + # Check if fix_includes is available + subprocess.run(["fix_includes", "--help"], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print( + "Warning: fix_includes tool not found. Install IWYU tools to use --fix option." + ) + return 1 + + # Find all .h and .cpp files + cpp_files: List[Path] = [] + for pattern in ["**/*.cpp", "**/*.h", "**/*.hpp"]: + cpp_files.extend(source_dir.glob(pattern)) + + if not cpp_files: + print("No C++ files found to fix") + return 0 + + print(f"Applying IWYU fixes to {len(cpp_files)} files...") + + # Run fix_includes on the source directory + cmd = ["fix_includes", "--update_comments", str(source_dir)] + + try: + result = subprocess.run(cmd) + if result.returncode == 0: + print("IWYU fixes applied successfully") + else: + print(f"fix_includes failed with return code {result.returncode}") + return result.returncode + except subprocess.CalledProcessError as e: + print(f"Failed to apply IWYU fixes: {e}") + return e.returncode + + +def main() -> int: + args = parse_args() + here = Path(__file__).parent + project_root = here.parent + + # Check if IWYU is available + if not check_iwyu_available(): + print("Error: include-what-you-use not found in PATH") + print("Install it with:") + print(" Ubuntu/Debian: sudo apt install iwyu") + print(" macOS: brew install include-what-you-use") + print(" Or build from source: https://include-what-you-use.org/") + return 1 + + print("Found include-what-you-use") + + # If no board specified, run on C++ test suite + if not args.board: + print("No board specified, running IWYU on C++ test suite") + return run_iwyu_on_cpp_tests(args) + + # Run on specific board + build = project_root / ".build" + + if not build.exists(): + print(f"Build directory {build} not found") + print("Run a compilation first: ./compile [board] --examples [example]") + return 1 + + board_dir = build / args.board + if not board_dir.exists(): + print(f"Board {args.board} not found in {build}") + print("Available boards:") + for d in build.iterdir(): + if d.is_dir(): + print(f" {d.name}") + return 1 + + project_dir = find_platformio_project_dir(board_dir) + if not project_dir: + print(f"No platformio.ini found in {board_dir} or its subdirectories") + print("This usually means the board hasn't been compiled yet.") + print(f"Try running: ./compile {args.board} --examples Blink") + return 1 + + # Run IWYU on the PlatformIO project + result = run_iwyu_on_platformio_project(project_dir, args) + + # Apply fixes if requested and analysis succeeded + if args.fix and result == 0: + src_dir = project_dir / "src" + if src_dir.exists(): + fix_result = apply_iwyu_fixes(src_dir) + if fix_result != 0: + result = fix_result + + return result + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/ci/fingerprint_cache.py b/v1.5/lib/FastLED/ci/ci/fingerprint_cache.py new file mode 100644 index 0000000..ecc37df --- /dev/null +++ b/v1.5/lib/FastLED/ci/ci/fingerprint_cache.py @@ -0,0 +1,238 @@ +""" +Fingerprint Cache Feature + +A two-layer file change detection system that efficiently determines if source files +have been modified by combining fast modification time checks with slower but accurate +MD5 hash verification. +""" + +import hashlib +import json +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Optional + + +@dataclass +class CacheEntry: + """Cache entry storing file modification time and content hash.""" + + modification_time: float + md5_hash: str + + +@dataclass +class FingerprintCacheConfig: + """Configuration for fingerprint cache behavior.""" + + cache_file: Path + hash_algorithm: str + ignore_patterns: list[str] + max_cache_size: int + + +class FingerprintCache: + """ + Two-layer file change detection using modification time and content hashing. + + Provides efficient change detection by: + 1. Fast modification time comparison (microsecond performance) + 2. Accurate content verification via MD5 hashing when needed + """ + + def __init__(self, cache_file: Path, modtime_only: bool = False): + """ + Initialize fingerprint cache. + + Args: + cache_file: Path to JSON cache file + modtime_only: When True, disable hashing and rely solely on modtime. + """ + self.cache_file = cache_file + self._modtime_only: bool = modtime_only + self.cache = self._load_cache() + + def _load_cache(self) -> Dict[str, CacheEntry]: + """ + Load cache from JSON file, return empty dict if file doesn't exist. + + Returns: + Dictionary mapping file paths to cache entries + """ + if not self.cache_file.exists(): + return {} + + try: + with open(self.cache_file, "r") as f: + data = json.load(f) + + # Convert JSON dict to CacheEntry objects + cache: Dict[str, CacheEntry] = {} + for file_path, entry_data in data.items(): + cache[file_path] = CacheEntry( + modification_time=entry_data["modification_time"], + md5_hash=entry_data["md5_hash"], + ) + return cache + except (json.JSONDecodeError, KeyError, TypeError): + # Cache corrupted - start fresh + return {} + + def _save_cache(self) -> None: + """Save current cache state to JSON file.""" + # Ensure cache directory exists + self.cache_file.parent.mkdir(parents=True, exist_ok=True) + + # Convert CacheEntry objects to JSON-serializable dict + data = {} + for file_path, entry in self.cache.items(): + data[file_path] = { + "modification_time": entry.modification_time, + "md5_hash": entry.md5_hash, + } + + with open(self.cache_file, "w") as f: + json.dump(data, f, indent=2) + + def _compute_md5(self, file_path: Path) -> str: + """ + Compute MD5 hash of file content. + + Args: + file_path: Path to file to hash + + Returns: + MD5 hash as hexadecimal string + + Raises: + IOError: If file cannot be read + """ + try: + hasher = hashlib.md5() + with open(file_path, "rb") as f: + # Read in chunks to handle large files efficiently + for chunk in iter(lambda: f.read(8192), b""): + hasher.update(chunk) + return hasher.hexdigest() + except IOError as e: + raise IOError(f"Cannot read file {file_path}: {e}") + + def has_changed(self, src_path: Path, previous_modtime: float) -> bool: + """ + Determine if a source file has changed since the last known modification time. + + Two-layer verification process: + 1. Fast modification time check - if times match, return False immediately + 2. Content verification - compute/use cached MD5 hash for accurate comparison + + Args: + src_path: Path to the source file to check + previous_modtime: Previously known modification time (Unix timestamp) + + Returns: + True if the file has changed, False if unchanged + + Raises: + FileNotFoundError: If source file doesn't exist + """ + if not src_path.exists(): + raise FileNotFoundError(f"Source file not found: {src_path}") + + current_modtime = os.path.getmtime(src_path) + + # Optional mode: strictly use modification time only (no hashing) + # This is required for toolchains that invalidate PCH on any newer + # dependency regardless of content changes (e.g., Clang). + if self._modtime_only: + # Treat as changed only when file is newer than the reference time + # (keeps behavior stable when reference is an external artifact's mtime) + return current_modtime > previous_modtime + + # Layer 1: Quick modification time check + if current_modtime == previous_modtime: + return False # No change detected + + file_key = str(src_path.resolve()) # Use absolute path as key + + # Layer 2: Content verification via hash comparison + if file_key in self.cache: + cached_entry = self.cache[file_key] + previous_hash = ( + cached_entry.md5_hash + ) # Store previous hash before potential update + + if current_modtime == cached_entry.modification_time: + # File is cached with current modtime - use cached hash + current_hash = cached_entry.md5_hash + else: + # Cache is stale - compute new hash + current_hash = self._compute_md5(src_path) + self._update_cache_entry(src_path, current_modtime, current_hash) + + # Compare current hash with previous cached hash + # If they match, content hasn't actually changed despite modtime difference + return current_hash != previous_hash + + else: + # File not in cache - compute hash and cache it + current_hash = self._compute_md5(src_path) + self._update_cache_entry(src_path, current_modtime, current_hash) + return True # Assume changed since we have no previous state to compare + + def _update_cache_entry( + self, file_path: Path, modification_time: float, md5_hash: str + ) -> None: + """ + Update cache with new entry and save to disk. + + Args: + file_path: Path to file being cached + modification_time: File modification time + md5_hash: File content hash + """ + file_key = str(file_path.resolve()) + self.cache[file_key] = CacheEntry( + modification_time=modification_time, md5_hash=md5_hash + ) + self._save_cache() + + def get_cache_stats(self) -> dict[str, int]: + """ + Get cache statistics. + + Returns: + Dictionary with cache size and other metrics + """ + return { + "total_entries": len(self.cache), + "cache_file_exists": self.cache_file.exists(), + "cache_file_size_bytes": self.cache_file.stat().st_size + if self.cache_file.exists() + else 0, + } + + def clear_cache(self) -> None: + """Clear all cache entries and remove cache file.""" + self.cache.clear() + if self.cache_file.exists(): + self.cache_file.unlink() + + +def has_changed(src_path: Path, previous_modtime: float, cache_file: Path) -> bool: + """ + Convenience function implementing the main API. + + Args: + src_path: Path to the source file to check + previous_modtime: Previously known modification time (Unix timestamp) + cache_file: Path to the fingerprint cache file + + Returns: + True if the file has changed, False if unchanged + + Raises: + FileNotFoundError: If source file doesn't exist + """ + cache = FingerprintCache(cache_file) + return cache.has_changed(src_path, previous_modtime) diff --git a/v1.5/lib/FastLED/ci/compile-commands.py b/v1.5/lib/FastLED/ci/compile-commands.py new file mode 100644 index 0000000..8b65a41 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compile-commands.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +""" +Compile Commands Generator + +Generates a compile_commands.json for clangd/clang-tidy based on explicit +tools/flags configuration. See FEATURE.md for usage and rationale. + +Key behaviors: +- Reads configuration from ci/build_commands.toml +- Enumerates library source files under src/ (respecting optional filters.exclude_dirs) +- Emits one entry per translation unit with accurate arguments/command fields + +Notes: +- This script intentionally avoids project-unit-test driven flags to keep + IntelliSense consistent with the core FastLED library build. +""" + +from __future__ import annotations + +import _thread +import argparse +import json +import os +import subprocess +import sys + +# Require Python 3.11+ environment for TOML parsing +import tomllib +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional, cast + +# Require typeguard; no try-import fallbacks +from typeguard import typechecked + +# Use the canonical BuildFlags/Compiler tooling +from ci.compiler.clang_compiler import commands_json + + +PROJECT_ROOT: Path = Path(__file__).resolve().parent.parent +# Default to bespoke config for compile-commands generation +DEFAULT_CONFIG_PATH: Path = PROJECT_ROOT / "ci" / "build_commands.toml" + + +@typechecked +@dataclass +class CliConfig: + build_flags_toml: Path + exclude_dirs: list[str] + + +@typechecked +@dataclass +class Args: + output: Path + clean: bool + verbose: bool + config: Path + exclude: list[str] + + +def parse_args(args: Optional[list[str]] = None) -> Args: + parser = argparse.ArgumentParser( + description=( + "Generate compile_commands.json for clangd/clang-tidy based on explicit build configuration." + ) + ) + parser.add_argument( + "--output", + dest="output", + metavar="PATH", + help="Output path for compile_commands.json (default: project root)", + ) + parser.add_argument( + "--config", + dest="config", + metavar="PATH", + help=( + "Path to BuildFlags-style TOML (default: ci/build_example.toml). " + "Must be parseable by BuildFlags.parse()." + ), + ) + parser.add_argument( + "--clean", + dest="clean", + action="store_true", + help="Rebuild command set from scratch (ignored currently; reserved)", + ) + parser.add_argument( + "--verbose", + dest="verbose", + action="store_true", + help="Print collected flags/files", + ) + parser.add_argument( + "--exclude-dir", + dest="exclude", + action="append", + default=[], + help="Relative directory under project root to exclude (can be used multiple times)", + ) + + parsed = parser.parse_args(args) + + output_path_str: Optional[str] = None + if hasattr(parsed, "output"): + output_path_str = parsed.output + + if output_path_str is None or len(output_path_str) == 0: + # Default to project root compile_commands.json + output_path = PROJECT_ROOT / "compile_commands.json" + else: + output_path = Path(output_path_str).resolve() + + clean_value: bool = False + if hasattr(parsed, "clean"): + clean_value = bool(parsed.clean) + + verbose_value: bool = False + if hasattr(parsed, "verbose"): + verbose_value = bool(parsed.verbose) + + # Config path + config_path: Path + if getattr(parsed, "config", None): + config_path = Path(parsed.config).resolve() + else: + config_path = DEFAULT_CONFIG_PATH + + # Excluded directories + exclude_list: list[str] = list(getattr(parsed, "exclude", []) or []) + + return Args( + output=output_path, + clean=clean_value, + verbose=verbose_value, + config=config_path, + exclude=exclude_list, + ) + + +def _assert_config_exists(config_path: Path) -> None: + if not config_path.exists(): + raise RuntimeError( + f"CRITICAL: Required BuildFlags TOML not found at {config_path}. " + f"Please provide a valid configuration parseable by BuildFlags.parse()." + ) + + +def _create_cli_config(build_flags_toml: Path, exclude: list[str]) -> CliConfig: + return CliConfig(build_flags_toml=build_flags_toml, exclude_dirs=exclude) + + +def _collect_source_files(root: Path, exclude: list[str], verbose: bool) -> list[Path]: + source_files: list[Path] = [] + + # Compute excluded directories as absolute paths for quick prefix checks + excluded_dirs_abs: list[Path] = [] + for ex in exclude: + p = (PROJECT_ROOT / ex).resolve() + excluded_dirs_abs.append(p) + + src_root: Path = PROJECT_ROOT / "src" + if not src_root.exists(): + raise RuntimeError( + f"CRITICAL: Expected source root at {src_root} not found. " + f"This path is required to enumerate translation units." + ) + + for dirpath, dirnames, filenames in os.walk(src_root): + current_dir = Path(dirpath).resolve() + + # Apply directory exclusions + skip_dir: bool = False + for ex in excluded_dirs_abs: + try: + current_dir.relative_to(ex) + skip_dir = True + break + except ValueError: + # Not under this excluded path + pass + + if skip_dir: + # Prune search by clearing dirnames to avoid descending further + del dirnames[:] + continue + + for fname in filenames: + if fname.endswith(".cpp") or fname.endswith(".c"): + source_files.append(current_dir / fname) + + if verbose: + print(f"Discovered {len(source_files)} source files under {src_root}") + + if len(source_files) == 0: + raise RuntimeError( + "CRITICAL: No source files discovered under 'src/'. " + "Please verify the repository layout and filters.exclude_dirs configuration." + ) + + return source_files + + # No per-file argument composition here; delegated to commands_json() + # This function intentionally removed to rely on canonical toolchain + + # Emission handled by commands_json() + + +def main(entry_args: Optional[list[str]] = None) -> int: + args = parse_args(entry_args) + + # Log startup context + print("[compile-commands] Starting generation") + print(f"[compile-commands] Project root: {PROJECT_ROOT}") + print(f"[compile-commands] Config (BuildFlags TOML): {args.config}") + print(f"[compile-commands] Output path: {args.output}") + if args.exclude: + print(f"[compile-commands] Excluding directories: {args.exclude}") + else: + print("[compile-commands] No excluded directories configured") + + # Determine BuildFlags TOML path + _assert_config_exists(args.config) + + # Collect source files + source_root = PROJECT_ROOT / "src" + print(f"[compile-commands] Source root: {source_root}") + sources = _collect_source_files( + source_root, + exclude=args.exclude, + verbose=args.verbose, + ) + print(f"[compile-commands] Discovered {len(sources)} source files") + + # Delegate to canonical commands_json() using BuildFlags toolchain + print("[compile-commands] Generating compile_commands.json entries...") + commands_json( + config_path=args.config, + include_root=source_root, + sources=sources, + output_json=args.output, + quick_build=True, + strict_mode=False, + ) + + # Post-generation verification + if not args.output.exists(): + print( + f"CRITICAL: Generation completed but output file was not found at {args.output}" + ) + return 2 + + try: + text = args.output.read_text(encoding="utf-8") + # Best-effort count of entries to report status + import json as _json + from typing import cast + + entries = cast(list[dict[str, Any]], _json.loads(text)) + num_entries = len(entries) if isinstance(entries, list) else 0 + print( + f"[compile-commands] Wrote {num_entries} entries to {args.output} (size: {args.output.stat().st_size} bytes)" + ) + except Exception as e: # noqa: BLE001 + print( + f"WARNING: Output written to {args.output} but could not parse JSON for summary: {e}" + ) + + if args.verbose: + print("[compile-commands] Done.") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("Operation interrupted by user") + _thread.interrupt_main() + raise + except FileNotFoundError as e: + print(f"CRITICAL: Required file not found: {e}") + sys.exit(1) + except ValueError as e: + print(f"CRITICAL: Invalid configuration value: {e}") + sys.exit(1) + except Exception as e: # noqa: BLE001 + print(f"CRITICAL: Unexpected error: {e}") + raise diff --git a/v1.5/lib/FastLED/ci/compiled_size.py b/v1.5/lib/FastLED/ci/compiled_size.py new file mode 100644 index 0000000..a571a92 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiled_size.py @@ -0,0 +1,115 @@ +import argparse +import json +import re +import subprocess +from pathlib import Path +from typing import Any, Dict + + +def _create_board_info(path: Path) -> Dict[str, Any]: + build_info = json.loads(path.read_text()) + assert build_info.keys(), f"No boards found in {build_info}" + assert len(build_info.keys()) == 1, ( + f"Multiple boards found in {build_info}, so correct board should be specified" + ) + return build_info[next(iter(build_info))] + + +def _find_build_info(board: str) -> Path: + # Prefer PIO layout + candidate = Path(".build") / "pio" / board / "build_info.json" + if candidate.exists(): + return candidate + # Fallback legacy layout + candidate = Path(".build") / board / "build_info.json" + if candidate.exists(): + return candidate + raise FileNotFoundError( + f"build_info.json not found for board '{board}' in .build/pio/{board} or .build/{board}" + ) + + +def _run_pio_size(build_dir: Path) -> int | None: + try: + # Try to compute size without building first + result = subprocess.run( + ["pio", "run", "-d", str(build_dir), "-t", "size"], + capture_output=True, + text=True, + check=False, + ) + output = (result.stdout or "") + "\n" + (result.stderr or "") + m = re.search(r"Program:\s*(\d+)\s*bytes", output) + if m: + return int(m.group(1)) + # If size target did not yield, try a full build then retry size + subprocess.run( + ["pio", "run", "-d", str(build_dir)], capture_output=False, check=False + ) + result = subprocess.run( + ["pio", "run", "-d", str(build_dir), "-t", "size"], + capture_output=True, + text=True, + check=False, + ) + output = (result.stdout or "") + "\n" + (result.stderr or "") + m = re.search(r"Program:\s*(\d+)\s*bytes", output) + if m: + return int(m.group(1)) + except Exception: + pass + return None + + +def check_firmware_size(board: str) -> int: + build_info_json = _find_build_info(board) + board_info = _create_board_info(build_info_json) + assert board_info, f"Board {board} not found in {build_info_json}" + prog_path = Path(board_info["prog_path"]) # absolute or relative path + base_path = prog_path.parent + suffixes = [".bin", ".hex", ".uf2"] + firmware: Path | None = None + for suffix in suffixes: + candidate = base_path / f"firmware{suffix}" + if candidate.exists(): + firmware = candidate + break + if firmware is None: + # As a fallback, if firmware.* doesn't exist, use ELF size (filesize) if present + if prog_path.exists(): + return prog_path.stat().st_size + # As a last resort, run PlatformIO size target in the build directory + build_dir = build_info_json.parent + size = _run_pio_size(build_dir) + if size is not None: + return size + msg = ( + ", ".join([f"firmware{suffix}" for suffix in suffixes]) + + f" not found in {base_path}" + ) + raise FileNotFoundError(msg) + return firmware.stat().st_size + + +def main(board: str): + try: + size = check_firmware_size(board) + print(f"Firmware size for {board}: {size} bytes") + except FileNotFoundError as e: + print(f"Error: {e}") + except json.JSONDecodeError: + print(f"Error: Unable to parse build_info.json for {board}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Check FastLED firmware size for the specified board." + ) + parser.add_argument( + "--board", type=str, required=True, help="Board to check firmware size for" + ) + args = parser.parse_args() + + main(args.board) diff --git a/v1.5/lib/FastLED/ci/compiled_size_history.py b/v1.5/lib/FastLED/ci/compiled_size_history.py new file mode 100644 index 0000000..db2a97f --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiled_size_history.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import json +import os +import shutil +import subprocess +from pathlib import Path +from typing import List + +import dateutil.parser # type: ignore + + +HERE = Path(__file__).resolve().parent + + +def run_command(command: str): + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + output, error = process.communicate() + return output.decode("utf-8"), error.decode("utf-8") + + +def step_back_commits(steps: int) -> bool: + step_back_command = f"git reset --hard HEAD~{steps}" + output, error = run_command(step_back_command) + if error: + print(f"Error stepping back {steps} commit(s): {error}") + return False + return True + + +def check_firmware_size(board: str) -> int: + root_build_dir = Path(".build") / board + build_info_json = root_build_dir / "build_info.json" + build_info = json.loads(build_info_json.read_text()) + board_info = build_info.get(board) + assert board_info, f"Board {board} not found in {build_info_json}" + prog_path = Path(board_info["prog_path"]) + base_path = prog_path.parent + suffixes = [".bin", ".hex", ".uf2"] + firmware: Path + for suffix in suffixes: + firmware = base_path / f"firmware{suffix}" + if firmware.exists(): + break + else: + msg = ( + ", ".join([f"firmware{suffix}" for suffix in suffixes]) + + f" not found in {base_path}" + ) + raise FileNotFoundError(msg) + size_command = f"du -b {firmware}" + output, error = run_command(size_command) + if error: + print(f"Error checking firmware size: {error}") + return -1 + size_in_bytes = output.strip().split()[0] + return int(size_in_bytes) + + +def get_commit_hash() -> str | None: + hash_command = "git rev-parse HEAD" + output, error = run_command(hash_command) + if error: + print(f"Error getting commit hash: {error}") + return None + return output.strip() + + +def get_commit_date(commit_hash: str) -> str | None: + date_command = f"git show -s --format=%ci {commit_hash}" + output, error = run_command(date_command) + if error: + print(f"Error getting commit date: {error}") + return None + return dateutil.parser.parse(output.strip()).isoformat() + + +def main( + board: str, + num_commits: int, + skip_step: int, + start_commit: str | None = None, + end_commit: str | None = None, +): + # change to the script dir + os.chdir(str(HERE)) + # Create tmp directory if it doesn't exist + + if os.path.exists("tmp"): + shutil.rmtree("tmp") + os.makedirs("tmp", exist_ok=True) + + # Change to the tmp directory + os.chdir("tmp") + + # 1. Git clone FastLED repository + print("Cloning FastLED repository...") + clone_command = "git clone https://github.com/FastLED/FastLED.git" + output, error = run_command(clone_command) + # if error: + # print(f"Error cloning repository: {error}") + # os.chdir("..") + # return + + # Change to the FastLED directory + os.chdir("FastLED") + + # Checkout the latest commit + run_command("git checkout master") + + # If end_commit is specified, checkout that commit + # if end_commit: + # print(f"Checking out end commit: {end_commit}") + # checkout_command = f"git checkout {end_commit}" + # output, error = run_command(checkout_command) + # #if error: + # # print(f"Error checking out end commit: {error}") + # # return + + # Prepare CSV file + csv_filename = "../../firmware_sizes.csv" + with open(csv_filename, "w", newline="") as csvfile: + csvwriter = csv.writer(csvfile) + csvwriter.writerow(["datetime", "commit_hash", "binary_size"]) + + commits_checked = 0 + first_iteration = True + while True: + current_commit = get_commit_hash() + + if first_iteration and start_commit: + first_iteration = False + while True: + if current_commit == start_commit: + break + if not step_back_commits(1): + break + current_commit = get_commit_hash() + + if num_commits and commits_checked >= num_commits: + print(f"Checked {num_commits} commits") + break + + if end_commit and current_commit == end_commit: + print(f"Checked until end commit: {end_commit}") + break + + # 2. Run ci-compile.py for current commit + print(f"\nChecking commit {commits_checked + 1}") + + # remove .build/esp32dev/pio/build/esp32dev/ directory + board_files = Path(".build") / board / ".pio" / "build" / board + if board_files.exists(): + shutil.rmtree(str(board_files), ignore_errors=True) + compile_command = f"uv run -m ci.ci-compile {board} --examples Blink" + output, error = run_command(compile_command) + if error: + print(f"Error running ci-compile.py: {error}") + if not step_back_commits(skip_step): + break + continue + + # 3. Check firmware size and get commit hash + print("Checking firmware size...") + try: + size = check_firmware_size(board) + except FileNotFoundError as e: + print(f"Error checking firmware size: {e}") + if not step_back_commits(skip_step): + break + continue + except AssertionError as e: + print(f"Error: {e}") + if not step_back_commits(skip_step): + break + continue + commit_hash = get_commit_hash() + if size and commit_hash: + commit_date = get_commit_date(commit_hash) + print(f"Firmware size: {size} bytes") + + # Write to CSV incrementally + with open(csv_filename, "a", newline="") as csvfile: + csvwriter = csv.writer(csvfile) + csvwriter.writerow([commit_date, commit_hash, size]) + + print(f"Result appended to {csv_filename}") + + commits_checked += 1 + + # 4. Step back one commit + print("Stepping back 1 commit...") + if not step_back_commits(1): + break + + # Don't remove the tmp directory + print("\nTemporary directory 'tmp' has been left intact for inspection.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Check FastLED firmware size for multiple commits." + ) + parser.add_argument( + "--num-commits", type=int, default=1, help="Number of commits to check" + ) + parser.add_argument( + "--skip-step", + type=int, + default=1, + help="Number of commits to skip between checks", + ) + parser.add_argument("--start-commit", type=str, help="Starting commit hash") + parser.add_argument("--end-commit", type=str, help="Ending commit hash") + parser.add_argument( + "--board", type=str, required=True, help="Board to check firmware size for" + ) + args = parser.parse_args() + + if args.start_commit or args.end_commit: + if not (args.start_commit and not args.end_commit): + print("Both start commit and end commit must be specified.") + exit(1) + # if start_commit is specified, end_commit must be specified + + num_commits = args.num_commits + if args.start_commit and args.end_commit: + if args.start_commit == args.end_commit: + print("Start commit and end commit are the same.") + exit(1) + num_commits = 999999 + + main(args.board, num_commits, args.skip_step, args.start_commit, args.end_commit) diff --git a/v1.5/lib/FastLED/ci/compiler/__init__.py b/v1.5/lib/FastLED/ci/compiler/__init__.py new file mode 100644 index 0000000..72b7aff --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/__init__.py @@ -0,0 +1,5 @@ +""" +FastLED Compiler Module + +High-performance compilation system built on proven Python compiler API. +""" diff --git a/v1.5/lib/FastLED/ci/compiler/build_dynamic_lib.py b/v1.5/lib/FastLED/ci/compiler/build_dynamic_lib.py new file mode 100644 index 0000000..f80dc8d --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/build_dynamic_lib.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +FastLED Dynamic Library Builder +Builds FastLED as a shared/dynamic library for unit testing +""" + +import os +import subprocess +import sys +from pathlib import Path +from typing import List + +from ci.compiler.clang_compiler import ( + BuildFlags, + Compiler, + CompilerOptions, + Result, +) +from ci.util.paths import PROJECT_ROOT + + +def build_fastled_dynamic_library(build_dir: Path) -> Path: + """Build FastLED as a dynamic library""" + print("Building FastLED dynamic library...") + + # Define library path with appropriate extension + if sys.platform == "win32": + fastled_lib_path = build_dir / "fastled.dll" + else: + fastled_lib_path = build_dir / "libfastled.so" + + # Configure compiler with shared library flags + settings = CompilerOptions( + include_path=str(Path(PROJECT_ROOT) / "src"), + defines=[ + "STUB_PLATFORM", + "ARDUINO=10808", + "FASTLED_USE_STUB_ARDUINO", + "SKETCH_HAS_LOTS_OF_MEMORY=1", + "FASTLED_STUB_IMPL", + "FASTLED_FORCE_NAMESPACE=1", + "FASTLED_TESTING", + "FASTLED_NO_AUTO_NAMESPACE", + "FASTLED_NO_PINMAP", + "HAS_HARDWARE_PIN_SUPPORT", + # "FASTLED_DEBUG_LEVEL=1", + # "BUILDING_FASTLED_DLL", # New define for DLL exports + # "FASTLED_EXPORT=__declspec(dllexport)" if sys.platform == "win32" else "FASTLED_EXPORT=", # DLL exports for Windows + ], + std_version="c++17", + compiler="clang++", + compiler_args=[ + # NOTE: All compiler flags should come from build_unit.toml + # Keep only platform-specific include paths and shared library flags + "-I" + str(Path(PROJECT_ROOT) / "src/platforms/stub"), + "-I" + str(Path(PROJECT_ROOT) / "tests"), + # Add shared library flags - platform specific + "-shared" if sys.platform != "win32" else "/DLL", + "-fPIC" if sys.platform != "win32" else "", + ], + use_pch=True, + parallel=True, + ) + + # Load build flags from TOML + build_flags_path = Path(PROJECT_ROOT) / "ci" / "build_unit.toml" + build_flags = BuildFlags.parse( + build_flags_path, quick_build=False, strict_mode=False + ) + + compiler = Compiler(settings, build_flags) + + # Compile all FastLED source files + fastled_src_dir = Path(PROJECT_ROOT) / "src" + all_cpp_files = list(fastled_src_dir.rglob("*.cpp")) + + print(f"Found {len(all_cpp_files)} FastLED source files") + + # Compile to object files + object_files: List[Path] = [] + for src_file in all_cpp_files: + relative_path = src_file.relative_to(fastled_src_dir) + safe_name = ( + str(relative_path.with_suffix("")).replace("/", "_").replace("\\", "_") + ) + obj_path = build_dir / f"{safe_name}_fastled.o" + + future = compiler.compile_cpp_file( + src_file, + output_path=obj_path, + additional_flags=[ + "-c", + "-DFASTLED_STUB_IMPL", + "-DFASTLED_FORCE_NAMESPACE=1", + "-DFASTLED_NO_AUTO_NAMESPACE", + "-DFASTLED_NO_PINMAP", + "-DPROGMEM=", + "-DHAS_HARDWARE_PIN_SUPPORT", + "-DFASTLED_ENABLE_JSON=1", + "-fno-exceptions", + "-fno-rtti", + "-fPIC", # Position Independent Code for shared library + ], + ) + result: Result = future.result() + if not result.ok: + print(f"ERROR: Failed to compile {src_file}: {result.stderr}") + continue + + object_files.append(obj_path) + + print(f"Linking {len(object_files)} object files into dynamic library...") + + # Link as shared library + if sys.platform == "win32": + # Use lld-link for Windows DLL creation + link_cmd = [ + "lld-link", + "/DLL", + "/NOLOGO", + "/OUT:" + str(fastled_lib_path), + "/LIBPATH:C:/Program Files (x86)/Windows Kits/10/Lib/10.0.19041.0/um/x64", + "/LIBPATH:C:/Program Files (x86)/Windows Kits/10/Lib/10.0.19041.0/ucrt/x64", + "/LIBPATH:C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.37.32822/lib/x64", + *[str(obj) for obj in object_files], + "msvcrt.lib", + "legacy_stdio_definitions.lib", + "kernel32.lib", + "user32.lib", + ] + else: + link_cmd = [ + "clang++", + "-shared", + "-o", + str(fastled_lib_path), + *[str(obj) for obj in object_files], + ] + + try: + # Use streaming to prevent buffer overflow + process = subprocess.Popen( + link_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + encoding="utf-8", + errors="replace", + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] + + while True: + stdout_line = process.stdout.readline() if process.stdout else "" + stderr_line = process.stderr.readline() if process.stderr else "" + + if stdout_line: + stdout_lines.append(stdout_line.rstrip()) + if stderr_line: + stderr_lines.append(stderr_line.rstrip()) + + if process.poll() is not None: + remaining_stdout = process.stdout.read() if process.stdout else "" + remaining_stderr = process.stderr.read() if process.stderr else "" + + if remaining_stdout: + for line in remaining_stdout.splitlines(): + stdout_lines.append(line.rstrip()) + if remaining_stderr: + for line in remaining_stderr.splitlines(): + stderr_lines.append(line.rstrip()) + break + + stderr_result = "\n".join(stderr_lines) + if process.returncode != 0: + raise Exception(f"Failed to create dynamic library: {stderr_result}") + + print(f"Successfully created dynamic library: {fastled_lib_path}") + return fastled_lib_path + + except Exception as e: + print(f"ERROR: Exception during library creation: {e}") + raise + + +if __name__ == "__main__": + # Create build directory in temp + import tempfile + + build_dir = Path(tempfile.gettempdir()) / "fastled_test_build" + build_dir.mkdir(parents=True, exist_ok=True) + + # Build the dynamic library + build_fastled_dynamic_library(build_dir) diff --git a/v1.5/lib/FastLED/ci/compiler/cache_enhanced_compilation.py b/v1.5/lib/FastLED/ci/compiler/cache_enhanced_compilation.py new file mode 100644 index 0000000..ed44358 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/cache_enhanced_compilation.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +""" +Clean Cache-Enhanced Compilation + +Simple wrapper that adds fingerprint cache to FastLED compilation. +Dramatically speeds up incremental builds by skipping unchanged files. +""" + +import time +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Set + +from ci.ci.fingerprint_cache import FingerprintCache +from ci.compiler.clang_compiler import Compiler +from ci.compiler.test_example_compilation import CompilationResult + + +class CacheAwareCompiler: + """Simple wrapper that adds cache checking to compilation.""" + + def __init__(self, compiler: Compiler, cache_file: Path, verbose: bool = False): + self.compiler = compiler + self.cache = FingerprintCache(cache_file) + self.verbose = verbose + self.stats = { + "files_checked": 0, + "files_skipped": 0, + "files_compiled": 0, + "cache_hits": 0, + "cache_misses": 0, + } + + def should_compile(self, file_path: Path, baseline_time: float) -> bool: + """Check if file needs compilation using cache.""" + self.stats["files_checked"] += 1 + + try: + needs_compile = self.cache.has_changed(file_path, baseline_time) + + if needs_compile: + self.stats["cache_misses"] += 1 + self.stats["files_compiled"] += 1 + return True + else: + self.stats["cache_hits"] += 1 + self.stats["files_skipped"] += 1 + if self.verbose: + print(f"[CACHE] Skipping unchanged: {file_path.name}") + return False + except KeyboardInterrupt: + import _thread + + _thread.interrupt_main() + raise + except Exception as e: + if self.verbose: + print(f"[CACHE] Error checking {file_path.name}: {e}") + self.stats["cache_misses"] += 1 + self.stats["files_compiled"] += 1 + return True + + def _headers_changed(self, baseline_time: float) -> bool: + """Detect if any relevant header dependency has changed since last run. + + Uses the compiler's PCH dependency discovery to collect a conservative + set of headers that impact example compilation, then queries the + fingerprint cache to see if any actually changed content. + """ + try: + # Reuse the compiler's dependency discovery (covers src/** and platforms/**) + dependencies: List[Path] = [] + if hasattr(self.compiler, "_get_pch_dependencies"): + dependencies = self.compiler._get_pch_dependencies() # type: ignore[attr-defined] + else: + # Fallback: hash all headers under the compiler's include path + include_root = Path(self.compiler.settings.include_path) + for pattern in ("**/*.h", "**/*.hpp"): + dependencies.extend(include_root.glob(pattern)) + + changed_any = False + for dep in dependencies: + try: + if self.cache.has_changed(dep, baseline_time): + if self.verbose: + print(f"[CACHE] Header changed: {dep}") + changed_any = True + break + except FileNotFoundError: + # Missing dependency means we must rebuild conservatively + if self.verbose: + print(f"[CACHE] Header missing (forces rebuild): {dep}") + changed_any = True + break + return changed_any + except KeyboardInterrupt: + import _thread + + _thread.interrupt_main() + raise + except Exception as e: + if self.verbose: + print(f"[CACHE] Header scan failed, forcing rebuild: {e}") + return True + + def compile_with_cache( + self, + ino_files: List[Path], + pch_compatible_files: Set[Path], + log_fn: Callable[[str], None], + full_compilation: bool, + verbose: bool = False, + baseline_time: Optional[float] = None, + ) -> Dict[str, Any]: + """Compile only files that have changed.""" + + if baseline_time is None: + baseline_time = time.time() - 3600 # Default: 1 hour ago + + start_time = time.time() + + # If any header dependency changed, conservatively rebuild all example files + force_recompile_due_to_headers = self._headers_changed(baseline_time) + if force_recompile_due_to_headers and self.verbose: + print( + "[CACHE] Header dependency changes detected - forcing recompilation of example sources" + ) + + # Check which files need compilation + files_to_compile: List[Path] = [] + for ino_file in ino_files: + if force_recompile_due_to_headers or self.should_compile( + ino_file, baseline_time + ): + files_to_compile.append(ino_file) + + # Check .cpp files too + cpp_files_to_compile: List[Path] = [] + for ino_file in ino_files: + cpp_files = self.compiler.find_cpp_files_for_example(ino_file) + for cpp_file in cpp_files: + if force_recompile_due_to_headers or self.should_compile( + cpp_file, baseline_time + ): + cpp_files_to_compile.append(cpp_file) + + # Log cache results + total_files = len(ino_files) + sum( + len(self.compiler.find_cpp_files_for_example(f)) for f in ino_files + ) + log_fn( + f"[CACHE] {self.stats['files_skipped']}/{total_files} files unchanged, {self.stats['files_compiled']}/{total_files} need compilation" + ) + + if self.stats["files_skipped"] > 0: + time_saved = self.stats["files_skipped"] * 0.5 # Estimate + log_fn(f"[CACHE] Estimated time saved: {time_saved:.1f}s") + + # Compile files that changed + if files_to_compile or cpp_files_to_compile: + result = self._run_actual_compilation( + files_to_compile, + cpp_files_to_compile, + pch_compatible_files, + log_fn, + full_compilation, + verbose, + ) + else: + log_fn("[CACHE] All files unchanged - no compilation needed!") + result = self._create_success_result(len(ino_files), ino_files) + + # Log final stats + if total_files > 0: + hit_rate = ( + ( + self.stats["cache_hits"] + / (self.stats["cache_hits"] + self.stats["cache_misses"]) + * 100 + ) + if (self.stats["cache_hits"] + self.stats["cache_misses"]) > 0 + else 0 + ) + skip_rate = self.stats["files_skipped"] / total_files * 100 + log_fn( + f"[CACHE] {hit_rate:.1f}% cache hit rate, {skip_rate:.1f}% files skipped" + ) + + return { + "compilation_result": result, + "cache_stats": self.stats, + "total_time": time.time() - start_time, + } + + def _run_actual_compilation( + self, + ino_files: List[Path], + cpp_files_to_compile: List[Path], + pch_compatible_files: Set[Path], + log_fn: Callable[[str], None], + full_compilation: bool, + verbose: bool, + ): + """Run the actual compilation for changed files.""" + from ci.compiler.test_example_compilation import compile_examples_simple + + # Temporarily filter .cpp files to only compile changed ones + original_method = self.compiler.find_cpp_files_for_example + + def filtered_cpp_files(ino_file: Path) -> List[Path]: + all_cpp = original_method(ino_file) + return [cpp for cpp in all_cpp if cpp in cpp_files_to_compile] + + self.compiler.find_cpp_files_for_example = filtered_cpp_files + + try: + return compile_examples_simple( + self.compiler, + ino_files, + pch_compatible_files, + log_fn, + full_compilation, + verbose, + ) + finally: + self.compiler.find_cpp_files_for_example = original_method + + def _create_success_result( + self, file_count: int, ino_files: Optional[List[Path]] = None + ) -> CompilationResult: + """Create a successful compilation result for cached files.""" + + # For cached files, we need to populate object_file_map with existing object files + # so that linking can proceed properly + object_file_map: Dict[Path, List[Path]] = {} + if ino_files: + for ino_file in ino_files: + # Find existing object files for this example + example_name = ino_file.parent.name + build_dir = Path(".build/examples") / example_name + + obj_files: List[Path] = [] + if build_dir.exists(): + # Look for the main .ino object file + ino_obj = build_dir / f"{ino_file.stem}.o" + if ino_obj.exists(): + obj_files.append(ino_obj) + + # Look for additional .cpp files in the same directory as the .ino + cpp_files = self.compiler.find_cpp_files_for_example(ino_file) + for cpp_file in cpp_files: + cpp_obj = build_dir / f"{cpp_file.stem}.o" + if cpp_obj.exists(): + obj_files.append(cpp_obj) + + if obj_files: + object_file_map[ino_file] = obj_files + + return CompilationResult( + successful_count=file_count, + failed_count=0, + compile_time=0.0, + failed_examples=[], + object_file_map=object_file_map, + ) + + +def create_cache_compiler( + compiler: Compiler, cache_file: Optional[Path] = None, verbose: bool = False +) -> CacheAwareCompiler: + """Create a cache-aware compiler. Simple factory function.""" + if cache_file is None: + cache_file = Path(".build/fingerprint_cache.json") + return CacheAwareCompiler(compiler, cache_file, verbose) diff --git a/v1.5/lib/FastLED/ci/compiler/cache_setup.scons b/v1.5/lib/FastLED/ci/compiler/cache_setup.scons new file mode 100644 index 0000000..4019f3a --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/cache_setup.scons @@ -0,0 +1,514 @@ +#!/usr/bin/env python3 +""" +Standalone cache setup script for PlatformIO builds with Python fake compilers. +This script is executed by PlatformIO as a post-build extra_script. + +Configuration is passed through environment variables to avoid template string issues. +""" + +# ruff: noqa: F405, F821 # Suppress SCons-specific import and undefined name warnings + +import json +import os +import shutil +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + + +# Import env and try to import projenv (SCons-specific imports) +# These are dynamically available in PlatformIO/SCons environment +try: + Import("env") # type: ignore[name-defined] # SCons-specific import + env: Any # SCons environment object +except NameError: + env = None # For type checking when not in SCons context + +_VERBOSE = os.environ.get("VERBOSE", "0") in ("1", "true", "True", "yes", "y") + +# Try to import projenv if it exists +has_projenv: bool = False +projenv: Optional[Any] = None +try: + Import("projenv") # type: ignore[name-defined] # SCons-specific import + # projenv is now available in scope from Import + has_projenv = True +except (NameError, Exception): + has_projenv = False + projenv = None + +# Import cached compiler functions with type stubs +create_cached_toolchain: Optional[Any] = None +get_platform_packages_paths: Optional[Any] = None +cached_compiler_available: bool = False + +try: + from ci.util.cached_compiler import ( + create_cached_toolchain, + get_platform_packages_paths, + ) + + cached_compiler_available = True +except ImportError as e: + print("WARNING: Could not import cached compiler module: " + str(e)) + cached_compiler_available = False + +# Debug: Dump the environment state to disk for inspection +env_dump: Dict[str, str] = {} +try: + if env is not None and hasattr(env, "Dictionary"): # type: ignore[has-type] + for key in env.Dictionary(): # type: ignore[union-attr] + try: + value = env[key] # type: ignore[index] + # Convert to string to avoid JSON serialization issues + env_dump[key] = str(value) # type: ignore[arg-type] + except Exception: + env_dump[key] = "" + else: + env_dump["error"] = "env not available or missing Dictionary method" +except Exception as e: + env_dump["error"] = f"Failed to access env: {e}" + +# Write environment dump to disk +env_dump_path = "env_dump.json" +with open(env_dump_path, "w") as f: + json.dump(env_dump, f, indent=2) +print("Environment state dumped to: " + env_dump_path) + +# Also dump projenv if available +if has_projenv and projenv is not None: + projenv_dump: Dict[str, str] = {} + for key in projenv.Dictionary(): # type: ignore + try: + value = projenv[key] # type: ignore + projenv_dump[key] = str(value) + except Exception: + projenv_dump[key] = "" + + projenv_dump_path = "projenv_dump.json" + with open(projenv_dump_path, "w") as f: + json.dump(projenv_dump, f, indent=2) + print("Projenv state dumped to: " + projenv_dump_path) + +# Read cache configuration from environment variables +cache_type = os.environ.get("FASTLED_CACHE_TYPE", "no_cache") +cache_executable = os.environ.get("FASTLED_CACHE_EXECUTABLE", "") +sccache_path = os.environ.get("FASTLED_SCCACHE_PATH", "") +sccache_dir = os.environ.get("FASTLED_SCCACHE_DIR", "") +sccache_cache_size = os.environ.get("FASTLED_SCCACHE_CACHE_SIZE", "2G") +xcache_path = os.environ.get("FASTLED_XCACHE_PATH", "") +debug_enabled = os.environ.get("FASTLED_CACHE_DEBUG", "0") == "1" + +print("Cache configuration from environment:") +print(" Cache type: " + cache_type) +print(" Cache executable: " + cache_executable) +print(" SCCACHE path: " + sccache_path) +print(" SCCACHE dir: " + sccache_dir) +print(" Debug enabled: " + str(debug_enabled)) + +# Set up cache environment variables for subprocess execution +try: + if env is not None and hasattr(env, "Append"): # type: ignore[has-type] + if sccache_dir: + env.Append(ENV={"SCCACHE_DIR": sccache_dir}) # type: ignore[union-attr] + os.environ["SCCACHE_DIR"] = sccache_dir + + if sccache_cache_size: + env.Append(ENV={"SCCACHE_CACHE_SIZE": sccache_cache_size}) # type: ignore[union-attr] + os.environ["SCCACHE_CACHE_SIZE"] = sccache_cache_size + + # Ensure sccache binary directory is on PATH so xcache can find it even + # inside ESP-IDF's virtual environments where PATH is heavily modified. + if sccache_path: + sccache_dir_path = str(Path(sccache_path).parent) + env.PrependENVPath("PATH", sccache_dir_path) # type: ignore[union-attr] + if has_projenv and projenv is not None and hasattr(projenv, "PrependENVPath"): + projenv.PrependENVPath("PATH", sccache_dir_path) # type: ignore[union-attr] + os.environ["PATH"] = sccache_dir_path + os.pathsep + os.environ.get("PATH", "") + + if debug_enabled: + env.Append(ENV={"XCACHE_DEBUG": "1", "SCCACHE_DEBUG": "1"}) # type: ignore[union-attr] + if has_projenv and projenv is not None and hasattr(projenv, "Append"): + projenv.Append(ENV={"XCACHE_DEBUG": "1", "SCCACHE_DEBUG": "1"}) # type: ignore[union-attr] +except Exception as e: + print(f"Warning: Failed to set up cache environment: {e}") + +# Check if cache is available and cached compiler system can be used +USE_CACHE = False + +if cached_compiler_available and cache_executable: + if cache_type == "xcache": + # For xcache, check if the Python script exists and sccache is available + USE_CACHE = ( + cache_executable + and xcache_path + and Path(xcache_path).exists() + and sccache_path + and shutil.which(sccache_path) + ) + + if USE_CACHE: + print("xcache wrapper detected and configured for Python fake compilers") + print(" xcache path: " + str(xcache_path)) + print(" cache executable: " + str(cache_executable)) + else: + # For sccache/ccache, check if executable is in PATH + USE_CACHE = shutil.which(cache_executable) is not None + + if USE_CACHE: + print( + str(cache_type) + " detected and configured for Python cached compilers" + ) + print(" cache executable: " + str(cache_executable)) +elif not cached_compiler_available: + print( + "WARNING: Python cached compiler system not available, cache will be disabled" + ) +else: + print( + "Cache executable not found: " + + str(cache_executable) + + ", cache will be disabled" + ) + +if USE_CACHE and env is not None and hasattr(env, "get"): # type: ignore[has-type] + # Get current compilers from environment + original_cc = env.get("CC") # type: ignore[union-attr] + original_cxx = env.get("CXX") # type: ignore[union-attr] + + print("DEBUG: Found compilers in env:") + # Use repr for safer type conversion + cc_str = repr(original_cc) + cxx_str = repr(original_cxx) + cc_type_str = type(original_cc).__name__ + cxx_type_str = type(original_cxx).__name__ + print(" CC: " + cc_str + " (type: " + cc_type_str + ")") + print(" CXX: " + cxx_str + " (type: " + cxx_type_str + ")") + + # Extract compiler information for fake compiler generation + def extract_compiler_info(compiler_env_var: Any) -> Optional[str]: + """Extract compiler name from environment variable value.""" + if not compiler_env_var: + return None + + if isinstance(compiler_env_var, list): + return str(compiler_env_var[0]) if compiler_env_var else None # type: ignore[arg-type] + else: + # Handle string values like "arm-none-eabi-gcc" or "gcc" + return str(compiler_env_var).split()[0] # type: ignore[arg-type] + + cc_name = extract_compiler_info(original_cc) or "gcc" + cxx_name = extract_compiler_info(original_cxx) or "g++" + + print("Extracted compiler names:") + print(" CC name: " + str(cc_name)) + print(" CXX name: " + str(cxx_name)) + + # Create toolchain info for fake compiler generation + toolchain_info: Dict[str, str] = { + "CC": cc_name, + "CXX": cxx_name, + } + + # Create cache config for fake compiler system + cache_config: Dict[str, str] = { + "CACHE_TYPE": cache_type, + "CACHE_EXECUTABLE": cache_executable, + "SCCACHE_PATH": sccache_path, + "SCCACHE_DIR": sccache_dir, + "XCACHE_PATH": xcache_path, + } + + # Check if toolset exists and is valid + # Create a cache key based on the configuration to invalidate cache when config changes + cache_key = f"{cc_name}_{cxx_name}_{cache_type}_{cache_executable}" + + # Cache in the local build directory (e.g., .build/pio/uno/) + cache_file = Path(current_dir) / "compiler_cache.json" + + cached_tools: Optional[Dict[str, str]] = None + + # Try to load from persistent cache file + if cache_file.exists(): + try: + import json + + with open(cache_file, "r") as f: + cache_data = json.load(f) + + cached_real_cc = cache_data.get("real_cc") + cached_real_cxx = cache_data.get("real_cxx") + cached_fake_cc = cache_data.get("fake_cc") + cached_fake_cxx = cache_data.get("fake_cxx") + + if ( + cached_real_cc + and cached_real_cxx + and cached_fake_cc + and cached_fake_cxx + ): + print("Found local compiler cache:") + print(f" Cache file: {cache_file}") + print(f" Real CC: {cached_real_cc}") + print(f" Real CXX: {cached_real_cxx}") + + # Check if fake compiler scripts still exist, recreate if needed + fake_cc_path = Path(cached_fake_cc.replace("python ", "")) + fake_cxx_path = Path(cached_fake_cxx.replace("python ", "")) + + if fake_cc_path.exists() and fake_cxx_path.exists(): + # Fast path: use existing cached compilers + # On Windows, ensure we have .cmd shims to avoid 'python script.py @file' + # causing Python to treat @file as the script name. + import sys as _sys + import os as _os + + is_win = ( + _os.name == "nt" + or _sys.platform.startswith("win") + or _sys.platform.startswith("cygwin") + or _sys.platform.startswith("msys") + ) + + def _ensure_cmd_wrapper(fake_str: str, real_path: str) -> str: + p = Path(fake_str.replace("python ", "")) + if is_win and p.suffix.lower() == ".py": + cmd = p.with_suffix(".cmd") + if not cmd.exists(): + # Create missing .cmd wrapper now + from ci.util.cached_compiler import create_cached_compiler_script + out_dir = p.parent + created = create_cached_compiler_script( + compiler_name=p.stem.split("cached_")[-1], + cache_executable=cache_config.get("CACHE_EXECUTABLE", "sccache"), + real_compiler_path=real_path, + output_dir=out_dir, + debug=debug_enabled, + ) + cmd = Path(str(created).replace("python ", "")) + if cmd.exists(): + return str(cmd) + return fake_str + + # Create/choose proper wrapper paths + new_cc = _ensure_cmd_wrapper(cached_fake_cc, cached_real_cc) + new_cxx = _ensure_cmd_wrapper(cached_fake_cxx, cached_real_cxx) + + cached_tools = {"CC": new_cc, "CXX": new_cxx} + + # Update cache file if we switched to .cmd + try: + if new_cc != cached_fake_cc or new_cxx != cached_fake_cxx: + cache_data["fake_cc"] = new_cc + cache_data["fake_cxx"] = new_cxx + with open(cache_file, "w") as f: + json.dump(cache_data, f, indent=2) + except KeyboardInterrupt as ke: + import _thread + _thread.interrupt_main() + raise ke + except Exception: + pass + print( + "SUCCESS: Using cached compilers (instant, no platform search needed):" + ) + print(f" CC: {cached_tools['CC']}") + print(f" CXX: {cached_tools['CXX']}") + print(" Platform search skipped - using cached toolset") + else: + print( + "Cached compiler scripts missing, recreating with cached real paths..." + ) + # Recreate cached compilers using cached real paths (fast) + cached_compilers_dir = Path(current_dir) / "cached_compilers" + cached_compilers_dir.mkdir(parents=True, exist_ok=True) + + from ci.util.cached_compiler import create_cached_compiler_script + + cache_executable = cache_config.get("CACHE_EXECUTABLE", "sccache") + + # Create cached CC script using cached real path + cached_cc_script = create_cached_compiler_script( + compiler_name="CC", + cache_executable=cache_executable, + real_compiler_path=cached_real_cc, + output_dir=cached_compilers_dir, + debug=debug_enabled, + ) + + # Create cached CXX script using cached real path + cached_cxx_script = create_cached_compiler_script( + compiler_name="CXX", + cache_executable=cache_executable, + real_compiler_path=cached_real_cxx, + output_dir=cached_compilers_dir, + debug=debug_enabled, + ) + + cached_tools = { + "CC": str(cached_cc_script), + "CXX": str(cached_cxx_script), + } + + # Update cache file with new script paths + cache_data["fake_cc"] = cached_tools["CC"] + cache_data["fake_cxx"] = cached_tools["CXX"] + + with open(cache_file, "w") as f: + json.dump(cache_data, f, indent=2) + + print("Recreated cached compilers using cached real paths:") + print(f" CC: {cached_tools['CC']}") + print(f" CXX: {cached_tools['CXX']}") + except Exception as e: + print(f"Warning: Failed to load cache file {cache_file}: {e}") + # Fall through to full recreation + + # If no valid cache found, create the toolset from scratch + if cached_tools is None: + print("No valid cache found, creating compiler toolset from scratch...") + print(" This is the first compile or configuration changed") + + # Get platform packages paths for toolchain resolution (expensive operation) + platform_packages: List[str] = [] + if get_platform_packages_paths is not None: + print("Searching platform packages (this may take ~10 seconds)...") + platform_packages = get_platform_packages_paths() + print(f"Found {len(platform_packages)} platform package directories") + + if create_cached_toolchain is not None: + try: + # Find the real compiler paths (expensive operation, done only once) + from ci.util.cached_compiler import find_toolchain_compiler + + print("Resolving real compiler paths...") + + real_cc_path = find_toolchain_compiler(cc_name, platform_packages) + real_cxx_path = find_toolchain_compiler(cxx_name, platform_packages) + + if not real_cc_path or not real_cxx_path: + print(f"ERROR: Could not find real compilers:") + print(f" CC '{cc_name}': {real_cc_path}") + print(f" CXX '{cxx_name}': {real_cxx_path}") + cached_tools = None + else: + print(f"Found real compilers:") + print(f" Real CC: {real_cc_path}") + print(f" Real CXX: {real_cxx_path}") + + # Create cached compiler scripts + cached_compilers_dir = Path(current_dir) / "cached_compilers" + cached_compilers_dir.mkdir(parents=True, exist_ok=True) + + from ci.util.cached_compiler import create_cached_compiler_script + + cache_executable = cache_config.get("CACHE_EXECUTABLE", "sccache") + + # Create cached CC script + cached_cc_script = create_cached_compiler_script( + compiler_name="CC", + cache_executable=cache_executable, + real_compiler_path=real_cc_path, + output_dir=cached_compilers_dir, + debug=debug_enabled, + ) + + # Create cached CXX script + cached_cxx_script = create_cached_compiler_script( + compiler_name="CXX", + cache_executable=cache_executable, + real_compiler_path=real_cxx_path, + output_dir=cached_compilers_dir, + debug=debug_enabled, + ) + + cached_tools = { + "CC": str(cached_cc_script), + "CXX": str(cached_cxx_script), + } + + print("Created new compiler toolset:") + print(f" CC: {cached_tools['CC']}") + print(f" CXX: {cached_tools['CXX']}") + + # Save to local build directory cache file + cache_data = { + "cache_key": cache_key, + "real_cc": real_cc_path, + "real_cxx": real_cxx_path, + "fake_cc": cached_tools["CC"], + "fake_cxx": cached_tools["CXX"], + "build_dir": str(current_dir), + "platform_packages_count": len(platform_packages), + } + + with open(cache_file, "w") as f: + json.dump(cache_data, f, indent=2) + + print(f"Saved compiler toolset to local cache: {cache_file}") + print(" This cache will persist across builds for this platform") + + except Exception as e: + print("ERROR: Toolset creation failed with exception: " + str(e)) + import traceback + + traceback.print_exc() + cached_tools = None + else: + print("ERROR: create_cached_toolchain function is None") + + if cached_tools: + # Use Python cached compilers instead of batch scripts + new_cc = cached_tools.get("CC") + new_cxx = cached_tools.get("CXX") + + if new_cc and new_cxx: + print("Created Python cached compilers:") + print(" CC: " + str(new_cc)) + print(" CXX: " + str(new_cxx)) + + # Apply to both environments + env.Replace(CC=new_cc, CXX=new_cxx) # type: ignore + if has_projenv and projenv is not None: + projenv.Replace(CC=new_cc, CXX=new_cxx) # type: ignore + print("Applied Python fake compilers to both env and projenv") + else: + print("Applied Python fake compilers to env (projenv not available)") + + # Apply to library builders (critical for framework caching) + try: + for lib_builder in env.GetLibBuilders(): # type: ignore + lib_builder.env.Replace(CC=new_cc, CXX=new_cxx) # type: ignore + if _VERBOSE: + print( + "Applied Python fake compilers to library builder: " + + str(getattr(lib_builder, "name", "unnamed")) + ) + except KeyboardInterrupt: + import _thread + _thread.interrupt_main() + raise + except Exception as e: + print("WARNING: Could not apply to library builders: " + str(e)) + + print("Python fake compiler cache enabled: " + str(cache_type)) + print(" Original CC: " + str(original_cc)) + print(" Original CXX: " + str(original_cxx)) + print(" Fake CC: " + str(new_cc)) + print(" Fake CXX: " + str(new_cxx)) + else: + print( + "ERROR: Failed to create Python fake compilers, falling back to no cache" + ) + USE_CACHE = False + else: + print("ERROR: Python fake compiler creation failed, falling back to no cache") + USE_CACHE = False + +if not USE_CACHE: + if cache_executable: + print("Warning: " + str(cache_type) + " setup failed; using default compilers") + else: + print("No cache executable configured; using default compilers") + +print("Python fake compiler cache environment configured successfully") diff --git a/v1.5/lib/FastLED/ci/compiler/clang_compiler.py b/v1.5/lib/FastLED/ci/compiler/clang_compiler.py new file mode 100644 index 0000000..b00284b --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/clang_compiler.py @@ -0,0 +1,3162 @@ +#!/usr/bin/env python3 + +""" +Clang compiler accessibility and testing functions for FastLED. +Supports the simple build system approach outlined in FEATURE.md. +""" + +import _thread +import os +import platform +import shutil +import subprocess +import sys +import tempfile +import time +import tomllib +import warnings +from concurrent.futures import Future, ThreadPoolExecutor +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Union + +from ..ci.fingerprint_cache import FingerprintCache + + +_HERE = Path(__file__).parent + + +def cpu_count() -> int: + # get the nubmer of cpus on the machine + import multiprocessing + + return multiprocessing.cpu_count() or 1 + + +def get_max_workers() -> int: + # Check for NO_PARALLEL environment variable + import os + + if os.environ.get("NO_PARALLEL"): + print("NO_PARALLEL environment variable set - forcing sequential compilation") + return 1 + return cpu_count() * 2 + + +_EXECUTOR = ThreadPoolExecutor(max_workers=get_max_workers()) + + +def optimize_python_command(cmd: list[str]) -> list[str]: + """ + Optimize command list for subprocess execution in uv environment. + + For python commands, we need to use 'uv run python' to ensure access to + installed packages like ziglang. Direct sys.executable bypasses uv environment. + + Args: + cmd: Command list that may contain 'python' as first element + + Returns: + list[str]: Optimized command with 'python' prefixed by 'uv run' + """ + if cmd and (cmd[0] == "python" or cmd[0] == "python3"): + # Use uv run python to ensure access to uv-managed packages + optimized_cmd = ["uv", "run", "python"] + cmd[1:] + return optimized_cmd + return cmd + + +@dataclass +class LibarchiveOptions: + """Configuration options for static library archive generation.""" + + use_thin: bool = False # Use thin archives (ar T flag) for faster linking + # Future expansion points: + # use_deterministic: bool = True # Deterministic archives (ar D flag) + # symbol_table: bool = True # Generate symbol table (ar s flag) + # verbose: bool = False # Verbose output (ar v flag) + + +@dataclass +class CompilerOptions: + """ + Configuration options for compilation operations. + Merged from former CompileOptions and CompilerSettings. + """ + + # Core compiler settings (formerly CompilerSettings) + include_path: str + compiler: str = "python -m ziglang c++" + defines: list[str] | None = None + std_version: str = "c++17" + compiler_args: list[str] = field(default_factory=list[str]) + + # PCH (Precompiled Headers) settings + use_pch: bool = False + pch_header_content: str | None = None + pch_output_path: str | None = None + + # Archive generation settings + archiver: str = "ar" # Default archiver tool + archiver_args: list[str] = field(default_factory=list[str]) + + # Compilation operation settings (formerly CompileOptions, unity options removed) + additional_flags: list[str] = field( + default_factory=list[str] + ) # Extra compiler flags + parallel: bool = True # Enable parallel compilation + temp_dir: str | Path | None = None # Custom temporary directory + + +@dataclass +class Result: + """ + Result of a subprocess.run command execution. + """ + + ok: bool + stdout: str + stderr: str + return_code: int + + +@dataclass +class UnityChunkResult: + """Result for a single unity chunk compilation.""" + + ok: bool + unity_cpp: Path + object_path: Path + stderr: str + return_code: int + + +@dataclass +class UnityChunksResult: + """Aggregate result for a chunked unity build.""" + + success: bool + chunks: list[UnityChunkResult] + + +@dataclass +class CompileResult: + """ + Result of a compile operation. + """ + + success: bool + output_path: str + stderr: str + size: int + command: list[str] + + +@dataclass +class VersionCheckResult: + """ + Result of a clang version check. + """ + + success: bool + version: str + error: str + + +@dataclass +class LinkOptions: + """Configuration options for program linking operations.""" + + # Output configuration + output_executable: Union[str, Path] # Output executable path + + # Input files + object_files: List[Union[str, Path]] = field( + default_factory=lambda: [] + ) # .o files to link + static_libraries: List[Union[str, Path]] = field( + default_factory=lambda: [] + ) # .a files to link + + # Linker configuration + linker: Optional[str] = None # Custom linker path (auto-detected if None) + linker_args: List[str] = field( + default_factory=lambda: [] + ) # All linker command line arguments + + # Platform-specific defaults can be added via helper functions + temp_dir: Optional[Union[str, Path]] = ( + None # Custom temporary directory for intermediate files + ) + + +@dataclass +class ArchiveOptions: + """Archive creation options loaded from build configuration TOML""" + + flags: str # Archive flags (e.g., "rcsD" for deterministic builds) + linux: Optional["ArchivePlatformOptions"] = None # Linux-specific flags + windows: Optional["ArchivePlatformOptions"] = None # Windows-specific flags + darwin: Optional["ArchivePlatformOptions"] = None # macOS-specific flags + + +@dataclass +class ArchivePlatformOptions: + """Platform-specific archive options""" + + flags: str # Platform-specific archive flags + + +@dataclass +class BuildTools: + """Build tool paths and configurations""" + + cpp_compiler: list[str] # C++ compiler (maps to CompilerOptions.compiler) + archiver: list[str] # Archive tool (maps to CompilerOptions.archiver) + linker: list[str] # Linker tool (maps to LinkOptions.linker) + c_compiler: list[str] # C compiler (maps to CompilerOptions.c_compiler) + objcopy: list[str] # Object copy utility (for firmware/embedded) + nm: list[str] # Symbol table utility (for analysis) + strip: list[str] # Strip utility (for release builds) + ranlib: list[str] # Archive indexer (for static libraries) + + +@dataclass +class BuildFlags: + """ + Build flags loaded from TOML configuration with parsing and serialization capabilities. + + This class represents the build flags structure and can parse from TOML files + and serialize back to TOML format. + """ + + defines: List[str] + compiler_flags: List[str] + include_flags: List[str] + link_flags: List[str] + strict_mode_flags: List[str] + tools: BuildTools + archive: ArchiveOptions + + @classmethod + def parse( + cls, toml_path: Path, quick_build: bool = False, strict_mode: bool = False + ) -> "BuildFlags": + """ + Parse build flags from TOML configuration file. + + Args: + toml_path: Path to the build configuration TOML file (build_unit.toml or build_example.toml) + quick_build: Use quick build mode flags (default: debug mode) + strict_mode: Enable strict mode warning flags + + Returns: + BuildFlags: Parsed build flags from TOML file + """ + try: + with open(toml_path, "rb") as f: + config = tomllib.load(f) + except FileNotFoundError: + raise FileNotFoundError( + f"Required build_flags.toml not found at {toml_path}" + ) + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + raise RuntimeError(f"Failed to parse build_flags.toml: {e}") + + # Extract tools configuration with validation + if "tools" not in config: + raise RuntimeError( + f"FATAL ERROR: [tools] section missing from build_flags.toml at {toml_path}\n" + f"The [tools] section is required and must define all compiler tools.\n" + f"Example:\n" + f"[tools]\n" + f'compiler_command = ["uv", "run", "python", "-m", "ziglang", "c++"]\n' + f'c_compiler = "clang"' + ) + + tools_config = config["tools"] + + # # Extract linker_command and archiver_command with validation + # linker_command = tools_config.get("linker_command", []) + # archiver_command = tools_config.get("archiver_command", []) + + # # Validate command fields if present + # if linker_command and not isinstance(linker_command, list): + # raise RuntimeError( + # f"FATAL ERROR: tools.linker_command must be a list in build_flags.toml at {toml_path}\n" + # f"Got: {linker_command} (type: {type(linker_command).__name__})\n" + # f'Expected: ["python", "-m", "ziglang", "c++"]' + # ) + + # if archiver_command and not isinstance(archiver_command, list): + # raise RuntimeError( + # f"FATAL ERROR: tools.archiver_command must be a list in build_flags.toml at {toml_path}\n" + # f"Got: {archiver_command} (type: {type(archiver_command).__name__})\n" + # f'Expected: ["python", "-m", "ziglang", "ar"]' + # ) + + cpp_compiler = tools_config.get("cpp_compiler", None) + linker = tools_config.get("linker", None) + c_compiler = tools_config.get("c_compiler", None) + objcopy = tools_config.get("objcopy", None) + nm = tools_config.get("nm", None) + strip = tools_config.get("strip", None) + ranlib = tools_config.get("ranlib", None) + archiver = tools_config.get("archiver", None) + + assert cpp_compiler is not None, ( + f"FATAL ERROR: cpp_compiler is not set in build_flags.toml at {toml_path}" + ) + assert linker is not None, ( + f"FATAL ERROR: linker is not set in build_flags.toml at {toml_path}" + ) + assert c_compiler is not None, ( + f"FATAL ERROR: c_compiler is not set in build_flags.toml at {toml_path}" + ) + assert objcopy is not None, ( + f"FATAL ERROR: objcopy is not set in build_flags.toml at {toml_path}" + ) + assert nm is not None, ( + f"FATAL ERROR: nm is not set in build_flags.toml at {toml_path}" + ) + assert strip is not None, ( + f"FATAL ERROR: strip is not set in build_flags.toml at {toml_path}" + ) + assert ranlib is not None, ( + f"FATAL ERROR: ranlib is not set in build_flags.toml at {toml_path}" + ) + assert archiver is not None, ( + f"FATAL ERROR: archiver is not set in build_flags.toml at {toml_path}" + ) + + # tools = BuildTools( + # compiler=cpp_compiler, # Use last element as fallback + # archiver=archiver, # Use fallback for deprecated field + # linker=linker, + # c_compiler=c_compiler, + # objcopy=objcopy, + # nm=nm, + # strip=strip, + # ranlib=ranlib, + # compiler_command=compiler_command, # Add the full command + # linker_command=linker, # Add the linker command + # archiver_command=archiver, # Add the archiver command + # ) + + tools = BuildTools( + cpp_compiler=cpp_compiler, + archiver=archiver, + linker=linker, + c_compiler=c_compiler, + objcopy=objcopy, + nm=nm, + strip=strip, + ranlib=ranlib, + ) + + # Extract archive configuration - NO DEFAULTS, must be in build_flags.toml + if "archive" not in config: + raise RuntimeError( + f"CRITICAL: [archive] section missing from build_flags.toml at {toml_path}\n" + f"The [archive] section is required for deterministic builds.\n" + f"Example:\n" + f"[archive]\n" + f'flags = "rcsD" # r=insert, c=create, s=symbol table, D=deterministic' + ) + + archive_config = config["archive"] + if "flags" not in archive_config: + raise RuntimeError( + f"CRITICAL: archive.flags missing from build configuration TOML at {toml_path}\n" + f"Archive flags are required for proper static library creation.\n" + f"Example:\n" + f"[archive]\n" + f'flags = "rcs" # r=insert, c=create, s=symbol table' + ) + + # Parse platform-specific archive configurations + linux_options = None + windows_options = None + darwin_options = None + + if "linux" in archive_config and "flags" in archive_config["linux"]: + linux_options = ArchivePlatformOptions( + flags=archive_config["linux"]["flags"] + ) + + if "windows" in archive_config and "flags" in archive_config["windows"]: + windows_options = ArchivePlatformOptions( + flags=archive_config["windows"]["flags"] + ) + + if "darwin" in archive_config and "flags" in archive_config["darwin"]: + darwin_options = ArchivePlatformOptions( + flags=archive_config["darwin"]["flags"] + ) + + archive_options = ArchiveOptions( + flags=archive_config["flags"], + linux=linux_options, + windows=windows_options, + darwin=darwin_options, + ) + + # Extract base flags + base_flags = cls( + defines=config.get("all", {}).get("defines", []), + compiler_flags=config.get("all", {}).get("compiler_flags", []), + include_flags=config.get("all", {}).get("include_flags", []), + link_flags=config.get("linking", {}).get("base", {}).get("flags", []), + strict_mode_flags=config.get("strict_mode", {}).get("flags", []), + tools=tools, + archive=archive_options, + ) + + # Add platform-specific flags if available + current_platform = platform.system().lower() + + # Check for new-style platform configuration (windows, linux, etc.) + if current_platform in config: + platform_config = config[current_platform] + + # Use CPP flags for C++ compilation (most common case) + if "cpp_flags" in platform_config: + base_flags.compiler_flags = platform_config["cpp_flags"] + + # Use platform-specific link flags + if "link_flags" in platform_config: + if current_platform == "windows": + # On Windows, replace base linking flags entirely (avoid pthread conflicts) + base_flags.link_flags = platform_config["link_flags"] + else: + # On other platforms, extend base linking flags + base_flags.link_flags.extend(platform_config["link_flags"]) + + # Fallback to old-style platform configuration for backward compatibility + else: + compiler_flags_config = config.get("compiler_flags", {}) + if current_platform in compiler_flags_config: + platform_flags = compiler_flags_config[current_platform].get( + "flags", [] + ) + if platform_flags: + base_flags.compiler_flags = platform_flags + + linking_config = config.get("linking", {}) + if current_platform in linking_config: + platform_link_flags = linking_config[current_platform].get("flags", []) + if platform_link_flags: + if current_platform == "windows": + base_flags.link_flags = platform_link_flags + else: + base_flags.link_flags.extend(platform_link_flags) + + # Add build mode specific flags + build_mode = "quick" if quick_build else "debug" + build_modes = config.get("build_modes", {}) + if build_mode in build_modes: + mode_config = build_modes[build_mode] + if "flags" in mode_config: + base_flags.compiler_flags.extend(mode_config["flags"]) + if "link_flags" in mode_config: + base_flags.link_flags.extend(mode_config["link_flags"]) + + return base_flags + + def serialize(self) -> str: + """ + Serialize build flags to TOML format string. + + Returns: + str: TOML representation of the build flags + """ + toml_content: List[str] = [] + + # Add header + header_lines: List[str] = [ + "# ================================================================================================", + "# FastLED Build Flags Configuration", + "# ================================================================================================", + "# Generated build flags configuration", + "", + ] + toml_content.extend(header_lines) + + # [all] section + all_section_lines: List[str] = [ + "[all]", + "# Universal compilation flags", + "", + ] + toml_content.extend(all_section_lines) + + # Add defines + if self.defines: + toml_content.append("defines = [") + for define in self.defines: + toml_content.append(f' "{define}",') + toml_content.append("]") + toml_content.append("") + + # Add compiler flags + if self.compiler_flags: + toml_content.append("compiler_flags = [") + for flag in self.compiler_flags: + toml_content.append(f' "{flag}",') + toml_content.append("]") + toml_content.append("") + + # Add include flags + if self.include_flags: + toml_content.append("include_flags = [") + for flag in self.include_flags: + toml_content.append(f' "{flag}",') + toml_content.append("]") + toml_content.append("") + + # [linking.base] section + if self.link_flags: + linking_section_lines: List[str] = [ + "[linking.base]", + "# Base linking flags", + "", + "flags = [", + ] + toml_content.extend(linking_section_lines) + for flag in self.link_flags: + toml_content.append(f' "{flag}",') + toml_content.append("]") + toml_content.append("") + + # [strict_mode] section + if self.strict_mode_flags: + strict_section_lines: List[str] = [ + "[strict_mode]", + "# Strict mode compilation flags", + "", + "flags = [", + ] + toml_content.extend(strict_section_lines) + for flag in self.strict_mode_flags: + toml_content.append(f' "{flag}",') + toml_content.append("]") + toml_content.append("") + + # [tools] section + tools_section_lines: List[str] = [ + "[tools]", + "# Build tool configuration", + "", + ] + toml_content.extend(tools_section_lines) + + # Add tools - using new field names + + # Add cpp_compiler if it exists + if self.tools.cpp_compiler: + toml_content.append(f"cpp_compiler = {self.tools.cpp_compiler!r}") + + # Add linker if it exists + if self.tools.linker: + toml_content.append(f"linker = {self.tools.linker!r}") + + # Add archiver if it exists + if self.tools.archiver: + toml_content.append(f"archiver = {self.tools.archiver!r}") + + # Add individual tool commands as strings (for backward compatibility) + if self.tools.c_compiler: + toml_content.append(f"c_compiler = {self.tools.c_compiler!r}") + if self.tools.objcopy: + toml_content.append(f"objcopy = {self.tools.objcopy!r}") + if self.tools.nm: + toml_content.append(f"nm = {self.tools.nm!r}") + if self.tools.strip: + toml_content.append(f"strip = {self.tools.strip!r}") + if self.tools.ranlib: + toml_content.append(f"ranlib = {self.tools.ranlib!r}") + toml_content.append("") + + # [archive] section + archive_section_lines = [ + "[archive]", + "# Archive creation flags for deterministic builds", + "", + f'flags = "{self.archive.flags}"', + "", + ] + toml_content.extend(archive_section_lines) + + return "\n".join(toml_content) + + def to_toml_file(self, toml_path: Path) -> None: + """ + Write build flags to TOML file. + + Args: + toml_path: Path where to write the TOML file + """ + toml_content = self.serialize() + + # Ensure directory exists + toml_path.parent.mkdir(parents=True, exist_ok=True) + + # Write to file + with open(toml_path, "w", encoding="utf-8") as f: + f.write(toml_content) + + @classmethod + def from_toml_file( + cls, toml_path: Path, quick_build: bool = False, strict_mode: bool = False + ) -> "BuildFlags": + """ + Convenience method to create BuildFlags from TOML file. + + This is an alias for parse() method for better readability. + + Args: + toml_path: Path to the build configuration TOML file (build_unit.toml or build_example.toml) + quick_build: Use quick build mode flags (default: debug mode) + strict_mode: Enable strict mode warning flags + + Returns: + BuildFlags: Parsed build flags from TOML file + """ + return cls.parse(toml_path, quick_build, strict_mode) + + +class Compiler: + """ + Compiler wrapper for FastLED .ino file compilation. + """ + + def __init__(self, settings: CompilerOptions, build_flags: BuildFlags): + self.settings = settings + self.build_flags = build_flags + self._pch_file_path: Path | None = None + self._pch_header_path: Path | None = None + self._pch_ready: bool = False + + # Initialize fingerprint cache for PCH optimization + cache_dir = Path(".build") / "cache" + cache_dir.mkdir(parents=True, exist_ok=True) + # For PCH validity, toolchains like Clang require strict modtime checks. + # Use modtime_only=True to avoid content-hash-based reuse that would + # conflict with Clang's mtime validation. + self._pch_cache = FingerprintCache( + cache_dir / "pch_fingerprint_cache.json", modtime_only=True + ) + + def get_compiler_args(self) -> list[str]: + """ + Get a copy of the complete compiler arguments that would be used for compilation. + + Uses compiler_args from build configuration TOML without any hardcoded modifications. + + Returns: + list[str]: Copy of compiler arguments from TOML configuration + """ + # Use compiler args exactly as specified in build configuration TOML + cmd = self.settings.compiler_args.copy() + + # Add standard compilation requirements (don't override std version - use what's in TOML) + cmd.extend( + [ + "-x", + "c++", # Force C++ compilation of .ino files + f"-I{self.settings.include_path}", # FastLED include path + ] + ) + + # Add defines if specified + if self.settings.defines: + for define in self.settings.defines: + cmd.append(f"-D{define}") + + # Add PCH flag if available + if self.settings.use_pch and self._pch_ready and self._pch_file_path: + cmd.extend(["-include-pch", str(self._pch_file_path)]) + + # Add standard compilation flags + cmd.extend(["-c"]) + + return cmd.copy() # Return a copy to prevent modification + + def generate_pch_header(self) -> str: + """ + Generate the default PCH header content with commonly used FastLED headers. + + Returns: + str: PCH header content with common includes + """ + if self.settings.pch_header_content: + return self.settings.pch_header_content + + # Default PCH content based on common FastLED example includes + default_content = """// FastLED PCH - Common headers for faster compilation +#pragma once + +// Core headers that are used in nearly all FastLED examples +#include +#include + +// Common C++ standard library headers +#include +#include +#include +""" + return default_content + + def _get_pch_dependencies(self) -> List[Path]: + """ + Get list of files that PCH depends on for change detection. + + Returns: + List of Path objects representing files that affect PCH validity + """ + dependencies: List[Path] = [] + + # FastLED.h is the main dependency + fastled_h = Path(self.settings.include_path) / "FastLED.h" + if fastled_h.exists(): + dependencies.append(fastled_h) + + # Core FastLED headers that are commonly included + core_headers = [ + "platforms.h", + "led_sysdefs.h", + "hsv2rgb.h", + "colorutils.h", + "colorpalettes.h", + "fastled_config.h", + ] + + for header in core_headers: + header_path = Path(self.settings.include_path) / header + if header_path.exists(): + dependencies.append(header_path) + + # CRITICAL: Add ALL source files in src/** directory + # PCH contains namespace configuration and other build settings that + # can be affected by any source file change + src_dir = Path(self.settings.include_path) + if src_dir.exists(): + # Add all header files recursively in src/ + for pattern in ["**/*.h", "**/*.hpp"]: + for header in src_dir.glob(pattern): + if header.is_file(): + dependencies.append(header) + + # Platform-specific headers + platform_dir = Path(self.settings.include_path) / "platforms" + if platform_dir.exists(): + # Add common platform headers + for pattern in ["*.h", "*/*.h"]: + for header in platform_dir.glob(pattern): + if header.is_file(): + dependencies.append(header) + + return dependencies + + def _should_rebuild_pch(self) -> bool: + """ + Check if PCH needs to be rebuilt based on dependency changes. + + Returns: + True if PCH should be rebuilt, False if cache is valid + """ + if not self.settings.use_pch: + return False + + # Set PCH file path if using configured path + if self.settings.pch_output_path: + self._pch_file_path = Path(self.settings.pch_output_path) + + # If PCH file doesn't exist, need to build + if not self._pch_file_path or not self._pch_file_path.exists(): + print("[PCH CACHE] PCH file doesn't exist, rebuilding required") + return True + + # Get PCH file modification time as baseline + pch_modtime = os.path.getmtime(self._pch_file_path) + + # Check if any PCH dependencies have changed + dependencies = self._get_pch_dependencies() + print(f"[PCH CACHE] Checking {len(dependencies)} PCH dependencies...") + + for dep_file in dependencies: + try: + if self._pch_cache.has_changed(dep_file, pch_modtime): + print( + f"[PCH CACHE] Dependency changed: {dep_file.name} - PCH rebuild required" + ) + return True + except FileNotFoundError: + print( + f"[PCH CACHE] Dependency not found: {dep_file.name} - PCH rebuild required" + ) + return True + + print("[PCH CACHE] All dependencies unchanged - using cached PCH") + return False + + def create_pch_file(self) -> bool: + """ + Create a precompiled header file for faster compilation. + + IMPORTANT: PCH generation uses direct clang++ compilation + for maximum compatibility. + + Returns: + bool: True if PCH creation was successful, False otherwise + """ + if not self.settings.use_pch: + return False + + # Check if existing PCH is still valid using fingerprint cache + if not self._should_rebuild_pch(): + # PCH is still valid, reuse existing one + if self.settings.pch_output_path: + self._pch_file_path = Path(self.settings.pch_output_path) + self._pch_ready = True + print("[PCH CACHE] Reusing valid cached PCH file") + return True + + try: + # Create PCH header content + pch_content = self.generate_pch_header() + + # Create temporary PCH header file + pch_header_file = tempfile.NamedTemporaryFile( + mode="w", suffix=".hpp", delete=False + ) + pch_header_file.write(pch_content) + pch_header_path = Path(pch_header_file.name) + pch_header_file.close() + + # Create PCH output path + if self.settings.pch_output_path: + pch_output_path = Path(self.settings.pch_output_path) + else: + pch_output_path = pch_header_path.with_suffix(".hpp.pch") + + # Build PCH compilation command using TOML configuration + # Start with the compiler command, then add args from TOML + compiler_parts = self.settings.compiler.split() + cmd: list[str] = optimize_python_command( + compiler_parts + self.settings.compiler_args.copy() + ) + + # Add PCH-specific flags (don't override std version - use what's in TOML) + cmd.extend( + [ + "-x", + "c++-header", + f"-I{self.settings.include_path}", + ] + ) + + # Add defines if specified + if self.settings.defines: + for define in self.settings.defines: + cmd.append(f"-D{define}") + + # Skip existing PCH flags to avoid conflicts, but keep everything else + skip_next = False + final_cmd: List[str] = [] + for arg in cmd: + if skip_next: + skip_next = False + continue + elif arg.startswith("-include-pch"): + skip_next = True # Skip the PCH file path argument too + continue + else: + final_cmd.append(arg) + + # Add the PCH file paths + final_cmd.extend([str(pch_header_path), "-o", str(pch_output_path)]) + cmd = final_cmd + + # DEBUG: Print the complete PCH compilation command + print("[PCH] Compilation Command:") + for i, arg in enumerate(cmd): + print(f" {i}: {arg}") + print() + + # Compile PCH with direct compiler (no sccache) - use single stream to prevent buffer overflow + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, + bufsize=1, # Line buffered + encoding="utf-8", # Explicitly use UTF-8 + errors="replace", # Replace invalid chars instead of failing + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] # Keep separate for API compatibility + + # Read only stdout (which contains both stdout and stderr) + while True: + output_line = process.stdout.readline() if process.stdout else "" + + # Process output lines (both stdout and stderr combined) + if output_line: + line_stripped = output_line.rstrip() + # Add to both lists for API compatibility + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + + # Check if process has finished + if process.poll() is not None: + # Read any remaining output + remaining_output = process.stdout.read() if process.stdout else "" + + if remaining_output: + for line in remaining_output.splitlines(): + line_stripped = line.rstrip() + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + break + + if process.returncode == 0: + self._pch_file_path = pch_output_path + self._pch_header_path = ( + pch_header_path # Keep track of header file for cleanup + ) + self._pch_ready = True + + # Update fingerprint cache for all PCH dependencies + print( + "[PCH CACHE] PCH compilation successful, updating dependency cache..." + ) + pch_modtime = os.path.getmtime(pch_output_path) + dependencies = self._get_pch_dependencies() + + for dep_file in dependencies: + try: + # Force cache update for each dependency + self._pch_cache.has_changed(dep_file, pch_modtime - 1) + print(f"[PCH CACHE] Cached dependency: {dep_file.name}") + except FileNotFoundError: + print( + f"[PCH CACHE] Warning: Could not cache dependency: {dep_file.name}" + ) + + return True + else: + # PCH compilation failed, clean up and continue without PCH + # Print debug info to help diagnose PCH issues + stderr_output = "\n".join(stderr_lines) + print( + f"[DEBUG] PCH compilation failed with command: {subprocess.list2cmdline(cmd)}" + ) + print(f"[DEBUG] PCH error output: {stderr_output}") + os.unlink(pch_header_path) + return False + + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + # PCH creation failed, continue without PCH + print(f"[DEBUG] PCH creation exception: {e}") + return False + + def cleanup_pch(self): + """Clean up PCH files.""" + if self._pch_file_path and self._pch_file_path.exists(): + try: + os.unlink(self._pch_file_path) + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception: + pass + if self._pch_header_path and self._pch_header_path.exists(): + try: + os.unlink(self._pch_header_path) + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception: + pass + self._pch_file_path = None + self._pch_header_path = None + self._pch_ready = False + + def check_clang_version(self) -> VersionCheckResult: + """ + Check that ziglang c++ is accessible and return version information. + Handles ziglang c++ compiler properly. + + Returns: + VersionCheckResult: Result containing success status, version string, and error message + """ + # Test python access first + try: + # Use optimized python -m ziglang c++ (no uv run overhead) + version_cmd = ["python", "-m", "ziglang", "c++", "--version"] + + # Optimize command to use sys.executable instead of shell 'python' resolution + python_exe = optimize_python_command(version_cmd) + + # Use Popen with stderr redirected to stdout to prevent buffer overflow + process = subprocess.Popen( + python_exe, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, + bufsize=1, + encoding="utf-8", + errors="replace", + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] # Keep separate for API compatibility + + # Read only stdout (which contains both stdout and stderr) + while True: + output_line = process.stdout.readline() if process.stdout else "" + + if output_line: + line_stripped = output_line.rstrip() + # Add to both lists for API compatibility + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + + if process.poll() is not None: + # Read remaining output + remaining_output = process.stdout.read() if process.stdout else "" + + if remaining_output: + for line in remaining_output.splitlines(): + line_stripped = line.rstrip() + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + break + + stdout_result = "\n".join(stdout_lines) + stderr_result = "\n".join(stderr_lines) + if process.returncode == 0 and "clang version" in stdout_result: + version = ( + stdout_result.split()[2] + if len(stdout_result.split()) > 2 + else "unknown" + ) + return VersionCheckResult( + success=True, version=f"ziglang:{version}", error="" + ) + else: + return VersionCheckResult( + success=False, + version="", + error=f"ziglang c++ version check failed: {stderr_result}", + ) + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + return VersionCheckResult( + success=False, version="", error=f"ziglang c++ not accessible: {str(e)}" + ) + + def compile_ino_file( + self, + ino_path: str | Path, + output_path: str | Path | None = None, + additional_flags: list[str] | None = None, + use_pch_for_this_file: bool | None = None, + ) -> Future[Result]: + """ + Compile a single .ino file using clang++ with optional temporary output. + + Args: + ino_path (str|Path): Path to the .ino file to compile + output_path (str|Path, optional): Output object file path. If None, uses temp file. + additional_flags (list, optional): Additional compiler flags + use_pch_for_this_file (bool, optional): Override PCH usage for this specific file. + If None, uses global PCH setting. If True/False, forces PCH on/off for this file. + + Returns: + Future[Result]: Future that will contain Result dataclass with subprocess execution details + """ + # Submit the compilation task to a thread pool + + return _EXECUTOR.submit( + self._compile_ino_file_sync, + ino_path, + output_path, + additional_flags, + use_pch_for_this_file, + ) + + def _compile_ino_file_sync( + self, + ino_path: str | Path, + output_path: str | Path | None = None, + additional_flags: list[str] | None = None, + use_pch_for_this_file: bool | None = None, + ) -> Result: + """ + Internal synchronous implementation of compile_ino_file. + + Args: + ino_path (str|Path): Path to the .ino file to compile + output_path (str|Path, optional): Output object file path. If None, uses temp file. + additional_flags (list, optional): Additional compiler flags + use_pch_for_this_file (bool, optional): Override PCH usage for this specific file. + If None, uses global PCH setting. If True/False, forces PCH on/off for this file. + + Returns: + Result: Result dataclass containing subprocess execution details + """ + ino_file_path = Path(ino_path).resolve() + + # Create output path if not provided + if output_path is None: + temp_file = tempfile.NamedTemporaryFile(suffix=".o", delete=False) + output_path = temp_file.name + temp_file.close() + cleanup_temp = True + else: + cleanup_temp = False + + # Build compiler command + # Handle cache-wrapped compilers (sccache/ccache) or ziglang c++ + if len(self.settings.compiler_args) > 0 and self.settings.compiler_args[ + 0:4 + ] == ["python", "-m", "ziglang", "c++"]: + # This is optimized ziglang c++ in compiler_args, use it directly + cmd = self.settings.compiler_args[ + 0:4 + ] # Use optimized ziglang c++ command from compiler_args + remaining_cache_args = self.settings.compiler_args[ + 4: + ] # Skip the ziglang c++ part + elif len(self.settings.compiler_args) > 0 and self.settings.compiler_args[ + 0:6 + ] == ["uv", "run", "python", "-m", "ziglang", "c++"]: + # This is legacy ziglang c++ in compiler_args, use it directly + cmd = self.settings.compiler_args[ + 0:6 + ] # Use ziglang c++ command from compiler_args + remaining_cache_args = self.settings.compiler_args[ + 6: + ] # Skip the ziglang c++ part + elif ( + len(self.settings.compiler_args) > 0 + and self.settings.compiler_args[0] == "clang++" + ): + # This is a clang++, replace with optimized ziglang c++ + cmd = ["python", "-m", "ziglang", "c++"] + remaining_cache_args = self.settings.compiler_args[1:] + else: + # Use ziglang c++ directly + cmd = ["python", "-m", "ziglang", "c++"] + remaining_cache_args = self.settings.compiler_args + + # Add standard clang arguments + cmd.extend( + [ + "-x", + "c++", # Force C++ compilation of .ino files + f"-std={self.settings.std_version}", + f"-I{self.settings.include_path}", # FastLED include path + ] + ) + + # Add defines if specified + if self.settings.defines: + for define in self.settings.defines: + cmd.append(f"-D{define}") + + # Add remaining compiler args (after skipping clang++ for cache-wrapped compilers) + cmd.extend(remaining_cache_args) + + # Determine whether to use PCH for this specific file + should_use_pch = ( + use_pch_for_this_file + if use_pch_for_this_file is not None + else self.settings.use_pch + ) + + # Add PCH flag if available and should be used for this file + if should_use_pch and self._pch_ready and self._pch_file_path: + cmd.extend(["-include-pch", str(self._pch_file_path)]) + + cmd.extend( + [ + "-c", + str(ino_file_path), # Compile .ino file directly + "-o", + str(output_path), + ] + ) + + if additional_flags: + cmd.extend(additional_flags) + + try: + # Optimize command to use sys.executable instead of shell 'python' resolution + python_exe = optimize_python_command(cmd) + + # Use Popen with stderr redirected to stdout for single stream pumping + process = subprocess.Popen( + python_exe, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, + bufsize=1, # Line buffered + encoding="utf-8", # Explicitly use UTF-8 + errors="replace", # Replace invalid chars instead of failing + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] # Keep separate for API compatibility + + # Read only stdout (which contains both stdout and stderr) + while True: + output_line = process.stdout.readline() if process.stdout else "" + + # Process output lines (both stdout and stderr combined) + if output_line: + line_stripped = output_line.rstrip() + # Add to both lists for API compatibility + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + + # Check if process has finished + if process.poll() is not None: + # Read any remaining output + remaining_output = process.stdout.read() if process.stdout else "" + + if remaining_output: + for line in remaining_output.splitlines(): + line_stripped = line.rstrip() + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + break + + # Clean up temp file on failure if we created it + if cleanup_temp and process.returncode != 0 and os.path.exists(output_path): + try: + os.unlink(output_path) + except: + pass + + return Result( + ok=(process.returncode == 0), + stdout="\n".join(stdout_lines), + stderr="\n".join(stderr_lines), + return_code=process.returncode, + ) + + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + if cleanup_temp and os.path.exists(output_path): + try: + os.unlink(output_path) + except Exception: + pass + + return Result(ok=False, stdout="", stderr=str(e), return_code=-1) + + def compile_cpp_file( + self, + cpp_path: str | Path, + output_path: str | Path | None = None, + additional_flags: list[str] | None = None, + use_pch_for_this_file: bool | None = None, + ) -> Future[Result]: + """ + Compile a .cpp file asynchronously. + + Args: + cpp_path (str|Path): Path to the .cpp file to compile + output_path (str|Path, optional): Output object file path. If None, uses temp file. + additional_flags (list, optional): Additional compiler flags + use_pch_for_this_file (bool, optional): Override PCH usage for this specific file. + If None, uses global PCH setting. If True/False, forces PCH on/off for this file. + + Returns: + Future[Result]: Future object that will contain the compilation result + """ + # Submit the compilation task to a thread pool + return _EXECUTOR.submit( + self._compile_cpp_file_sync, + cpp_path, + output_path, + additional_flags, + use_pch_for_this_file, + ) + + def _compile_cpp_file_sync( + self, + cpp_path: str | Path, + output_path: str | Path | None = None, + additional_flags: list[str] | None = None, + use_pch_for_this_file: bool | None = None, + ) -> Result: + """ + Internal synchronous implementation of compile_cpp_file. + + Args: + cpp_path (str|Path): Path to the .cpp file to compile + output_path (str|Path, optional): Output object file path. If None, uses temp file. + additional_flags (list, optional): Additional compiler flags + use_pch_for_this_file (bool, optional): Override PCH usage for this specific file. + If None, uses global PCH setting. If True/False, forces PCH on/off for this file. + + Returns: + Result: Result dataclass containing subprocess execution details + """ + cpp_file_path = Path(cpp_path).resolve() + + # Create output path if not provided + if output_path is None: + temp_file = tempfile.NamedTemporaryFile(suffix=".o", delete=False) + output_path = temp_file.name + temp_file.close() + cleanup_temp = True + else: + cleanup_temp = False + + # Build compiler command + # Handle cache-wrapped compilers (sccache/ccache) or ziglang c++ + if len(self.settings.compiler_args) > 0 and self.settings.compiler_args[ + 0:4 + ] == ["python", "-m", "ziglang", "c++"]: + # This is optimized ziglang c++ in compiler_args, use it directly + cmd = self.settings.compiler_args[ + 0:4 + ] # Use optimized ziglang c++ command from compiler_args + remaining_cache_args = self.settings.compiler_args[ + 4: + ] # Skip the ziglang c++ part + elif len(self.settings.compiler_args) > 0 and self.settings.compiler_args[ + 0:6 + ] == ["uv", "run", "python", "-m", "ziglang", "c++"]: + # This is legacy ziglang c++ in compiler_args, use it directly + cmd = self.settings.compiler_args[ + 0:6 + ] # Use ziglang c++ command from compiler_args + remaining_cache_args = self.settings.compiler_args[ + 6: + ] # Skip the ziglang c++ part + elif ( + len(self.settings.compiler_args) > 0 + and self.settings.compiler_args[0] == "clang++" + ): + # This is a clang++, replace with optimized ziglang c++ + cmd = ["python", "-m", "ziglang", "c++"] + remaining_cache_args = self.settings.compiler_args[1:] + else: + # Use ziglang c++ directly + cmd = ["python", "-m", "ziglang", "c++"] + remaining_cache_args = self.settings.compiler_args + + # Add standard clang arguments + cmd.extend( + [ + "-x", + "c++", # Force C++ compilation + f"-std={self.settings.std_version}", + f"-I{self.settings.include_path}", # FastLED include path + ] + ) + + # Add defines if specified + if self.settings.defines: + for define in self.settings.defines: + cmd.append(f"-D{define}") + + # Add remaining compiler args (after skipping clang++ for cache-wrapped compilers) + cmd.extend(remaining_cache_args) + + # Determine whether to use PCH for this specific file + should_use_pch = ( + use_pch_for_this_file + if use_pch_for_this_file is not None + else self.settings.use_pch + ) + + # Add PCH flag if available and should be used for this file + if should_use_pch and self._pch_ready and self._pch_file_path: + cmd.extend(["-include-pch", str(self._pch_file_path)]) + + cmd.extend( + [ + "-c", + str(cpp_file_path), # Compile the .cpp file directly + "-o", + str(output_path), + ] + ) + + if additional_flags: + cmd.extend(additional_flags) + + try: + # Optimize command to use sys.executable instead of shell 'python' resolution + python_exe = optimize_python_command(cmd) + + # Use Popen with stderr redirected to stdout for single stream pumping + process = subprocess.Popen( + python_exe, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, + bufsize=1, # Line buffered + encoding="utf-8", # Explicitly use UTF-8 + errors="replace", # Replace invalid chars instead of failing + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] # Keep separate for API compatibility + + # Read only stdout (which contains both stdout and stderr) + while True: + output_line = process.stdout.readline() if process.stdout else "" + + # Process output lines (both stdout and stderr combined) + if output_line: + line_stripped = output_line.rstrip() + print(output_line, end="", flush=True) # Stream to console + # Add to both lists for API compatibility + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + + # Check if process has finished + if process.poll() is not None: + # Read any remaining output + remaining_output = process.stdout.read() if process.stdout else "" + + if remaining_output: + print(remaining_output, end="", flush=True) + for line in remaining_output.splitlines(): + line_stripped = line.rstrip() + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + break + + # Clean up temp file on failure if we created it + if cleanup_temp and process.returncode != 0 and os.path.exists(output_path): + try: + os.unlink(output_path) + except: + pass + + return Result( + ok=(process.returncode == 0), + stdout="".join(stdout_lines), + stderr="".join(stderr_lines), + return_code=process.returncode, + ) + + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + if cleanup_temp and os.path.exists(output_path): + try: + os.unlink(output_path) + except Exception: + pass + + return Result(ok=False, stdout="", stderr=str(e), return_code=-1) + + def find_ino_files( + self, examples_dir: str | Path, filter_names: list[str] | None = None + ) -> list[Path]: + """ + Find all .ino files in the specified examples directory. + + Args: + examples_dir (str|Path): Directory containing .ino files + filter_names (list, optional): Only include files with these stem names (case-insensitive) + + Returns: + list[Path]: List of .ino file paths + """ + examples_dir = Path(examples_dir) + ino_files = list(examples_dir.rglob("*.ino")) + + if filter_names: + # Create case-insensitive mapping for better user experience + filter_lower = {name.lower() for name in filter_names} + ino_files = [f for f in ino_files if f.stem.lower() in filter_lower] + + return ino_files + + def find_cpp_files_for_example(self, ino_file: Path) -> list[Path]: + """ + Find all .cpp files in the same directory as the given .ino file and its subdirectories. + + Args: + ino_file (Path): Path to the .ino file + + Returns: + list[Path]: List of .cpp file paths in the example directory tree + """ + example_dir = ino_file.parent + # Search recursively for .cpp files in the example directory and all subdirectories + cpp_files = list(example_dir.rglob("*.cpp")) + return cpp_files + + def find_include_dirs_for_example(self, ino_file: Path) -> list[str]: + """ + Find all directories that should be added as include paths for the given example. + This includes the example root directory and any subdirectories that contain header files. + + Args: + ino_file (Path): Path to the .ino file + + Returns: + list[str]: List of directory paths that should be added as -I flags + """ + example_dir = ino_file.parent + include_dirs: list[str] = [] + + # Always include the example root directory + include_dirs.append(str(example_dir)) + + # Check for header files in subdirectories + for subdir in example_dir.iterdir(): + if subdir.is_dir() and subdir.name not in [ + ".git", + "__pycache__", + ".pio", + ".vscode", + ]: + # Check if this directory contains header files + header_files = [ + f + for f in subdir.rglob("*") + if f.is_file() and f.suffix in [".h", ".hpp"] + ] + if header_files: + include_dirs.append(str(subdir)) + + return include_dirs + + def test_clang_accessibility(self) -> bool: + """ + Comprehensive test that ziglang c++ is accessible and working properly. + This is the foundational test for the simple build system approach. + + Returns: + bool: True if all tests pass, False otherwise + """ + print("Testing ziglang c++ accessibility...") + + # Test 1: Check ziglang c++ version + version_result = self.check_clang_version() + if not version_result.success: + print(f"X ziglang c++ version check failed: {version_result.error}") + return False + print(f"[OK] ziglang c++ version: {version_result.version}") + + # Test 2: Simple compilation test with Blink example + print("Testing Blink.ino compilation...") + future_result = self.compile_ino_file("examples/Blink/Blink.ino") + result = future_result.result() # Wait for completion and get result + + if not result.ok: + print(f"X Blink compilation failed: {result.stderr}") + return False + + # Check if output file was created and has reasonable size + # Since we use temp file, we can't easily check size here + print(f"[OK] Blink.ino compilation: return code {result.return_code}") + + # Test 3: Complex example compilation test with DemoReel100 + print("Testing DemoReel100.ino compilation...") + future_result = self.compile_ino_file("examples/DemoReel100/DemoReel100.ino") + result = future_result.result() # Wait for completion and get result + + if not result.ok: + print(f"X DemoReel100 compilation failed: {result.stderr}") + return False + + print(f"[OK] DemoReel100.ino compilation: return code {result.return_code}") + + print("[INFO] All ziglang c++ accessibility tests passed!") + print("[OK] Implementation ready: Simple build system can proceed") + return True + + def _cleanup_file(self, file_path: str | Path) -> None: + """Clean up a file, ignoring errors.""" + try: + if os.path.exists(file_path): + os.unlink(file_path) + except: + pass + + def analyze_ino_for_pch_compatibility(self, ino_path: str | Path) -> bool: + """ + Analyze a .ino file to determine if it's compatible with PCH. + + PCH is incompatible if the file has any defines, includes, or other preprocessor + directives before including FastLED.h, as this can cause conflicts with the + precompiled header assumptions. + + Args: + ino_path (str|Path): Path to the .ino file to analyze + + Returns: + bool: True if compatible with PCH, False if PCH should be disabled + """ + try: + ino_file_path = Path(ino_path).resolve() + + with open(ino_file_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + lines = content.split("\n") + + for line in lines: + # Strip whitespace and comments for analysis + stripped = line.strip() + + # Skip empty lines + if not stripped: + continue + + # Skip single-line comments + if stripped.startswith("//"): + continue + + # Skip multi-line comment starts (basic detection) + if stripped.startswith("/*") and "*/" in stripped: + continue + + # Check for FastLED.h include (various formats) + if "#include" in stripped and ( + "FastLED.h" in stripped + or '"FastLED.h"' in stripped + or "" in stripped + ): + return True + + # Check for problematic constructs before FastLED.h + problematic_patterns = [ + stripped.startswith("#include"), # Any other include + stripped.startswith("#define"), # Any define + stripped.startswith("#ifdef"), # Conditional compilation + stripped.startswith("#ifndef"), # Conditional compilation + stripped.startswith("#if "), # Conditional compilation + stripped.startswith("#pragma"), # Pragma directives + stripped.startswith("using "), # Using declarations + stripped.startswith("namespace"), # Namespace declarations + "=" in stripped + and not stripped.startswith("//"), # Variable assignments + ] + + if any(problematic_patterns): + # Found problematic code before FastLED.h + return False + + # If we found FastLED.h and no problematic code before it, PCH is compatible + # If we didn't find FastLED.h at all, assume PCH is still ok (might be included indirectly) + return True + + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception: + # If we can't analyze the file, err on the side of caution and disable PCH + return False + + def compile_unity( + self, + compile_options: CompilerOptions, + list_of_cpp_files: Sequence[str | Path], + unity_output_path: str | Path | None = None, + ) -> Future[Result]: + """ + Compile multiple .cpp files as a unity build. + + Generates a single unity.cpp file that includes all specified .cpp files, + then compiles this single file for improved compilation speed and optimization. + + Args: + compile_options: Configuration for the compilation + list_of_cpp_files: List of .cpp file paths to include in unity build + unity_output_path: Custom unity.cpp output path. If None, uses temp file. + + Returns: + Future[Result]: Future that will contain compilation result + """ + return _EXECUTOR.submit( + self._compile_unity_sync, + compile_options, + list_of_cpp_files, + unity_output_path, + ) + + def compile_unity_chunks( + self, + compile_options: CompilerOptions, + list_of_cpp_files: list[str | Path], + chunks: int, + unity_dir: str | Path | None = None, + no_parallel: bool = False, + ) -> Future[UnityChunksResult]: + """Compile a chunked UNITY build producing N unity{n}.o objects. + + Args: + compile_options: Compilation options (PCH is forced off for UNITY) + list_of_cpp_files: Input .cpp files (will be sorted deterministically) + chunks: Number of chunks (>=1). Capped to number of files. + unity_dir: Directory to write unity{n}.cpp/.o (temp dir if None) + no_parallel: If True, compile sequentially + + Returns: + Future[UnityChunksResult] + """ + return _EXECUTOR.submit( + self._compile_unity_chunks_sync, + compile_options, + list_of_cpp_files, + chunks, + unity_dir, + no_parallel, + ) + + def _compile_unity_sync( + self, + compile_options: CompilerOptions, + list_of_cpp_files: Sequence[str | Path], + unity_output_path: str | Path | None = None, + ) -> Result: + """ + Synchronous implementation of unity compilation. + + Args: + compile_options: Configuration for the compilation + list_of_cpp_files: List of .cpp file paths to include in unity build + unity_output_path: Custom unity.cpp output path. If None, uses temp file. + + Returns: + Result: Success status and command output + """ + if not list_of_cpp_files: + return Result( + ok=False, + stdout="", + stderr="No .cpp files provided for unity compilation", + return_code=1, + ) + + # Convert to Path objects and validate all files exist + cpp_paths: list[Path] = [] + for cpp_file in list_of_cpp_files: + cpp_path = Path(cpp_file).resolve() + if not cpp_path.exists(): + return Result( + ok=False, + stdout="", + stderr=f"Source file not found: {cpp_path}", + return_code=1, + ) + if not cpp_path.suffix == ".cpp": + return Result( + ok=False, + stdout="", + stderr=f"File is not a .cpp file: {cpp_path}", + return_code=1, + ) + cpp_paths.append(cpp_path) + + # Determine unity.cpp output path + if unity_output_path: + unity_path = Path(unity_output_path).resolve() + cleanup_unity = False + else: + # Create temporary unity.cpp file + temp_dir = compile_options.temp_dir or tempfile.gettempdir() + temp_file = tempfile.NamedTemporaryFile( + suffix=".cpp", prefix="unity_", dir=temp_dir, delete=False + ) + unity_path = Path(temp_file.name) + temp_file.close() + cleanup_unity = True + + # Generate unity.cpp content + try: + unity_content = self._generate_unity_content(cpp_paths) + + # Check if file already exists with identical content to avoid unnecessary modifications + should_write = True + if unity_path.exists(): + try: + existing_content = unity_path.read_text(encoding="utf-8") + if existing_content == unity_content: + should_write = False + # File already has identical content, skip writing + except (IOError, UnicodeDecodeError): + # If we can't read the existing file, proceed with writing + should_write = True + + if should_write: + unity_path.write_text(unity_content, encoding="utf-8") + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + if cleanup_unity and unity_path.exists(): + try: + unity_path.unlink() + except Exception: + warnings.warn(f"Failed to unlink unity.cpp: {e}") + pass + return Result( + ok=False, + stdout="", + stderr=f"Failed to generate unity.cpp: {e}", + return_code=1, + ) + + # Determine output object file path + if cleanup_unity: + # If unity file is temporary, create temporary object file too + temp_obj_file = tempfile.NamedTemporaryFile(suffix=".o", delete=False) + output_path = temp_obj_file.name + temp_obj_file.close() + cleanup_obj = True + else: + # If unity file is permanent, create object file alongside it + output_path = unity_path.with_suffix(".o") + cleanup_obj = False + + try: + # Compile the unity.cpp file + result = self._compile_cpp_file_sync( + unity_path, + output_path, + compile_options.additional_flags, + compile_options.use_pch, + ) + + # Clean up temporary files on failure + if not result.ok: + if cleanup_unity and unity_path.exists(): + try: + unity_path.unlink() + except: + pass + if cleanup_obj and output_path and os.path.exists(output_path): + try: + os.unlink(output_path) + except: + pass + + return result + + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + # Clean up temporary files on exception + if cleanup_unity and unity_path.exists(): + try: + unity_path.unlink() + except Exception: + pass + if cleanup_obj and output_path and os.path.exists(output_path): + try: + os.unlink(output_path) + except Exception: + pass + + return Result( + ok=False, + stdout="", + stderr=f"Unity compilation failed: {e}", + return_code=-1, + ) + + def _compile_unity_chunks_sync( + self, + compile_options: CompilerOptions, + list_of_cpp_files: Sequence[str | Path], + chunks: int, + unity_dir: str | Path | None = None, + no_parallel: bool = False, + ) -> UnityChunksResult: + if not list_of_cpp_files: + return UnityChunksResult(success=False, chunks=[]) + + # Normalize and validate inputs + cpp_paths: list[Path] = [] + for cpp_file in list_of_cpp_files: + p = Path(cpp_file).resolve() + if not p.exists() or p.suffix != ".cpp": + return UnityChunksResult(success=False, chunks=[]) + cpp_paths.append(p) + + # Deterministic ordering + project_root = Path.cwd() + + def sort_key(p: Path) -> str: + try: + rel = p.relative_to(project_root) + except Exception: + rel = p + return rel.as_posix() + + cpp_paths = sorted(cpp_paths, key=sort_key) + + total = len(cpp_paths) + chunks = max(1, min(chunks, total)) + base = total // chunks + rem = total % chunks + + # Resolve unity dir + cleanup_dir = False + if unity_dir is None: + temp_dir = compile_options.temp_dir or tempfile.gettempdir() + unity_root = Path(temp_dir) / f"fastled_unity_{os.getpid()}" + unity_root.mkdir(parents=True, exist_ok=True) + cleanup_dir = True + else: + unity_root = Path(unity_dir) + unity_root.mkdir(parents=True, exist_ok=True) + + # Build per-chunk unity files + chunk_results: list[UnityChunkResult] = [] + compile_futures: list[tuple[int, Future[Result], Path, Path]] = [] + + start = 0 + for i in range(chunks): + size = base + (1 if i < rem else 0) + end = start + size + group = cpp_paths[start:end] + start = end + + unity_cpp = unity_root / f"unity{i + 1}.cpp" + + # Generate content (idempotent write if content unchanged) + unity_content = self._generate_unity_content(group) + try: + should_write = True + if unity_cpp.exists(): + try: + if unity_cpp.read_text(encoding="utf-8") == unity_content: + should_write = False + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception: + # If we cannot read for comparison, fall back to writing + should_write = True + if should_write: + unity_cpp.write_text(unity_content, encoding="utf-8") + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + if cleanup_dir: + try: + unity_cpp.unlink(missing_ok=True) + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception: + pass + return UnityChunksResult(success=False, chunks=[]) + + obj_path = unity_cpp.with_suffix(".o") + + # Force unity builds to not use PCH + additional_flags = list(compile_options.additional_flags or []) + if "-c" not in additional_flags: + additional_flags.insert(0, "-c") + per_file_options = CompilerOptions( + include_path=compile_options.include_path, + compiler=compile_options.compiler, + defines=compile_options.defines, + std_version=compile_options.std_version, + compiler_args=compile_options.compiler_args, + use_pch=False, + additional_flags=additional_flags, + parallel=compile_options.parallel, + temp_dir=compile_options.temp_dir, + archiver=compile_options.archiver, + archiver_args=compile_options.archiver_args, + pch_header_content=compile_options.pch_header_content, + pch_output_path=compile_options.pch_output_path, + ) + + if no_parallel: + res = self._compile_cpp_file_sync( + unity_cpp, obj_path, per_file_options.additional_flags, False + ) + chunk_results.append( + UnityChunkResult( + ok=res.ok, + unity_cpp=unity_cpp, + object_path=obj_path, + stderr=res.stderr, + return_code=res.return_code, + ) + ) + if not res.ok and cleanup_dir: + try: + unity_cpp.unlink(missing_ok=True) + obj_path.unlink(missing_ok=True) + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception: + pass + return UnityChunksResult(success=False, chunks=chunk_results) + else: + fut = _EXECUTOR.submit( + self._compile_cpp_file_sync, + unity_cpp, + obj_path, + per_file_options.additional_flags, + False, + ) + compile_futures.append((i, fut, unity_cpp, obj_path)) + + if not no_parallel: + for i, fut, unity_cpp, obj_path in compile_futures: + res = fut.result() + chunk_results.append( + UnityChunkResult( + ok=res.ok, + unity_cpp=unity_cpp, + object_path=obj_path, + stderr=res.stderr, + return_code=res.return_code, + ) + ) + if not res.ok and cleanup_dir: + try: + unity_cpp.unlink(missing_ok=True) + obj_path.unlink(missing_ok=True) + except Exception: + pass + + success = all(c.ok for c in chunk_results) and len(chunk_results) == chunks + # Note: we do not remove files on success; caller may need them for archiving + return UnityChunksResult(success=success, chunks=chunk_results) + + def _generate_unity_content(self, cpp_paths: list[Path]) -> str: + """ + Generate the content for unity.cpp file. + + Args: + cpp_paths: List of .cpp file paths to include + + Returns: + str: Content for unity.cpp file + """ + lines = [ + "// Unity build file generated by FastLED CI system", + "// This file includes all source .cpp files for unified compilation", + "//", + f"// Generated for {len(cpp_paths)} source files:", + ] + + # Add comment listing all included files + for cpp_path in cpp_paths: + lines.append(f"// - {cpp_path}") + + lines.extend( + [ + "//", + "", + "// Include all source files", + ] + ) + + # Add #include statements for all .cpp files + for cpp_path in cpp_paths: + # Use absolute paths to avoid include path issues + lines.append(f'#include "{cpp_path}"') + + lines.append("") # Final newline + + return "\n".join(lines) + + def create_archive( + self, + object_files: list[Path], + output_archive: Path, + options: LibarchiveOptions = LibarchiveOptions(), + ) -> Future[Result]: + """ + Create static library archive from object files. + Submits archive creation to thread pool for parallel execution. + + Args: + object_files: List of .o files to include in archive + output_archive: Output .a file path + options: Archive generation options + + Returns: + Future[Result]: Future object that will contain the archive creation result + """ + return _EXECUTOR.submit( + self.create_archive_sync, object_files, output_archive, options + ) + + def create_archive_sync( + self, object_files: list[Path], output_archive: Path, options: LibarchiveOptions + ) -> Result: + """ + Synchronous archive creation implementation. + + Args: + object_files: List of .o files to include in archive + output_archive: Output .a file path + options: Archive generation options + + Returns: + Result: Success status and command output + """ + return create_archive_sync( + object_files, + output_archive, + options, + self.settings.archiver, + self.build_flags, + ) + + def link_program(self, link_options: LinkOptions) -> Future[Result]: + """ + Link object files and libraries into an executable program. + + Args: + link_options: Configuration for the linking operation + + Returns: + Future[Result]: Future that will contain linking result + """ + return _EXECUTOR.submit(self._link_program_sync, link_options) + + def _link_program_sync(self, link_options: LinkOptions) -> Result: + """ + Synchronous implementation of program linking. + + Args: + link_options: Configuration for the linking operation + + Returns: + Result: Success status and command output + """ + return link_program_sync(link_options, self.build_flags) + + def detect_linker(self) -> str: + """ + Detect the best available linker for the current platform. + Preference order varies by platform: + - Windows: lld-link.exe > link.exe > ld.exe + - Linux: mold > lld > gold > ld + - macOS: ld64.lld > ld + + Returns: + str: Path to the detected linker + + Raises: + RuntimeError: If no suitable linker is found + """ + system = platform.system() + + if system == "Windows": + # Windows linker priority + for linker in [ + "lld-link.exe", + "lld-link", + "link.exe", + "link", + "ld.exe", + "ld", + ]: + linker_path = shutil.which(linker) + if linker_path: + return linker_path + raise RuntimeError("No linker found (lld-link, link, or ld required)") + + elif system == "Linux": + # Linux linker priority + for linker in ["mold", "lld", "gold", "ld"]: + linker_path = shutil.which(linker) + if linker_path: + return linker_path + raise RuntimeError("No linker found (mold, lld, gold, or ld required)") + + elif system == "Darwin": # macOS + # macOS linker priority + for linker in ["ld64.lld", "ld"]: + linker_path = shutil.which(linker) + if linker_path: + return linker_path + raise RuntimeError("No linker found (ld64.lld or ld required)") + + else: + # Default fallback for other platforms + linker_path = shutil.which("ld") + if linker_path: + return linker_path + raise RuntimeError(f"No linker found for platform {system}") + + +def get_configured_linker_command(build_flags_config: BuildFlags) -> list[str]: + """Get linker command from build configuration TOML.""" + if build_flags_config.tools.linker: + return build_flags_config.tools.linker + assert False, "FATAL ERROR: linker is not set in build_flags.toml" + + +def link_program_sync( + link_options: LinkOptions, build_flags_config: BuildFlags +) -> Result: + """ + Link object files and libraries into an executable program. + + Args: + link_options: Configuration for the linking operation + + Returns: + Result: Success status and command output + """ + if not link_options.object_files: + return Result( + ok=False, + stdout="", + stderr="No object files provided for linking", + return_code=1, + ) + + # Validate all object files exist + for obj_file in link_options.object_files: + obj_path = Path(obj_file) + if not obj_path.exists(): + return Result( + ok=False, + stdout="", + stderr=f"Object file not found: {obj_path}", + return_code=1, + ) + + # Validate all static libraries exist + for lib_file in link_options.static_libraries: + lib_path = Path(lib_file) + if not lib_path.exists(): + return Result( + ok=False, + stdout="", + stderr=f"Static library not found: {lib_path}", + return_code=1, + ) + + # Auto-detect linker if not provided + linker = link_options.linker + system = platform.system() + + # Check if we're using GNU-style arguments (indicates Zig toolchain) + using_gnu_style = any( + arg.startswith("-") and not arg.startswith("-Wl,") + for arg in link_options.linker_args + ) + + # Build linker command - prefer configuration over detection + cmd: list[str] = [] + + if linker is None: + if build_flags_config: + # Use configured toolchain for linking when available + configured_cmd = get_configured_linker_command(build_flags_config) + if configured_cmd: + cmd = configured_cmd[:] # Copy the configured command + else: + # Fallback to detection if no configuration available + try: + linker = detect_linker() + cmd = [linker] + except RuntimeError as e: + return Result(ok=False, stdout="", stderr=str(e), return_code=1) + elif system == "Windows" and using_gnu_style: + # Legacy fallback: Use Zig toolchain for linking when GNU-style args are provided + cmd = ["python", "-m", "ziglang", "c++"] + else: + try: + linker = detect_linker() + cmd = [linker] + except RuntimeError as e: + return Result(ok=False, stdout="", stderr=str(e), return_code=1) + else: + # linker was provided explicitly + if isinstance(linker, str): + # For Windows paths, don't use shlex.split() as it mangles backslashes + # The linker variable should be a single executable path, not a command with args + cmd = [linker] + else: + # linker should be str at this point + cmd = [str(linker)] + + # Add platform-specific output flag first + output_path = Path(link_options.output_executable) + + if system == "Windows" and using_gnu_style: + # Zig toolchain or GNU-style linker on Windows + cmd.extend(["-o", str(output_path)]) + elif system == "Windows" and linker and ("lld-link" in linker or "link" in linker): + # Windows (lld-link/link) style + cmd.append(f"/OUT:{output_path}") + else: + # Unix-style (ld/mold/gold/etc.) + cmd.extend(["-o", str(output_path)]) + + # Add object files + cmd.extend(str(obj) for obj in link_options.object_files) + + # Add static libraries + cmd.extend(str(lib) for lib in link_options.static_libraries) + + # Add custom linker arguments + cmd.extend(link_options.linker_args) + + # Ensure output directory exists + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Execute linker command + try: + # Optimize command to use sys.executable instead of shell 'python' resolution + python_exe = optimize_python_command(cmd) + + # Use Popen with stderr redirected to stdout for single stream pumping + process = subprocess.Popen( + python_exe, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, + bufsize=1, # Line buffered + encoding="utf-8", # Explicitly use UTF-8 + errors="replace", # Replace invalid chars instead of failing + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] # Keep separate for API compatibility + + # Read only stdout (which contains both stdout and stderr) + while True: + output_line = process.stdout.readline() if process.stdout else "" + + # Process output lines (both stdout and stderr combined) + if output_line: + line_stripped = output_line.rstrip() + print(output_line, end="", flush=True) # Stream to console + # Add to both lists for API compatibility + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + + # Check if process has finished + if process.poll() is not None: + # Read any remaining output + remaining_output = process.stdout.read() if process.stdout else "" + + if remaining_output: + print(remaining_output, end="", flush=True) + for line in remaining_output.splitlines(): + line_stripped = line.rstrip() + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + break + + return Result( + ok=process.returncode == 0, + stdout="".join(stdout_lines), + stderr="".join(stderr_lines), + return_code=process.returncode, + ) + + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + # Check if output executable was created successfully despite the exception + output_path = Path(link_options.output_executable) + if output_path.exists() and output_path.stat().st_size > 0: + # Linking succeeded - executable was created successfully + # The exception was likely during output reading or cleanup, not linking + return Result( + ok=True, + stdout="", + stderr=f"Linking succeeded with minor exception during cleanup: {type(e).__name__}: {e}", + return_code=0, + ) + + # Create verbose error message with full command and exception details + cmd_str = subprocess.list2cmdline(str(arg) for arg in cmd) + verbose_error = ( + f"Linker command failed with exception: {type(e).__name__}: {e}\n" + f"Failed command: {cmd_str}\n" + f"Working directory: {Path.cwd()}\n" + f"Output executable: {link_options.output_executable}\n" + f"Object files: {link_options.object_files}\n" + f"Static libraries: {link_options.static_libraries}\n" + f"Linker args: {link_options.linker_args}" + ) + return Result(ok=False, stdout="", stderr=verbose_error, return_code=-1) + + +# Helper functions for common linker argument patterns + + +# NOTE: Linker flag functions removed - all linker configuration now comes from build configuration TOML +# Use BuildFlags.parse() to load linker flags from TOML configuration instead + + +# Convenience functions for backward compatibility + + +# Default settings for backward compatibility - NO FALLBACKS POLICY +def _get_default_settings() -> CompilerOptions: + """Get default compiler settings with MANDATORY configuration-based stub platform defines.""" + + from ci.compiler.test_example_compilation import ( + extract_stub_platform_defines_from_toml, + extract_stub_platform_include_paths_from_toml, + load_build_flags_toml, + ) + + # Add the ci/compiler directory to path for imports + ci_dir = _HERE.parent + + # MANDATORY: Load build_unit.toml configuration - NO fallbacks allowed + toml_path = ci_dir / "build_unit.toml" + if not toml_path.exists(): + raise RuntimeError( + f"CRITICAL: build_unit.toml not found at {toml_path}. " + f"This file is MANDATORY for all compiler operations." + ) + + # Load configuration with strict error handling + build_config = load_build_flags_toml(str(toml_path)) + stub_defines = extract_stub_platform_defines_from_toml(build_config) + stub_include_paths = extract_stub_platform_include_paths_from_toml(build_config) + + # Convert relative include paths to absolute and create compiler args + compiler_args: list[str] = [] + for include_path in stub_include_paths: + if os.path.isabs(include_path): + compiler_args.append(f"-I{include_path}") + else: + # Convert relative path to absolute from project root + abs_path = str(ci_dir.parent / include_path) + compiler_args.append(f"-I{abs_path}") + + return CompilerOptions( + include_path="./src", + defines=stub_defines, + std_version="c++17", + compiler_args=compiler_args, # Include paths from configuration + ) + + +def _get_default_build_flags() -> BuildFlags: + """Get default BuildFlags with MANDATORY configuration loading - NO FALLBACKS!""" + from pathlib import Path + + # MANDATORY: Load build_unit.toml configuration - NO fallbacks allowed + current_dir = Path(__file__).parent + ci_dir = current_dir.parent + toml_path = ci_dir / "build_unit.toml" + + if not toml_path.exists(): + raise RuntimeError( + f"CRITICAL: build_unit.toml not found at {toml_path}. " + f"This file is MANDATORY for all compiler operations." + ) + + # Use BuildFlags.parse() method for proper type construction + return BuildFlags.parse(toml_path) + + +# Shared compiler instance for backward compatibility functions +_default_compiler = None + + +def _get_default_compiler() -> Compiler: + """Get or create a shared compiler instance with default settings.""" + global _default_compiler + if _default_compiler is None: + # STRICT: Both CompilerOptions AND BuildFlags are REQUIRED - NO DEFAULTS! + build_flags = _get_default_build_flags() + default_settings = _get_default_settings() + _default_compiler = Compiler(default_settings, build_flags) + return _default_compiler + + +def check_clang_version() -> tuple[bool, str, str]: + """Backward compatibility wrapper.""" + compiler = _get_default_compiler() + result = compiler.check_clang_version() + return result.success, result.version, result.error + + +def compile_ino_file( + ino_path: str | Path, + output_path: str | Path | None = None, + additional_flags: list[str] | None = None, +) -> Result: + """Backward compatibility wrapper.""" + compiler = _get_default_compiler() + future_result = compiler.compile_ino_file(ino_path, output_path, additional_flags) + return future_result.result() # Wait for completion and return result + + +def test_clang_accessibility() -> bool: + """Backward compatibility wrapper.""" + compiler = _get_default_compiler() + return compiler.test_clang_accessibility() + + +def find_ino_files( + examples_dir: str = "examples", filter_names: list[str] | None = None +) -> list[Path]: + """Backward compatibility wrapper.""" + compiler = _get_default_compiler() + return compiler.find_ino_files(examples_dir, filter_names=filter_names) + + +def get_fastled_compile_command( + ino_file: str | Path, output_file: str | Path +) -> list[str]: + """Backward compatibility wrapper - DEPRECATED: Use FastLEDClangCompiler.compile() instead.""" + compiler = _get_default_compiler() + # This is a bit hacky since we removed get_compile_command, but needed for backward compatibility + settings = compiler.settings + cmd = [ + settings.compiler, + "-x", + "c++", + f"-std={settings.std_version}", + f"-I{settings.include_path}", + ] + + # Add defines if specified + if settings.defines: + for define in settings.defines: + cmd.append(f"-D{define}") + cmd.extend(settings.compiler_args) + cmd.extend( + [ + "-c", + str(ino_file), + "-o", + str(output_file), + ] + ) + return cmd + + +def main() -> bool: + """Test all functions in the module using the class-based approach.""" + print("=== Testing Compiler class ===") + + # Create compiler instance with custom settings + settings = CompilerOptions( + include_path="./src", defines=["STUB_PLATFORM"], std_version="c++17" + ) + # STRICT: Load BuildFlags explicitly - NO defaults allowed + build_flags = _get_default_build_flags() + compiler = Compiler(settings, build_flags) + + # Test version check + version_result = compiler.check_clang_version() + print( + f"Version check: success={version_result.success}, version={version_result.version}" + ) + if not version_result.success: + print(f"Error: {version_result.error}") + return False + + # Test finding ino files + ino_files = compiler.find_ino_files( + "examples", filter_names=["Blink", "DemoReel100"] + ) + print(f"Found {len(ino_files)} ino files: {[f.name for f in ino_files]}") + + if ino_files: + # Test compile_ino_file function + future_result = compiler.compile_ino_file(ino_files[0]) + result = future_result.result() # Wait for completion and get result + print( + f"Compile {ino_files[0].name}: success={result.ok}, return_code={result.return_code}" + ) + if not result.ok: + print(f"Error: {result.stderr}") + else: + print("[OK] Compilation successful") + + # Test compile_ino_file function with Result dataclass + future_result_dc = compiler.compile_ino_file(ino_files[0]) + result_dc = future_result_dc.result() # Wait for completion and get result + print( + f"compile_ino_file {ino_files[0].name}: ok={result_dc.ok}, return_code={result_dc.return_code}" + ) + + # Test compiler_args + print("\n=== Testing compiler_args ===") + settings_with_args = CompilerOptions( + include_path="./src", + defines=["STUB_PLATFORM"], + std_version="c++17", + compiler_args=["-Werror", "-Wall"], + ) + # STRICT: Load BuildFlags explicitly - NO defaults allowed + build_flags_with_args = _get_default_build_flags() + compiler_with_args = Compiler(settings_with_args, build_flags_with_args) + if ino_files: + # Test that compiler_args are included in the command + with tempfile.NamedTemporaryFile(suffix=".o", delete=False) as temp_file: + temp_output = temp_file.name + + # Get the command that would be executed + cmd = get_fastled_compile_command(ino_files[0], temp_output) + print(f"Command with compiler_args: {subprocess.list2cmdline(cmd)}") + + if "-Werror" not in cmd or "-Wall" not in cmd: + print("X compiler_args not found in command") + return False + print("[OK] compiler_args correctly passed to compile command.") + + # Test actual compilation + future_result = compiler_with_args.compile_ino_file(ino_files[0]) + result = future_result.result() # Wait for completion and get result + print(f"Compile {ino_files[0].name} with extra args: success={result.ok}") + if not result.ok: + print(f"Error: {result.stderr}") + + # Clean up + if os.path.exists(temp_output): + os.unlink(temp_output) + + # Test full accessibility test + print("\n=== Running full accessibility test ===") + result = compiler.test_clang_accessibility() + + # Test backward compatibility functions + print("\n=== Testing backward compatibility functions ===") + success_compat, version_compat, _ = check_clang_version() + print( + f"Backward compatibility version check: success={success_compat}, version={version_compat}" + ) + + return result and success_compat + + +def detect_linker() -> str: + """ + Detect the best available linker for the current platform. + Preference order varies by platform: + - Windows: lld-link.exe > link.exe > ld.exe + - Linux: mold > lld > gold > ld + - macOS: ld64.lld > ld + + Returns: + str: Path to the detected linker + + Raises: + RuntimeError: If no suitable linker is found + """ + system = platform.system() + + if system == "Windows": + # Windows linker priority + for linker in ["lld-link.exe", "lld-link", "link.exe", "link", "ld.exe", "ld"]: + linker_path = shutil.which(linker) + if linker_path: + return linker_path + raise RuntimeError("No linker found (lld-link, link, or ld required)") + + elif system == "Linux": + # Linux linker priority + for linker in ["mold", "lld", "gold", "ld"]: + linker_path = shutil.which(linker) + if linker_path: + return linker_path + raise RuntimeError("No linker found (mold, lld, gold, or ld required)") + + elif system == "Darwin": # macOS + # macOS linker priority + for linker in ["ld64.lld", "ld"]: + linker_path = shutil.which(linker) + if linker_path: + return linker_path + raise RuntimeError("No linker found (ld64.lld or ld required)") + + else: + # Default fallback for other platforms + linker_path = shutil.which("ld") + if linker_path: + return linker_path + raise RuntimeError(f"No linker found for platform {system}") + + +def get_configured_archiver_command(build_flags_config: BuildFlags) -> list[str] | None: + """Get archiver command from build configuration TOML.""" + if build_flags_config.tools.archiver: + return build_flags_config.tools.archiver + return None + + +# detect_archiver function REMOVED - auto-detection from environment is FORBIDDEN +# All tools must be explicitly configured in TOML build configuration + + +def create_archive_sync( + object_files: list[Path], + output_archive: Path, + options: LibarchiveOptions = LibarchiveOptions(), + archiver: str | None = None, + build_flags_config: BuildFlags | None = None, +) -> Result: + """ + Create a static library archive (.a) from compiled object files. + + Args: + object_files: List of .o files to include in archive + output_archive: Output .a file path + options: Archive generation options + archiver: Archiver command from TOML configuration (REQUIRED - no auto-detection) + build_flags_config: Build configuration with archive flags (REQUIRED) + + Returns: + Result: Success status and command output + """ + if not object_files: + return Result( + ok=False, + stdout="", + stderr="No object files provided for archive creation", + return_code=1, + ) + + # Validate all object files exist + for obj_file in object_files: + if not obj_file.exists(): + return Result( + ok=False, + stdout="", + stderr=f"Object file not found: {obj_file}", + return_code=1, + ) + + # Archiver MUST be provided from TOML configuration - NO AUTO-DETECTION + if archiver is None: + return Result( + ok=False, + stdout="", + stderr="CRITICAL: No archiver provided to create_archive_sync(). " + "Archiver must be configured in [tools] section of build TOML file. " + "Auto-detection from environment is NOT ALLOWED.", + return_code=1, + ) + + # Build archive command - STRICT configuration enforcement + cmd: list[str] = [] + if build_flags_config: + configured_cmd = get_configured_archiver_command(build_flags_config) + if configured_cmd: + # Use configured command - verify it matches what was passed + configured_str = subprocess.list2cmdline(configured_cmd) + if archiver and archiver != configured_str: + print(f"[ARCHIVE WARNING] Archiver mismatch detected:") + print(f" Passed archiver: '{archiver}'") + print(f" Configured archiver: '{configured_str}'") + print(f" Using configured archiver (takes precedence)") + + cmd = configured_cmd[:] + print( + f"[ARCHIVE] Using configured archiver command: {subprocess.list2cmdline(cmd)}" + ) + else: + raise RuntimeError( + f"CRITICAL: No archiver configured in build_flags_config.tools.archiver. " + f"Archiver fallbacks are NOT ALLOWED. " + f"Please configure archiver in [tools] section of your build TOML file." + ) + else: + raise RuntimeError( + f"CRITICAL: No build_flags_config provided to create_archive_sync(). " + f"Archive creation requires build configuration. " + f"System tool fallbacks are NOT ALLOWED to prevent silent configuration errors." + ) + + # Archive flags: MUST come from build configuration TOML - NO DEFAULTS + if ( + not build_flags_config + or not hasattr(build_flags_config, "archive") + or not hasattr(build_flags_config.archive, "flags") + ): + raise RuntimeError( + "CRITICAL: Archive flags not found in build configuration TOML. " + "Please ensure [archive] section with 'flags' is configured." + ) + + # Get platform-specific archive flags if available + import platform + + system = platform.system() + flags = build_flags_config.archive.flags # Default flags + + # Check for platform-specific archive flags + if system == "Darwin": # macOS + if ( + build_flags_config.archive.darwin + and build_flags_config.archive.darwin.flags + ): + flags = build_flags_config.archive.darwin.flags + print(f"[ARCHIVE] Using macOS-specific flags: {flags}") + elif system == "Linux": + if build_flags_config.archive.linux and build_flags_config.archive.linux.flags: + flags = build_flags_config.archive.linux.flags + print(f"[ARCHIVE] Using Linux-specific flags: {flags}") + elif system == "Windows": + if ( + build_flags_config.archive.windows + and build_flags_config.archive.windows.flags + ): + flags = build_flags_config.archive.windows.flags + print(f"[ARCHIVE] Using Windows-specific flags: {flags}") + + print(f"[ARCHIVE] Final flags selected: {flags}") + + if options.use_thin: + flags += "T" # Add thin archive flag + + cmd.append(flags) + cmd.append(str(output_archive)) + cmd.extend(str(obj) for obj in object_files) + + # Ensure output directory exists + output_archive.parent.mkdir(parents=True, exist_ok=True) + + # Debug output for archiver command (helpful for diagnosing fallback issues) + print(f"[ARCHIVE] Using archiver command: {subprocess.list2cmdline(cmd)}") + print(f"[ARCHIVE] Archive flags: {flags}") + print(f"[ARCHIVE] Platform: {system}") + + # Execute archive command with single stream to prevent buffer overflow + try: + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, + bufsize=1, + encoding="utf-8", + errors="replace", + cwd=None, + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] # Keep separate for API compatibility + + # Read only stdout (which contains both stdout and stderr) + while True: + output_line = process.stdout.readline() if process.stdout else "" + + if output_line: + line_stripped = output_line.rstrip() + # Add to both lists for API compatibility + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + + if process.poll() is not None: + # Read remaining output + remaining_output = process.stdout.read() if process.stdout else "" + + if remaining_output: + for line in remaining_output.splitlines(): + line_stripped = line.rstrip() + stdout_lines.append(line_stripped) + stderr_lines.append(line_stripped) + break + + return Result( + ok=process.returncode == 0, + stdout="\n".join(stdout_lines), + stderr="\n".join(stderr_lines), + return_code=process.returncode, + ) + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception as e: + error_msg = f"Archive command failed: {e}\nCommand was: {subprocess.list2cmdline(cmd)}\nThis may indicate that the configured archiver is not available." + print(f"[ARCHIVE ERROR] {error_msg}") + return Result(ok=False, stdout="", stderr=error_msg, return_code=-1) + + +def load_build_flags_from_toml( + toml_path: Path, quick_build: bool = False, strict_mode: bool = False +) -> BuildFlags: + """ + Load build flags from TOML configuration file. + + This is a convenience function that delegates to BuildFlags.parse(). + + Args: + toml_path: Path to the build configuration TOML file (build_unit.toml or build_example.toml) + quick_build: Use quick build mode flags (default: debug mode) + strict_mode: Enable strict mode warning flags + + Returns: + BuildFlags: Parsed build flags from TOML file + """ + return BuildFlags.parse(toml_path, quick_build, strict_mode) + + +def create_compiler_options_from_toml( + toml_path: Path, + include_path: str, + quick_build: bool = False, + strict_mode: bool = False, + additional_defines: List[str] | None = None, + additional_compiler_args: List[str] | None = None, + **kwargs: Any, +) -> CompilerOptions: + """ + Create CompilerOptions from TOML build flags configuration. + + Args: + toml_path: Path to the build configuration TOML file (build_unit.toml or build_example.toml) + include_path: Base include path for compilation + quick_build: Use quick build mode flags (default: debug mode) + strict_mode: Enable strict mode warning flags + additional_defines: Extra defines to add beyond TOML + additional_compiler_args: Extra compiler args to add beyond TOML + **kwargs: Additional CompilerOptions parameters + + Returns: + CompilerOptions: Configured compiler options with TOML flags + """ + # Load build flags from TOML + build_flags = BuildFlags.parse(toml_path, quick_build, strict_mode) + + # Create compiler args from TOML flags + compiler_args: List[str] = [] + compiler_args.extend(build_flags.compiler_flags) + compiler_args.extend(build_flags.include_flags) + + # Add strict mode flags if enabled + if strict_mode: + compiler_args.extend(build_flags.strict_mode_flags) + + # Add additional compiler args if provided + if additional_compiler_args: + compiler_args.extend(additional_compiler_args) + + # Extract defines without the "-D" prefix for CompilerOptions + defines: List[str] = [] + for define in build_flags.defines: + if define.startswith("-D"): + defines.append(define[2:]) # Remove "-D" prefix + else: + defines.append(define) + + # Add additional defines if provided + if additional_defines: + defines.extend(additional_defines) + + # Remove conflicting tool parameters from kwargs (TOML tools take precedence) + filtered_kwargs = { + k: v for k, v in kwargs.items() if k not in ["compiler", "archiver"] + } + + # STRICT: Compiler must be configured in TOML - NO DEFAULTS + if not build_flags.tools.cpp_compiler: + raise RuntimeError( + f"CRITICAL: No compiler configured in [tools] section of {toml_path}. " + f'Please add: cpp_compiler = ["uv", "run", "python", "-m", "ziglang", "c++"] ' + f"or similar compiler configuration. " + f"Default tool fallbacks are NOT ALLOWED." + ) + compiler_tool = subprocess.list2cmdline(build_flags.tools.cpp_compiler) + # STRICT: Archiver must be configured in TOML - NO DEFAULTS + if not build_flags.tools.archiver: + raise RuntimeError( + f"CRITICAL: No archiver configured in [tools] section of {toml_path}. " + f'Please add: archiver = ["uv", "run", "python", "-m", "ziglang", "ar"] ' + f"or similar archiver configuration. " + f"Default tool fallbacks are NOT ALLOWED." + ) + archiver_tool = subprocess.list2cmdline(build_flags.tools.archiver) + + # Create CompilerOptions with TOML-loaded flags and tools + return CompilerOptions( + include_path=include_path, + defines=defines, + compiler_args=compiler_args, + compiler=compiler_tool, # Use command-based tool or fallback + archiver=archiver_tool, # Use command-based tool or fallback + **filtered_kwargs, + ) + + +if __name__ == "__main__": + import sys + + success = main() + sys.exit(0 if success else 1) + +# -------------------------------------------------------------------------------------- +# Compile Commands JSON generation +# -------------------------------------------------------------------------------------- + + +def _normalize_define_to_token(define: str) -> str: + """Normalize a define into a single "-DNAME[=VALUE]" token. + + Args: + define: Define that may or may not start with "-D" and may include "=VALUE" + + Returns: + A single token in the form "-DNAME" or "-DNAME=VALUE". + """ + # Strip any existing leading -D + value: str = define[2:] if define.startswith("-D") else define + return f"-D{value}" + + +def _build_arguments_for_tu( + tool_tokens: List[str], + include_root: Path, + settings: "CompilerOptions", + defines_from_build: List[str], + source_file: Path, + object_output: Path, +) -> List[str]: + """Compose the full arguments list for a single translation unit. + + This mirrors the real build by using the tool tokens from BuildFlags, the + include root, normalized defines, and compiler args from CompilerOptions. + """ + args: List[str] = [] + + # Program tokens (e.g. ["uv","run","python","-m","ziglang","c++"]) as-is + args.extend(tool_tokens) + + # Language specification and standard version where available + args.extend(["-x", "c++"]) + if settings.std_version: + args.append(f"-std={settings.std_version}") + + # Base include for project source root + args.append(f"-I{str(include_root)}") + + # Normalized defines (single-token -DNAME[=VALUE]) + for d in defines_from_build: + args.append(_normalize_define_to_token(d)) + + # Additional compiler args from settings (already include flags from TOML) + for a in settings.compiler_args: + args.append(a) + + # Compile only + args.extend(["-c", str(source_file)]) + args.extend(["-o", str(object_output)]) + + return args + + +def commands_json( + config_path: Path, + include_root: Path, + sources: List[Path], + output_json: Path, + quick_build: bool = True, + strict_mode: bool = False, +) -> None: + """Emit compile_commands.json compatible entries using the BuildFlags toolchain. + + - Loads BuildFlags via BuildFlags.parse(config_path, quick_build, strict_mode) + - Creates CompilerOptions via create_compiler_options_from_toml(...) + - For each source, composes the arguments array using tool tokens and flags + - Writes JSON array to output_json with both "arguments" and "command" fields + """ + try: + # Fail fast on missing configuration + if not config_path.exists(): + raise RuntimeError( + f"CRITICAL: Build flags TOML not found at {config_path}. " + f"This file is required." + ) + + # Single-source-of-truth configuration via BuildFlags.parse + build_flags = BuildFlags.parse(config_path, quick_build, strict_mode) + settings = create_compiler_options_from_toml( + toml_path=config_path, + include_path=str(include_root), + quick_build=quick_build, + strict_mode=strict_mode, + ) + + # Validate tools are present (no defaults allowed) + if not build_flags.tools.cpp_compiler: + raise RuntimeError( + f"CRITICAL: [tools].cpp_compiler missing in {config_path}. " + f"Please configure compiler tokens." + ) + + # Prepare output directory + output_json.parent.mkdir(parents=True, exist_ok=True) + + # Object directory for per-file outputs (mirrors prior generator) + objects_dir: Path = include_root.parent / ".build" / "compile_commands" + objects_dir.mkdir(parents=True, exist_ok=True) + + entries: List[Dict[str, Any]] = [] + for src in sources: + obj_path: Path = (objects_dir / src.with_suffix(".o").name).resolve() + program_tokens: List[str] = list(build_flags.tools.cpp_compiler) + defines_tokens: List[str] = list(build_flags.defines) + + args_list = _build_arguments_for_tu( + tool_tokens=program_tokens, + include_root=include_root, + settings=settings, + defines_from_build=defines_tokens, + source_file=src, + object_output=obj_path, + ) + + entry: Dict[str, Any] = { + "directory": str(include_root.parent.resolve()), + "file": str(src.resolve()), + "arguments": args_list, + "command": subprocess.list2cmdline(args_list), + } + entries.append(entry) + + # Write JSON (common to both paths) + import json + + with output_json.open("w", encoding="utf-8") as f: + f.write(json.dumps(entries, indent=2)) + f.write("\n") + + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except FileNotFoundError as e: + raise RuntimeError(f"CRITICAL: Required file not found: {e}") + except Exception as e: + # Re-raise with context + raise RuntimeError(f"CRITICAL: Failed to generate compile_commands.json: {e}") diff --git a/v1.5/lib/FastLED/ci/compiler/compile_for_board.py b/v1.5/lib/FastLED/ci/compiler/compile_for_board.py new file mode 100644 index 0000000..bdfa423 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/compile_for_board.py @@ -0,0 +1,261 @@ +# pyright: reportUnknownMemberType=false +""" +Compilation support for individual boards. +""" + +import os +import shutil +import subprocess +import time +from pathlib import Path +from threading import Lock +from typing import List + +from ci.boards import Board # type: ignore +from ci.util.locked_print import locked_print + + +ERROR_HAPPENED = False + + +IS_GITHUB = "GITHUB_ACTIONS" in os.environ +FIRST_BUILD_LOCK = Lock() +USE_FIRST_BUILD_LOCK = IS_GITHUB + + +def errors_happened() -> bool: + """Return whether any errors happened during the build.""" + return ERROR_HAPPENED + + +def _fastled_js_is_parent_directory(p: Path) -> bool: + """Check if fastled_js is a parent directory of the given path.""" + # Check if fastled_js is a parent directory of p + return "fastled_js" in str(p.absolute()) + + +def compile_for_board_and_example( + board: Board, + example: Path, + build_dir: str | None, + verbose_on_failure: bool, + libs: list[str] | None, +) -> tuple[bool, str]: + """Compile the given example for the given board.""" + global ERROR_HAPPENED # pylint: disable=global-statement + if board.board_name == "web": + locked_print(f"Skipping web target for example {example}") + return True, "" + board_name = board.board_name + use_pio_run = board.use_pio_run + real_board_name = board.get_real_board_name() + libs = libs or [] + builddir = ( + Path(build_dir) / board_name if build_dir else Path(".build") / board_name + ) + builddir.mkdir(parents=True, exist_ok=True) + srcdir = builddir / "src" + # Remove the previous *.ino file if it exists, everything else is recycled + # to speed up the next build. + if srcdir.exists(): + shutil.rmtree(srcdir, ignore_errors=False) + locked_print(f"*** Building example {example} for board {board_name} ***") + cwd: str | None = None + shell: bool = False + # Copy all files from the example directory to the "src" directory + for src_file in example.rglob("*"): + if src_file.is_file(): + if _fastled_js_is_parent_directory(src_file): + # Skip the fastled_js folder, it's not needed for the build. + continue + src_dir = src_file.parent + path = src_dir.relative_to(example) + dst_dir = srcdir / path + os.makedirs(dst_dir, exist_ok=True) + locked_print(f"Copying {src_file} to {dst_dir / src_file.name}") + os.makedirs(srcdir, exist_ok=True) + shutil.copy(src_file, dst_dir / src_file.name) + # libs = ["src", "ci"] + if use_pio_run: + # we have to copy a few folders of pio ci in order to get this to work. + for lib in libs: + project_libdir = Path(lib) + assert project_libdir.exists() + build_lib = builddir / "lib" / lib + shutil.rmtree(build_lib, ignore_errors=True) + shutil.copytree(project_libdir, build_lib) + + cwd = str(builddir) + cmd_list = [ + "pio", + "run", + ] + # in this case we need to manually copy the example to the src directory + # because platformio doesn't support building a single file. + # ino_file = example / f"{example.name}.ino" + else: + cmd_list = [ + "pio", + "ci", + "--board", + real_board_name, + *[f"--lib={lib}" for lib in libs], + "--keep-build-dir", + f"--build-dir={builddir.as_posix()}", + ] + cmd_list.append(f"{example.as_posix()}/*ino") + cmd_str = subprocess.list2cmdline(cmd_list) + msg_lsit = [ + "\n\n******************************", + f"* Running command in cwd: {cwd if cwd else os.getcwd()}", + f"* {cmd_str}", + "******************************\n", + ] + msg = "\n".join(msg_lsit) + locked_print(msg) + + # Start timing for the process + start_time = time.time() + + # Run the process with real-time output capture and timing + result = subprocess.Popen( + cmd_list, + cwd=cwd, + shell=shell, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + # Capture output lines in real-time with timing + stdout_lines: List[str] = [] + if result.stdout: + for line in iter(result.stdout.readline, ""): + if line: + elapsed = time.time() - start_time + # Format timing as seconds with 2 decimal places + timing_prefix = f"{elapsed:5.2f} " + timed_line = timing_prefix + line.rstrip() + stdout_lines.append( + line.rstrip() + ) # Store original line for return value + locked_print(timed_line) + + # Wait for process to complete + result.wait() + + # Join all stdout lines for the return value + stdout = "\n".join(stdout_lines) + + # replace all instances of "lib/src" => "src" so intellisense can find the files + # with one click. + stdout = stdout.replace("lib/src", "src").replace("lib\\src", "src") + if result.returncode != 0: + if not verbose_on_failure: + ERROR_HAPPENED = True + return False, stdout + if ERROR_HAPPENED: + return False, "" + ERROR_HAPPENED = True + locked_print( + f"*** Error compiling example {example} for board {board_name} ***" + ) + # re-running command with verbose output to see what the defines are. + cmd_list.append("-v") + cmd_str = subprocess.list2cmdline(cmd_list) + msg_lsit = [ + "\n\n******************************", + "* Re-running failed command but with verbose output:", + f"* {cmd_str}", + "******************************\n", + ] + msg = "\n".join(msg_lsit) + locked_print(msg) + + # Start timing for the verbose re-run + start_time_verbose = time.time() + + # Run the verbose process with real-time output capture and timing + result = subprocess.Popen( + cmd_list, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + # Capture output lines in real-time with timing for verbose run + stdout_lines_verbose: List[str] = [] + if result.stdout: + for line in iter(result.stdout.readline, ""): + if line: + elapsed = time.time() - start_time_verbose + # Format timing as seconds with 2 decimal places + timing_prefix = f"{elapsed:5.2f} " + timed_line = timing_prefix + line.rstrip() + stdout_lines_verbose.append( + line.rstrip() + ) # Store original line for return value + locked_print(timed_line) + + # Wait for verbose process to complete + result.wait() + + # Join all stdout lines for the return value + stdout = "\n".join(stdout_lines_verbose) + stdout = ( + stdout + + "\n\nThis is a second attempt, but with verbose output, look above for compiler errors.\n" + ) + return False, stdout + locked_print(f"*** Finished building example {example} for board {board_name} ***") + return True, stdout + + +# Function to process task queues for each board +def compile_examples( + board: Board, + examples: list[Path], + build_dir: str | None, + verbose_on_failure: bool, + libs: list[str] | None, +) -> tuple[bool, str]: + """Process the task queue for the given board.""" + global ERROR_HAPPENED # pylint: disable=global-statement + board_name = board.board_name + is_first = True + for example in examples: + example = example.relative_to(Path(".").resolve()) + if ERROR_HAPPENED: + return True, "" + locked_print(f"\n*** Building {example} for board {board_name} ***") + if is_first: + locked_print( + f"*** Building for first example {example} board {board_name} ***" + ) + if is_first and USE_FIRST_BUILD_LOCK: + with FIRST_BUILD_LOCK: + # Github runners are memory limited and the first job is the most + # memory intensive since all the artifacts are being generated in parallel. + success, message = compile_for_board_and_example( + board=board, + example=example, + build_dir=build_dir, + verbose_on_failure=verbose_on_failure, + libs=libs, + ) + else: + success, message = compile_for_board_and_example( + board=board, + example=example, + build_dir=build_dir, + verbose_on_failure=verbose_on_failure, + libs=libs, + ) + is_first = False + if not success: + ERROR_HAPPENED = True + return ( + False, + f"Error building {example} for board {board_name}. stdout:\n{message}", + ) + return True, "" diff --git a/v1.5/lib/FastLED/ci/compiler/compiler.py b/v1.5/lib/FastLED/ci/compiler/compiler.py new file mode 100644 index 0000000..a21adf2 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/compiler.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +Abstract Base Class for FastLED Compilers + +Defines the interface that all FastLED compiler implementations must follow. +""" + +from abc import ABC, abstractmethod +from concurrent.futures import Future +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Any + + +class CacheType(Enum): + """Compiler cache type options.""" + + NO_CACHE = "no_cache" + SCCACHE = "sccache" + + +@dataclass +class CompilerResult: + """Base result class for compiler operations.""" + + success: bool + output: str + build_dir: Path + + +@dataclass +class InitResult(CompilerResult): + """Result from compiler initialization.""" + + @property + def platformio_ini(self) -> Path: + return self.build_dir / "platformio.ini" + + +@dataclass +class SketchResult(CompilerResult): + """Result from sketch compilation.""" + + example: str + + +class Compiler(ABC): + """Abstract base class defining the interface for FastLED compilers.""" + + def __init__(self) -> None: + """Initialize the compiler.""" + pass + + @abstractmethod + def build(self, examples: list[str]) -> list[Future[SketchResult]]: + """Build a list of examples with proper resource management. + + Args: + examples: List of example names or paths to compile + + Returns: + List of Future objects containing SketchResult for each example + """ + pass + + @abstractmethod + def clean(self) -> None: + """Clean build artifacts for this platform.""" + pass + + @abstractmethod + def clean_all(self) -> None: + """Clean all build artifacts (local and global) for this platform.""" + pass + + @abstractmethod + def deploy( + self, example: str, upload_port: str | None = None, monitor: bool = False + ) -> SketchResult: + """Deploy (upload) a specific example to the target device. + + Args: + example: Name of the example to deploy + upload_port: Optional specific port for upload + monitor: If True, attach to device monitor after successful upload + + Returns: + SketchResult indicating success/failure of deployment + """ + pass + + @abstractmethod + def cancel_all(self) -> None: + """Cancel all currently running builds.""" + pass + + @abstractmethod + def check_usb_permissions(self) -> tuple[bool, str]: + """Check if USB device access is properly configured. + + Returns: + Tuple of (has_access, status_message) + """ + pass + + @abstractmethod + def install_usb_permissions(self) -> bool: + """Install platform-specific USB permissions or equivalent. + + Returns: + True if installation succeeded, False otherwise + """ + pass + + @abstractmethod + def get_cache_stats(self) -> str: + """Get compiler statistics as a formatted string. + + This can include cache statistics, build metrics, performance data, + or any other relevant compiler statistics. + + Returns: + Formatted string containing compiler statistics, or empty string if none available + """ + pass diff --git a/v1.5/lib/FastLED/ci/compiler/cpp_test_compile.py b/v1.5/lib/FastLED/ci/compiler/cpp_test_compile.py new file mode 100644 index 0000000..f3a4f01 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/cpp_test_compile.py @@ -0,0 +1,993 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false, reportReturnType=false, reportMissingParameterType=false +import argparse +import hashlib +import json +import logging +import os +import shutil +import subprocess +import sys +import time +from concurrent.futures import Future +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from ci.util.paths import PROJECT_ROOT +from ci.util.running_process import RunningProcess +from ci.util.test_args import parse_args as parse_global_test_args + +from .clang_compiler import ( + BuildFlags, + Compiler, + CompilerOptions, + LinkOptions, + test_clang_accessibility, +) +from .test_example_compilation import create_fastled_compiler + + +# Configure logging +logger = logging.getLogger(__name__) + +BUILD_DIR = PROJECT_ROOT / "tests" / ".build" +BUILD_DIR.mkdir(parents=True, exist_ok=True) +CACHE_DIR = PROJECT_ROOT / ".cache" +CACHE_DIR.mkdir(parents=True, exist_ok=True) +TEST_FILES_LIST = CACHE_DIR / "test_files_list.txt" + + +# ============================================================================ +# HASH-BASED LINKING CACHE (same optimization as examples) +# ============================================================================ + + +def calculate_file_hash(file_path: Path) -> str: + """Calculate SHA256 hash of a file (same as FastLEDTestCompiler)""" + if not file_path.exists(): + return "no_file" + + hash_sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + return hash_sha256.hexdigest() + + +def calculate_linker_args_hash(linker_args: list[str]) -> str: + """Calculate SHA256 hash of linker arguments (same as FastLEDTestCompiler)""" + args_str = "|".join(sorted(linker_args)) + hash_sha256 = hashlib.sha256() + hash_sha256.update(args_str.encode("utf-8")) + return hash_sha256.hexdigest() + + +def calculate_link_cache_key( + object_files: list[str | Path], fastled_lib_path: Path, linker_args: list[str] +) -> str: + """Calculate comprehensive cache key for linking (same as examples)""" + # Calculate hash for each object file + obj_hashes: list[str] = [] + for obj_file in object_files: + obj_hashes.append(calculate_file_hash(Path(obj_file))) + + # Combine object file hashes in a stable way (sorted by path for consistency) + sorted_obj_paths = sorted(str(obj) for obj in object_files) + sorted_obj_hashes: list[str] = [] + for obj_path in sorted_obj_paths: + obj_file = Path(obj_path) + sorted_obj_hashes.append(calculate_file_hash(obj_file)) + + combined_obj_hash = hashlib.sha256( + "|".join(sorted_obj_hashes).encode("utf-8") + ).hexdigest() + + # Calculate other hashes + fastled_hash = calculate_file_hash(fastled_lib_path) + linker_hash = calculate_linker_args_hash(linker_args) + + # Combine all components (same format as FastLEDTestCompiler) + combined = f"fastled:{fastled_hash}|objects:{combined_obj_hash}|flags:{linker_hash}" + final_hash = hashlib.sha256(combined.encode("utf-8")).hexdigest() + + return final_hash[:16] # Use first 16 chars for readability + + +def get_link_cache_dir() -> Path: + """Get the link cache directory (same as examples)""" + cache_dir = Path(".build/link_cache") + cache_dir.mkdir(parents=True, exist_ok=True) + return cache_dir + + +def get_cached_executable(test_name: str, cache_key: str) -> Optional[Path]: + """Check if cached executable exists (same as examples)""" + cache_dir = get_link_cache_dir() + cached_exe = cache_dir / f"{test_name}_{cache_key}.exe" + return cached_exe if cached_exe.exists() else None + + +def cache_executable(test_name: str, cache_key: str, exe_path: Path) -> None: + """Cache an executable for future use (same as examples)""" + cache_dir = get_link_cache_dir() + cached_exe = cache_dir / f"{test_name}_{cache_key}.exe" + try: + shutil.copy2(exe_path, cached_exe) + except Exception as e: + print(f"Warning: Failed to cache {test_name}: {e}") + + +# ============================================================================ +# END HASH-BASED LINKING CACHE +# ============================================================================ + + +def get_test_files() -> Set[str]: + """Get a set of all test files""" + test_files: Set[str] = set() + tests_dir = PROJECT_ROOT / "tests" + if tests_dir.exists(): + for file_path in tests_dir.rglob("test_*.cpp"): + # Store relative paths for consistency + test_files.add(str(file_path.relative_to(PROJECT_ROOT))) + return test_files + + +def check_test_files_changed() -> bool: + """Check if test files have changed since last run""" + try: + current_files = get_test_files() + + if TEST_FILES_LIST.exists(): + # Read previous file list + with open(TEST_FILES_LIST, "r") as f: + previous_files = set(line.strip() for line in f if line.strip()) + + # Compare file sets + if current_files == previous_files: + return False # No changes + else: + print("Test files have changed, cleaning build directory...") + return True # Files changed + else: + # No previous file list, need to clean + return True + except Exception as e: + print(f"Warning: Error checking test file changes: {e}") + return True # Default to cleaning on error + + +def save_test_files_list() -> None: + """Save current test file list""" + try: + current_files = get_test_files() + with open(TEST_FILES_LIST, "w") as f: + for file_path in sorted(current_files): + f.write(f"{file_path}\n") + except KeyboardInterrupt: + raise + except Exception as e: + print(f"Warning: Failed to save test file list: {e}") + + +def clean_build_directory(): + print("Cleaning build directory...") + shutil.rmtree(BUILD_DIR, ignore_errors=True) + BUILD_DIR.mkdir(parents=True, exist_ok=True) + print("Build directory cleaned.") + + +HERE = Path(__file__).resolve().parent + +WASM_BUILD = False +USE_ZIG = False +USE_CLANG = False + + +# Legacy CMake helper functions removed - using optimized Python API + + +def get_unit_test_fastled_sources() -> list[Path]: + """Get essential FastLED .cpp files for unit test library creation (optimized paradigm).""" + # Always work from project root + project_root = Path( + __file__ + ).parent.parent.parent # Go up from ci/compiler/ to project root + src_dir = project_root / "src" + + # Core FastLED files that must be included for unit tests + core_files: list[Path] = [ + src_dir / "FastLED.cpp", + src_dir / "colorutils.cpp", + src_dir / "hsv2rgb.cpp", + ] + + # Find all .cpp files in key directories + additional_sources: list[Path] = [] + for pattern in ["*.cpp", "lib8tion/*.cpp", "platforms/stub/*.cpp"]: + additional_sources.extend(list(src_dir.glob(pattern))) + + # Include essential .cpp files from nested directories + additional_sources.extend(list(src_dir.rglob("*.cpp"))) + + # Filter out duplicates and ensure files exist + all_sources: list[Path] = [] + seen_files: set[Path] = set() + + for cpp_file in core_files + additional_sources: + # Skip stub_main.cpp since unit tests have their own main + if cpp_file.name == "stub_main.cpp": + continue + + # Skip platform-specific files that aren't needed for unit tests + rel_path_str = str(cpp_file) + if any( + skip in rel_path_str for skip in ["wasm", "esp", "avr", "arm", "teensy"] + ): + continue + + if cpp_file.exists() and cpp_file not in seen_files: + all_sources.append(cpp_file) + seen_files.add(cpp_file) + + return all_sources + + +def create_unit_test_fastled_library( + clean: bool = False, use_pch: bool = True +) -> Path | None: + """Create libfastled.a static library specifically for unit tests with FASTLED_FORCE_NAMESPACE=1. + + CRITICAL: Unit tests need their own separate library from examples because they use + different compilation flags. Unit tests require FASTLED_FORCE_NAMESPACE=1 to put + all symbols in the fl:: namespace that the tests expect. + """ + + # Unit tests get their own separate library directory under .build/fastled/unit/ + fastled_build_dir = BUILD_DIR.parent / ".build" / "fastled" / "unit" + fastled_build_dir.mkdir(parents=True, exist_ok=True) + lib_file = fastled_build_dir / "libfastled.a" + + if lib_file.exists() and not clean: + print(f"[LIBRARY] Using existing FastLED library: {lib_file}") + return lib_file + + print("[LIBRARY] Creating FastLED static library with proper compiler flags...") + + # Create a proper FastLED compiler (NOT unit test compiler) for the library + # This ensures the library is compiled without FASTLED_FORCE_NAMESPACE=1 + print("[LIBRARY] Creating FastLED library compiler (without unit test flags)...") + + # Save current directory and change to project root for create_fastled_compiler + import os + + project_root = Path(__file__).parent.parent.parent + original_cwd = os.getcwd() + os.chdir(str(project_root)) + + try: + library_compiler = create_fastled_compiler( + use_pch=use_pch, + parallel=True, + ) + + # CRITICAL: Add required defines for unit test library compilation + if library_compiler.settings.defines is None: + library_compiler.settings.defines = [] + + # Add FASTLED_FORCE_NAMESPACE=1 to export symbols in fl:: namespace + library_compiler.settings.defines.append("FASTLED_FORCE_NAMESPACE=1") + print("[LIBRARY] Added FASTLED_FORCE_NAMESPACE=1 to library compiler") + + # Add FASTLED_TESTING=1 to include MockTimeProvider and test utility functions + library_compiler.settings.defines.append("FASTLED_TESTING=1") + print("[LIBRARY] Added FASTLED_TESTING=1 to library compiler") + + finally: + # Restore original working directory + os.chdir(original_cwd) + + # Get FastLED sources using optimized selection + fastled_sources = get_unit_test_fastled_sources() + fastled_objects: list[Path] = [] + + obj_dir = fastled_build_dir / "obj" + obj_dir.mkdir(exist_ok=True) + + print(f"[LIBRARY] Compiling {len(fastled_sources)} FastLED source files...") + + # Compile each source file with optimized naming (same as examples) + futures: List[Tuple[Any, ...]] = [] + project_root = Path(__file__).parent.parent.parent + src_dir = project_root / "src" + + for cpp_file in fastled_sources: + # Create unique object file name by including relative path to prevent collisions + # Convert path separators to underscores to create valid filename + if cpp_file.is_relative_to(src_dir): + rel_path = cpp_file.relative_to(src_dir) + else: + rel_path = cpp_file + + # Replace path separators with underscores for unique object file names + obj_name = str(rel_path.with_suffix(".o")).replace("/", "_").replace("\\", "_") + obj_file = obj_dir / obj_name + future = library_compiler.compile_cpp_file(cpp_file, obj_file) + futures.append((future, obj_file, cpp_file)) + + # Wait for compilation to complete + compiled_count = 0 + for future, obj_file, cpp_file in futures: + try: + result = future.result() + if result.ok: + fastled_objects.append(obj_file) + compiled_count += 1 + else: + print( + f"[LIBRARY] WARNING: Failed to compile {cpp_file.relative_to(src_dir)}: {result.stderr[:100]}..." + ) + except Exception as e: + print( + f"[LIBRARY] WARNING: Exception compiling {cpp_file.relative_to(src_dir)}: {e}" + ) + + print( + f"[LIBRARY] Successfully compiled {compiled_count}/{len(fastled_sources)} FastLED sources" + ) + + if not fastled_objects: + print("[LIBRARY] ERROR: No FastLED source files compiled successfully") + return None + + # Create static library using the same approach as examples + print(f"[LIBRARY] Creating static library: {lib_file}") + + archive_future = library_compiler.create_archive(fastled_objects, lib_file) + archive_result = archive_future.result() + + if not archive_result.ok: + print(f"[LIBRARY] ERROR: Library creation failed: {archive_result.stderr}") + return None + + print(f"[LIBRARY] SUCCESS: FastLED library created: {lib_file}") + return lib_file + + +def create_unit_test_compiler( + use_pch: bool = True, enable_static_analysis: bool = False, debug: bool = False +) -> Compiler: + """Create compiler optimized for unit test compilation with PCH support.""" + + # Always work from the project root, not from ci/compiler + project_root = Path( + __file__ + ).parent.parent.parent # Go up from ci/compiler/ to project root + current_dir = project_root + src_path = current_dir / "src" + + # Load build flags configuration + build_flags_path = current_dir / "ci" / "build_unit.toml" + build_flags = BuildFlags.parse( + build_flags_path, quick_build=True, strict_mode=False + ) + + # Unit test specific defines + unit_test_defines = [ + "FASTLED_UNIT_TEST=1", + "FASTLED_FORCE_NAMESPACE=1", + "FASTLED_USE_PROGMEM=0", + "STUB_PLATFORM", + "ARDUINO=10808", + "FASTLED_USE_STUB_ARDUINO", + "SKETCH_HAS_LOTS_OF_MEMORY=1", + "FASTLED_STUB_IMPL", + "FASTLED_USE_JSON_UI=1", + "FASTLED_TESTING", + "FASTLED_NO_AUTO_NAMESPACE", + "FASTLED_NO_PINMAP", + "HAS_HARDWARE_PIN_SUPPORT", + "FASTLED_DEBUG_LEVEL=1", + "FASTLED_NO_ATEXIT=1", + "DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS", + "ENABLE_CRASH_HANDLER", + "RELEASE=1", # Disable FASTLED_FORCE_DBG to avoid fl::println dependency + ] + + # Unit test specific compiler args + unit_test_args = [ + "-std=gnu++17", + "-fpermissive", + "-Wall", + "-Wextra", + "-Wno-deprecated-register", + "-Wno-backslash-newline-escape", + "-fno-exceptions", + "-fno-rtti", + # Optimization/debug controls set below based on debug flag + "-fno-omit-frame-pointer", + "-fno-inline-functions", + "-fno-vectorize", + "-fno-unroll-loops", + "-fno-strict-aliasing", + f"-I{current_dir}", + f"-I{src_path}", + f"-I{current_dir / 'tests'}", + f"-I{src_path / 'platforms' / 'stub'}", + ] + + # Apply quick vs debug modes + if debug: + unit_test_args.extend( + [ + "-O0", + "-g3", + "-fstandalone-debug", + ] + ) + if os.name == "nt": + unit_test_args.extend( + ["-gdwarf-4"] + ) # GNU debug info for Windows GNU toolchain + else: + unit_test_args.extend( + [ + "-O0", + "-g0", + ] + ) + + # Note: DWARF flags are added above conditionally when debug=True + + # PCH configuration with unit test specific headers + pch_output_path = None + pch_header_content = None + if use_pch: + cache_dir = current_dir / ".build" / "cache" + cache_dir.mkdir(parents=True, exist_ok=True) + pch_output_path = str(cache_dir / "fastled_unit_test_pch.hpp.pch") + + # Unit test specific PCH header content + pch_header_content = """// FastLED Unit Test PCH - Common headers for faster test compilation +#pragma once + +// Core test framework +#include "test.h" + +// Core FastLED headers that are used in nearly all unit tests +#include "FastLED.h" + +// Common C++ standard library headers used in tests +#include +#include +#include +#include +#include +#include +#include +#include + +// Platform headers for stub environment +#include "platforms/stub/fastled_stub.h" + +// Commonly tested FastLED components +#include "lib8tion.h" +#include "colorutils.h" +#include "hsv2rgb.h" +#include "fl/math.h" +#include "fl/vector.h" + +// Using namespace to match test files +using namespace fl; +""" + print(f"[PCH] Unit tests will use precompiled headers: {pch_output_path}") + print( + f"[PCH] PCH includes: test.h, FastLED.h, lib8tion.h, colorutils.h, and more" + ) + else: + print("[PCH] Precompiled headers disabled for unit tests") + + # Determine compiler + compiler_cmd = "python -m ziglang c++" + if USE_CLANG: + compiler_cmd = "clang++" + print("USING CLANG COMPILER FOR UNIT TESTS") + elif USE_ZIG: + print("USING ZIG COMPILER FOR UNIT TESTS") + else: + print("USING DEFAULT COMPILER FOR UNIT TESTS") + + settings = CompilerOptions( + include_path=str(src_path), + defines=unit_test_defines, + std_version="c++17", + compiler=compiler_cmd, + compiler_args=unit_test_args, + use_pch=use_pch, + pch_output_path=pch_output_path, + pch_header_content=pch_header_content, + parallel=True, + ) + + return Compiler(settings, build_flags) + + +def compile_unit_tests_python_api( + specific_test: str | None = None, + enable_static_analysis: bool = False, + use_pch: bool = True, + clean: bool = False, + debug: bool = False, +) -> bool: + """Compile unit tests using the fast Python API instead of CMake. + + Returns: + bool: True if all tests compiled and linked successfully, False otherwise + """ + from .clang_compiler import Compiler, LinkOptions + + print("=" * 60) + print("COMPILING UNIT TESTS WITH PYTHON API") + print("=" * 60) + + if clean: + print("Cleaning build directory...") + shutil.rmtree(BUILD_DIR, ignore_errors=True) + BUILD_DIR.mkdir(parents=True, exist_ok=True) + + # Create optimized compiler for unit tests + compiler = create_unit_test_compiler( + use_pch=use_pch, enable_static_analysis=enable_static_analysis, debug=debug + ) + + # Find all test files - work from project root + project_root = Path(__file__).parent.parent.parent + tests_dir = project_root / "tests" + test_files = [] + + if specific_test: + # Handle specific test with case-insensitive matching + test_name = ( + specific_test + if specific_test.startswith("test_") + else f"test_{specific_test}" + ) + test_file = tests_dir / f"{test_name}.cpp" + + # First try exact case match + if test_file.exists(): + test_files = [test_file] + print(f"Compiling specific test: {test_file.name}") + else: + # Try case-insensitive matching for all test files + found_match = False + for existing_file in tests_dir.glob("test_*.cpp"): + # Check if the file matches case-insensitively + existing_stem = existing_file.stem + existing_name = existing_stem.replace("test_", "") + + if ( + existing_stem.lower() == test_name.lower() + or existing_name.lower() == specific_test.lower() + ): + test_files = [existing_file] + print( + f"Compiling specific test (case-insensitive match): {existing_file.name}" + ) + found_match = True + break + + if not found_match: + raise RuntimeError(f"Test file not found: {test_file}") + else: + # Find all test files + test_files = list(tests_dir.glob("test_*.cpp")) + print(f"Found {len(test_files)} unit test files") + + if not test_files: + print("No test files found") + return + + # Ensure output directory exists + bin_dir = BUILD_DIR / "bin" + bin_dir.mkdir(parents=True, exist_ok=True) + + # Create clean execution directory as specified in requirements + clean_bin_dir = PROJECT_ROOT / "tests" / "bin" + clean_bin_dir.mkdir(parents=True, exist_ok=True) + + # Step 1: Compile doctest main once + print("Compiling doctest main...") + doctest_main_path = tests_dir / "doctest_main.cpp" + doctest_main_obj = bin_dir / "doctest_main.o" + + if not doctest_main_obj.exists() or clean: + doctest_compile_future = compiler.compile_cpp_file( + cpp_path=str(doctest_main_path), output_path=str(doctest_main_obj) + ) + doctest_result = doctest_compile_future.result() + + if doctest_result.return_code != 0: + raise RuntimeError( + f"Failed to compile doctest main: {doctest_result.stderr}" + ) + + # Step 2: Build FastLED library using optimized examples paradigm + print("Building FastLED library...") + + fastled_lib_path = create_unit_test_fastled_library(clean, use_pch=use_pch) + + # Step 3: Compile and link each test (PARALLEL OPTIMIZATION) + print(f"Compiling {len(test_files)} tests...") + start_time = time.time() + + # Phase 1: Start all compilations in parallel (NON-BLOCKING) + print("🚀 Starting parallel compilation...") + compile_start = time.time() + compile_futures: Dict[str, Future[Any]] = {} + test_info: Dict[str, Any] = {} + + for test_file in test_files: + test_name = test_file.stem + executable_path = bin_dir / test_name + object_path = bin_dir / f"{test_name}.o" + + test_info[test_name] = { + "test_file": test_file, + "executable_path": executable_path, + "object_path": object_path, + } + + print(f" Compiling {test_name}...") + + # Start compilation (NON-BLOCKING) + compile_future = compiler.compile_cpp_file( + cpp_path=str(test_file), output_path=str(object_path) + ) + compile_futures[test_name] = compile_future + + # Phase 2: Wait for all compilations to complete and check cache before linking + compile_dispatch_time = time.time() - compile_start + print( + f"⏳ Waiting for compilations to complete... (dispatch took {compile_dispatch_time:.2f}s)" + ) + compile_wait_start = time.time() + link_futures: Dict[str, Dict[str, Any]] = {} + cache_hits = 0 + success_count = 0 + + for test_name, compile_future in compile_futures.items(): + try: + compile_result = ( + compile_future.result() + ) # BLOCKING WAIT for this specific test + + if compile_result.return_code != 0: + print(f"❌ Compilation failed for {test_name}: {compile_result.stderr}") + continue + + # Prepare linking info (same logic as before) + info = test_info[test_name] + executable_path = info["executable_path"] + object_path = info["object_path"] + + tests_with_own_main = ["test_example_compilation"] + if test_name in tests_with_own_main: + object_files: list[str | Path] = [object_path] + else: + object_files: list[str | Path] = [object_path, doctest_main_obj] + + # Platform-specific linker arguments for crash handler support + if os.name == "nt": # Windows + linker_args = ["-ldbghelp", "-lpsapi"] + else: # Linux/macOS + linker_args = ["-pthread"] + + # HASH-BASED CACHE CHECK (same as examples) + if not fastled_lib_path: + print(f"⚠️ No FastLED library found, skipping cache for {test_name}") + cache_key = "no_fastled_lib" + cached_exe = None + else: + cache_key = calculate_link_cache_key( + object_files, fastled_lib_path, linker_args + ) + cached_exe = get_cached_executable(test_name, cache_key) + + if cached_exe: + # Cache hit! Copy cached executable to target location + try: + shutil.copy2(cached_exe, executable_path) + # Also copy to clean execution directory + clean_executable_path = clean_bin_dir / f"{test_name}.exe" + shutil.copy2(cached_exe, clean_executable_path) + cache_hits += 1 + success_count += 1 + print(f" ⚡ {test_name}: Using cached executable (cache hit)") + continue # Skip linking entirely + except Exception as e: + print(f" ⚠️ Failed to copy cached {test_name}, will relink: {e}") + # Fall through to actual linking + + # Cache miss - proceed with actual linking + static_libraries: List[Union[str, Path]] = [] + if fastled_lib_path and fastled_lib_path.exists(): + static_libraries.append(fastled_lib_path) + + link_options = LinkOptions( + output_executable=str(executable_path), + object_files=object_files, + static_libraries=static_libraries, + linker_args=linker_args, + ) + + # Start linking (NON-BLOCKING) and store cache info for later + link_future = compiler.link_program(link_options) + link_futures[test_name] = { + "future": link_future, + "cache_key": cache_key, + "executable_path": executable_path, + } + + except Exception as e: + print(f"❌ ERROR compiling {test_name}: {e}") + continue + + # Phase 3: Wait for all linking to complete and cache successful results + compile_wait_time = time.time() - compile_wait_start + print( + f"🔗 Waiting for linking to complete... (compilation took {compile_wait_time:.2f}s)" + ) + link_start = time.time() + cache_misses = 0 + + for test_name, link_info in link_futures.items(): + try: + link_future = link_info["future"] + cache_key = link_info["cache_key"] + executable_path = link_info["executable_path"] + + link_result = link_future.result() # BLOCKING WAIT for this specific test + + if link_result.return_code != 0: + print(f"⚠️ Linking failed for {test_name}: {link_result.stderr}") + cache_misses += 1 + continue + else: + success_count += 1 + cache_misses += 1 # This was a fresh link, not from cache + + # Cache the successful executable for future use (same as examples) + cache_executable(test_name, cache_key, executable_path) + + # Also copy to clean execution directory + clean_executable_path = clean_bin_dir / f"{test_name}.exe" + shutil.copy2(executable_path, clean_executable_path) + + except Exception as e: + print(f"❌ ERROR linking {test_name}: {e}") + cache_misses += 1 + continue + + link_time = time.time() - link_start + compilation_time = time.time() - start_time + + print(f"✅ Unit test compilation completed in {compilation_time:.2f}s") + print(f" 📊 Time breakdown:") + print(f" • Dispatch: {compile_dispatch_time:.2f}s") + print(f" • Compilation: {compile_wait_time:.2f}s") + print(f" • Linking: {link_time:.2f}s") + print(f" 🎯 Cache statistics (archive + hash linking optimization):") + print(f" • Cache hits: {cache_hits} (skipped linking)") + print(f" • Cache misses: {cache_misses} (fresh linking)") + print( + f" • Cache hit ratio: {cache_hits / max(1, cache_hits + cache_misses) * 100:.1f}%" + ) + print(f" Successfully compiled: {success_count}/{len(test_files)} tests") + print(f" Average: {compilation_time / len(test_files):.2f}s per test") + print(f" Output directory: {bin_dir}") + print(f" Cache directory: {get_link_cache_dir()}") + + # Return success only if ALL tests compiled and linked successfully + all_tests_successful = success_count == len(test_files) + if not all_tests_successful: + failed_count = len(test_files) - success_count + print(f"❌ {failed_count} test(s) failed to compile or link") + + return all_tests_successful + + +def parse_arguments(): + parser = argparse.ArgumentParser( + description="Compile FastLED library with different compiler options." + ) + parser.add_argument("--use-zig", action="store_true", help="Use Zig compiler") + parser.add_argument("--use-clang", action="store_true", help="Use Clang compiler") + parser.add_argument("--wasm", action="store_true", help="Build for WebAssembly") + parser.add_argument( + "--clean", + action="store_true", + help="Clean the build directory before compiling", + ) + parser.add_argument( + "--test", + help="Specific test to compile (without test_ prefix)", + ) + parser.add_argument( + "--check", + action="store_true", + help="Enable static analysis (IWYU, clang-tidy)", + ) + parser.add_argument("--no-unity", action="store_true", help="Disable unity build") + parser.add_argument( + "--no-pch", action="store_true", help="Disable precompiled headers (PCH)" + ) + parser.add_argument("--debug", action="store_true", help="Enable debug symbols") + + parser.add_argument("--verbose", action="store_true", help="Enable verbose output") + return parser.parse_args() + + +def get_build_info(args: argparse.Namespace) -> dict[str, str | dict[str, str]]: + return { + "USE_ZIG": str(USE_ZIG), + "USE_CLANG": str(USE_CLANG), + "WASM_BUILD": str(WASM_BUILD), + "CC": os.environ.get("CC", ""), + "CXX": os.environ.get("CXX", ""), + "AR": os.environ.get("AR", ""), + "CFLAGS": os.environ.get("CFLAGS", ""), + "CXXFLAGS": os.environ.get("CXXFLAGS", ""), + "ARGS": { + "use_zig": str(args.use_zig), + "use_clang": str(args.use_clang), + "wasm": str(args.wasm), + "specific_test": str(args.test) if args.test else "all", + }, + } + + +def should_clean_build(build_info: dict[str, str | dict[str, str]]) -> bool: + build_info_file = BUILD_DIR / "build_info.json" + if not build_info_file.exists(): + return True + + try: + with open(build_info_file, "r") as f: + old_build_info = json.load(f) + except (json.JSONDecodeError, ValueError) as e: + # If JSON is corrupted or empty, remove it and force clean build + logger.warning( + f"Corrupted build_info.json detected ({e}), removing and forcing clean build" + ) + try: + build_info_file.unlink() + except OSError: + pass # Ignore if we can't remove it + return True + + # If build parameters have changed, we need to rebuild + if old_build_info != build_info: + # Check if this is just a change in specific test target + old_args_raw = old_build_info.get("ARGS", {}) + new_args_raw = build_info.get("ARGS", {}) + + # Ensure ARGS is a dictionary + if not isinstance(old_args_raw, dict) or not isinstance(new_args_raw, dict): + return True + + old_args: Dict[str, Any] = old_args_raw # type: ignore + new_args: Dict[str, Any] = new_args_raw # type: ignore + + # If only the specific test changed and everything else is the same, + # we don't need to clean the build directory + old_test = old_args.get("specific_test", "all") + new_test = new_args.get("specific_test", "all") + + # Create copies without the specific_test field for comparison + old_args_no_test: Dict[str, Any] = { + k: v for k, v in old_args.items() if k != "specific_test" + } + new_args_no_test: Dict[str, Any] = { + k: v for k, v in new_args.items() if k != "specific_test" + } + + # If only the test target changed, don't clean + if ( + old_args_no_test == new_args_no_test + and old_build_info.get("USE_ZIG") == build_info.get("USE_ZIG") + and old_build_info.get("USE_CLANG") == build_info.get("USE_CLANG") + and old_build_info.get("WASM_BUILD") == build_info.get("WASM_BUILD") + and old_build_info.get("CC") == build_info.get("CC") + and old_build_info.get("CXX") == build_info.get("CXX") + and old_build_info.get("AR") == build_info.get("AR") + and old_build_info.get("CFLAGS") == build_info.get("CFLAGS") + and old_build_info.get("CXXFLAGS") == build_info.get("CXXFLAGS") + ): + print( + f"Build parameters unchanged, only test target changed: {old_test} -> {new_test}" + ) + return False + + return True + + return False + + +def update_build_info(build_info: dict[str, str | dict[str, str]]): + build_info_file = BUILD_DIR / "build_info.json" + with open(build_info_file, "w") as f: + json.dump(build_info, f, indent=2) + + +def main() -> None: + global USE_ZIG, USE_CLANG, WASM_BUILD + + args = parse_arguments() + USE_ZIG = args.use_zig # use Zig's clang compiler + USE_CLANG = args.use_clang # Use pure Clang for WASM builds + WASM_BUILD = args.wasm + + using_gcc = not USE_ZIG and not USE_CLANG and not WASM_BUILD + if using_gcc: + if not shutil.which("g++"): + print( + "gcc compiler not found in PATH, falling back zig's built in clang compiler" + ) + USE_ZIG = True + USE_CLANG = False + + if USE_CLANG: + if not test_clang_accessibility(): + print( + "Clang compiler not found in PATH, falling back to Zig-clang compiler" + ) + USE_ZIG = True + USE_CLANG = False + + os.chdir(str(HERE)) + print(f"Current directory: {Path('.').absolute()}") + + # Auto-detection for --clean based on test file changes + need_clean = args.clean + if not need_clean: + # Only check for changes if --clean wasn't explicitly specified + need_clean = check_test_files_changed() + + build_info = get_build_info(args) + if need_clean or should_clean_build(build_info): + clean_build_directory() + # Save the file list after cleaning + save_test_files_list() + elif args.clean: + # If --clean was explicitly specified but not needed according to build info, + # still clean and save file list + clean_build_directory() + save_test_files_list() + + # Unit tests now use optimized Python API by default (same as examples) + use_pch = not getattr( + args, "no_pch", False + ) # Default to PCH enabled unless --no-pch specified + + # Use the optimized Python API with PCH optimization (now default for unit tests) + compilation_successful = compile_unit_tests_python_api( + specific_test=args.test, + enable_static_analysis=args.check, + use_pch=use_pch, + clean=need_clean, + debug=args.debug, + ) + + if compilation_successful: + update_build_info(build_info) + print("FastLED library compiled successfully.") + else: + print("❌ FastLED unit test compilation failed!") + import sys + + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/compiler/cpp_test_run.py b/v1.5/lib/FastLED/ci/compiler/cpp_test_run.py new file mode 100644 index 0000000..4d411b2 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/cpp_test_run.py @@ -0,0 +1,906 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false, reportMissingParameterType=false +import argparse +import multiprocessing +import os +import re +import subprocess +import sys +import tempfile +import time # Added for timing test execution +from dataclasses import dataclass +from pathlib import Path +from queue import Empty, PriorityQueue +from threading import Event, Lock, Thread +from typing import List + +import psutil + +from ci.util.paths import PROJECT_ROOT + + +def optimize_python_command(cmd: list[str]) -> list[str]: + """ + Optimize command list for subprocess execution in uv environment. + + For python commands, we need to use 'uv run python' to ensure access to + installed packages like ziglang. Direct sys.executable bypasses uv environment. + + Args: + cmd: Command list that may contain 'python' as first element + + Returns: + list[str]: Optimized command with 'python' prefixed by 'uv run' + """ + if cmd and (cmd[0] == "python" or cmd[0] == "python3"): + # Use uv run python to ensure access to uv-managed packages + optimized_cmd = ["uv", "run", "python"] + cmd[1:] + return optimized_cmd + return cmd + + +from ci.util.test_exceptions import ( + CompilationFailedException, + TestExecutionFailedException, + TestFailureInfo, + TestTimeoutException, +) + + +class OutputBuffer: + """Thread-safe output buffer with ordered output display""" + + def __init__(self) -> None: + self.output_queue: PriorityQueue[tuple[int, int, str]] = PriorityQueue() + self.next_sequence: int = 0 + self.sequence_lock: Lock = Lock() + self.stop_event: Event = Event() + self.output_thread: Thread = Thread(target=self._output_worker, daemon=True) + self.output_thread.start() + + def write(self, test_index: int, message: str) -> None: + """Write a message to the buffer with test index for ordering""" + with self.sequence_lock: + sequence = self.next_sequence + self.next_sequence += 1 + self.output_queue.put((test_index, sequence, message)) + + def _output_worker(self) -> None: + """Worker thread that processes output in order""" + while not self.stop_event.is_set() or not self.output_queue.empty(): + try: + item: tuple[int, int, str] = self.output_queue.get(timeout=0.1) + _, _, message = item + print(message, flush=True) + self.output_queue.task_done() + except Empty: + continue + except Exception as e: + print(f"Error in output worker: {e}") + continue + + def stop(self) -> None: + """Stop the output worker thread""" + self.stop_event.set() + if self.output_thread.is_alive(): + self.output_thread.join() + + +# Configure console for UTF-8 output on Windows +if os.name == "nt": # Windows + # Try to set console to UTF-8 mode + try: + # Set stdout and stderr to UTF-8 encoding + # Note: reconfigure() was added in Python 3.7 + if hasattr(sys.stdout, "reconfigure") and callable( + getattr(sys.stdout, "reconfigure", None) + ): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined] + if hasattr(sys.stderr, "reconfigure") and callable( + getattr(sys.stderr, "reconfigure", None) + ): + sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined] + except (AttributeError, OSError): + # Fallback for older Python versions or if reconfigure fails + pass + + +# Environment flags for backward compatibility +_SHOW_COMPILE = os.environ.get("FASTLED_TEST_SHOW_COMPILE", "").lower() in ( + "1", + "true", + "yes", +) +_SHOW_LINK = os.environ.get("FASTLED_TEST_SHOW_LINK", "").lower() in ( + "1", + "true", + "yes", +) + + +@dataclass +class FailedTest: + name: str + return_code: int + stdout: str + + +def check_iwyu_available() -> bool: + """Check if include-what-you-use is available in the system""" + try: + result = subprocess.run( + ["include-what-you-use", "--version"], + capture_output=True, + text=True, + timeout=10, + ) + return result.returncode == 0 + except ( + subprocess.CalledProcessError, + FileNotFoundError, + subprocess.TimeoutExpired, + ): + return False + + +def run_command( + command: str | list[str], + use_gdb: bool = False, + *, + verbose: bool = False, + show_compile: bool = False, + show_link: bool = False, +) -> tuple[int, str]: + captured_lines: list[str] = [] + + # Determine command type + is_test_execution = False + is_compile = False + is_link = False + if isinstance(command, str): + cmd_lower = command.replace("\\", "/").lower() + # Check if running test executable + is_test_execution = ( + "/test_" in cmd_lower + or ".build/bin/test_" in cmd_lower + or cmd_lower.endswith(".exe") + ) + # Check if compiling + is_compile = "-c" in cmd_lower and (".cpp" in cmd_lower or ".c" in cmd_lower) + # Check if linking + is_link = ( + not is_compile + and not is_test_execution + and ("-o" in cmd_lower or "lib" in cmd_lower) + ) + + if use_gdb: + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as gdb_script: + gdb_script.write("set pagination off\n") + gdb_script.write("run\n") + gdb_script.write("bt full\n") + gdb_script.write("info registers\n") + gdb_script.write("x/16i $pc\n") + gdb_script.write("thread apply all bt full\n") + gdb_script.write("quit\n") + + gdb_command = ( + f"gdb -return-child-result -batch -x {gdb_script.name} --args {command}" + ) + process = subprocess.Popen( + gdb_command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + shell=True, + text=False, + ) + assert process.stdout is not None + # Stream and capture output + while True: + line_bytes = process.stdout.readline() + line = line_bytes.decode("utf-8", errors="ignore") + if not line and process.poll() is not None: + break + if line: + captured_lines.append(line.rstrip()) + # Always print GDB output (it's only used for crashes anyway) + try: + print(line, end="", flush=True) + except UnicodeEncodeError: + # Fallback: replace problematic characters + print( + line.encode("utf-8", errors="replace").decode( + "utf-8", errors="replace" + ), + end="", + flush=True, + ) + + os.unlink(gdb_script.name) + output = "\n".join(captured_lines) + return process.returncode, output + else: + # Optimize list commands to avoid shell overhead + if isinstance(command, list): + # Optimize python commands and use shell=False for better performance + python_exe = optimize_python_command(command) + process = subprocess.Popen( + python_exe, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + shell=False, # Use shell=False for better performance with list commands + text=False, + ) + else: + # String commands still need shell=True + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + shell=True, + text=False, + ) + assert process.stdout is not None + # Stream and capture output + while True: + line_bytes = process.stdout.readline() + line = line_bytes.decode("utf-8", errors="ignore") + if not line and process.poll() is not None: + break + if line: + captured_lines.append(line.rstrip()) + # Determine if we should print this line + should_print = ( + verbose # Always print in verbose mode + or (is_compile and show_compile) # Print compilation if enabled + or (is_link and show_link) # Print linking if enabled + or (not is_test_execution) # Print non-test output + or ( + is_test_execution and process.returncode != 0 + ) # Print failed test output + or ( + is_test_execution + and any( + marker in line + for marker in [ + "Running test:", + "Test passed", + "Test FAILED", + "passed with return code", + "Test output:", + ] + ) + ) # Print test status + ) + if should_print: + try: + # Add prefix for compile/link commands + if is_compile and show_compile: + print("[COMPILE] ", end="", flush=True) + elif is_link and show_link: + print("[LINK] ", end="", flush=True) + print(line, end="", flush=True) + except UnicodeEncodeError: + # Fallback: replace problematic characters + print( + line.encode("utf-8", errors="replace").decode( + "utf-8", errors="replace" + ), + end="", + flush=True, + ) + + output = "\n".join(captured_lines) + return process.returncode, output + + +def compile_tests( + clean: bool = False, + unknown_args: list[str] = [], + specific_test: str | None = None, + quick_build: bool = True, + *, + verbose: bool = False, + show_compile: bool = False, + show_link: bool = False, +) -> None: + """ + Compile C++ tests using the Python build system. + """ + os.chdir(str(PROJECT_ROOT)) + print("🔧 Compiling tests using Python build system") + + _compile_tests_python( + clean, + unknown_args, + specific_test, + quick_build=quick_build, + verbose=verbose, + show_compile=show_compile, + show_link=show_link, + ) + + +def _compile_tests_python( + clean: bool = False, + unknown_args: list[str] = [], + specific_test: str | None = None, + quick_build: bool = True, + *, + verbose: bool = False, + show_compile: bool = False, + show_link: bool = False, +) -> None: + """Python build system with PCH optimization""" + # Use the optimized cpp_test_compile system directly + import subprocess + + cmd = ["uv", "run", "python", "-m", "ci.compiler.cpp_test_compile"] + + if specific_test: + cmd.extend(["--test", specific_test]) + if clean: + cmd.append("--clean") + if verbose: + cmd.append("--verbose") + if "--check" in unknown_args: + cmd.append("--check") + if "--no-pch" in unknown_args: + cmd.append("--no-pch") + # Forward debug mode to the compiler when quick_build is disabled + if not quick_build: + cmd.append("--debug") + + print("🚀 Using Python build system with PCH optimization") + result = subprocess.run(cmd) + if result.returncode != 0: + raise RuntimeError( + f"Unit test compilation failed with return code {result.returncode}" + ) + + +def run_tests( + specific_test: str | None = None, + *, + verbose: bool = False, + show_compile: bool = False, + show_link: bool = False, +) -> None: + """ + Run compiled tests with GDB crash analysis support. + """ + _run_tests_python(specific_test) + + +def _run_tests_python( + specific_test: str | None = None, + *, + verbose: bool = False, + show_compile: bool = False, + show_link: bool = False, +) -> None: + """Run tests from Python build system""" + # Import the test compiler system + from ci.compiler.test_compiler import FastLEDTestCompiler + + # Get test executables from Python build system + test_compiler = FastLEDTestCompiler.get_existing_instance() + if not test_compiler: + print("No compiled tests found. Run compilation first.") + sys.exit(1) + + test_executables = test_compiler.get_test_executables(specific_test) + if not test_executables: + test_name = specific_test or "any tests" + print(f"No test executables found for: {test_name}") + sys.exit(1) + + print(f"Running {len(test_executables)} tests from Python build...") + + # Print list of tests that will be executed + print("Tests to execute:") + for i, test_exec in enumerate(test_executables, 1): + # Convert absolute path to relative for display + rel_path = os.path.relpath(test_exec.executable_path) + print(f" {i}. {test_exec.name} ({rel_path})") + print("") + + failed_tests: list[FailedTest] = [] + + # Convert to file list format for compatibility with existing logic + files: list[str] = [] + test_paths: dict[str, str] = {} + for test_exec in test_executables: + file_name = test_exec.name + if os.name == "nt" and not file_name.endswith(".exe"): + file_name += ".exe" + files.append(file_name) + test_paths[file_name] = str(test_exec.executable_path) + + print(f"Starting test execution for {len(files)} test files...") + _execute_test_files(files, "", failed_tests, specific_test, test_paths) + _handle_test_results(failed_tests) + + +def _execute_test_files( + files: list[str], + test_dir: str, + failed_tests: list[FailedTest], + specific_test: str | None, + test_paths: dict[str, str] | None = None, + *, + verbose: bool = False, + show_compile: bool = False, + show_link: bool = False, +) -> None: + """ + Execute test files in parallel with full GDB crash analysis. + + Args: + files: List of test file names + test_dir: Directory containing tests (for CMake) or empty string (for Python API) + failed_tests: List to collect failed tests + specific_test: Specific test name if filtering + test_paths: Dict mapping file names to full paths (for Python API) + """ + total_tests = len(files) + successful_tests = 0 + completed_tests = 0 + + # Initialize output buffer for ordered output + output_buffer = OutputBuffer() + output_buffer.write(0, f"Executing {total_tests} test files in parallel...") + + # Determine number of workers based on configuration + + # Get configuration from args + args = parse_args() + + # Force sequential execution if NO_PARALLEL is set + if os.environ.get("NO_PARALLEL"): + max_workers = 1 + output_buffer.write( + 0, "NO_PARALLEL environment variable set - forcing sequential execution" + ) + elif args.sequential: + max_workers = 1 + elif args.parallel: + max_workers = args.parallel + else: + max_workers = max(1, multiprocessing.cpu_count() - 1) # Leave one core free + + # Check memory limit + if args.max_memory: + memory_limit = args.max_memory * 1024 * 1024 # Convert MB to bytes + available_memory = psutil.virtual_memory().available + if memory_limit > available_memory: + output_buffer.write( + 0, + f"Warning: Requested memory limit {args.max_memory}MB exceeds available memory {available_memory / (1024 * 1024):.0f}MB", + ) + output_buffer.write( + 0, "Reducing number of parallel workers to stay within memory limits" + ) + # Estimate memory per test based on previous runs or default to 100MB + memory_per_test = 100 * 1024 * 1024 # 100MB per test + max_parallel_by_memory = max(1, memory_limit // memory_per_test) + max_workers = min(max_workers, max_parallel_by_memory) + + output_buffer.write(0, f"Using {max_workers} parallel workers") + + # Thread-safe counter + counter_lock = Lock() + + def run_single_test( + test_file: str, test_index: int + ) -> tuple[bool, float, str, int]: + """Run a single test and return its results""" + nonlocal completed_tests + + if test_paths: + test_path = test_paths[test_file] + else: + test_path = os.path.join(test_dir, test_file) + + # For .cpp files, compile them first + if test_path.endswith(".cpp"): + # Create a temporary directory for compilation + with tempfile.TemporaryDirectory() as temp_dir: + # Compile the test file + output_buffer.write( + test_index, + f"[{test_index}/{total_tests}] Compiling test: {test_file}", + ) + compile_cmd = [ + "python", + "-m", + "ziglang", + "c++", + "-o", + os.path.join(temp_dir, "test.exe"), + test_path, + "-I", + os.path.join(PROJECT_ROOT, "src"), + "-I", + os.path.join(PROJECT_ROOT, "tests"), + # NOTE: Compiler flags now come from build configuration TOML + ] + return_code, stdout = run_command(compile_cmd) + if return_code != 0: + output_buffer.write( + test_index, + f"[{test_index}/{total_tests}] ERROR: Failed to compile test: {test_file}", + ) + return False, 0.0, f"Failed to compile test: {stdout}", return_code + + # Update test_path to point to the compiled executable + test_path = os.path.join(temp_dir, "test.exe") + + if not (os.path.isfile(test_path) and os.access(test_path, os.X_OK)): + output_buffer.write( + test_index, + f"[{test_index}/{total_tests}] ERROR: Test file not found or not executable: {test_path}", + ) + return False, 0.0, f"Test file not found or not executable: {test_path}", 1 + + output_buffer.write( + test_index, f"[{test_index}/{total_tests}] Running test: {test_file}" + ) + if verbose: + output_buffer.write(test_index, f" Command: {test_path}") + + start_time = time.time() + # Pass --minimal flag to doctest when not in verbose mode to suppress output unless tests fail + cmd = [test_path] + if not verbose: + cmd.append("--minimal") + return_code, stdout = run_command(cmd) + elapsed_time = time.time() - start_time + + output = stdout + failure_pattern = re.compile(r"Test .+ failed with return code (\d+)") + failure_match = failure_pattern.search(output) + is_crash = failure_match is not None + + # Handle crashes with GDB (must be done synchronously) + if is_crash: + output_buffer.write( + test_index, f"Test crashed. Re-running with GDB to get stack trace..." + ) + _, gdb_stdout = run_command(test_path, use_gdb=True) + stdout += "\n--- GDB Output ---\n" + gdb_stdout + + # Extract crash information + crash_info = extract_crash_info(gdb_stdout) + output_buffer.write( + test_index, f"Crash occurred at: {crash_info.file}:{crash_info.line}" + ) + output_buffer.write(test_index, f"Cause: {crash_info.cause}") + output_buffer.write(test_index, f"Stack: {crash_info.stack}") + + # Print output based on verbosity and status + if verbose or return_code != 0: + output_buffer.write(test_index, "Test output:") + output_buffer.write(test_index, stdout) + + if return_code == 0: + output_buffer.write( + test_index, f" Test {test_file} passed in {elapsed_time:.2f}s" + ) + else: + output_buffer.write( + test_index, + f" Test {test_file} FAILED with return code {return_code} in {elapsed_time:.2f}s", + ) + + with counter_lock: + completed_tests += 1 + + return return_code == 0, elapsed_time, stdout, return_code + + try: + # Run tests in parallel using ThreadPoolExecutor + from concurrent.futures import ThreadPoolExecutor, as_completed + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all tests + future_to_test = { + executor.submit(run_single_test, test_file, i + 1): test_file + for i, test_file in enumerate(files) + } + + # Process results as they complete + for future in as_completed(future_to_test): + test_file = future_to_test[future] + try: + success, _, stdout, return_code = future.result() + if success: + with counter_lock: + successful_tests += 1 + else: + failed_tests.append( + FailedTest( + name=test_file, return_code=return_code, stdout=stdout + ) + ) + except Exception as e: + output_buffer.write( + 0, f"ERROR: Test {test_file} failed with exception: {e}" + ) + failed_tests.append( + FailedTest(name=test_file, return_code=1, stdout=str(e)) + ) + + # Print final summary + output_buffer.write( + 0, + f"Test execution complete: {successful_tests} passed, {len(failed_tests)} failed", + ) + if successful_tests == total_tests: + output_buffer.write(0, "All tests passed successfully!") + else: + output_buffer.write( + 0, f"Some tests failed ({len(failed_tests)} of {total_tests})" + ) + + finally: + # Ensure output buffer is stopped + output_buffer.stop() + + +def _handle_test_results( + failed_tests: list[FailedTest], *, verbose: bool = False +) -> None: + """Handle test results and exit appropriately (preserving existing logic)""" + if failed_tests: + print("Failed tests summary:") + failures: List[TestFailureInfo] = [] + for failed_test in failed_tests: + print( + f"Test {failed_test.name} failed with return code {failed_test.return_code}" + ) + # Always show output on failure + print("Output:") + # Show indented output for better readability + for line in failed_test.stdout.splitlines(): + print(f" {line}") + print() # Add spacing between failed tests + + failures.append( + TestFailureInfo( + test_name=failed_test.name, + command=f"test_{failed_test.name}", + return_code=failed_test.return_code, + output=failed_test.stdout, + error_type="test_execution_failure", + ) + ) + + tests_failed = len(failed_tests) + failed_test_names = [test.name for test in failed_tests] + print( + f"{tests_failed} test{'s' if tests_failed != 1 else ''} failed: {', '.join(failed_test_names)}" + ) + raise TestExecutionFailedException(f"{tests_failed} test(s) failed", failures) + if verbose: + print("All tests passed.") + + +@dataclass +class CrashInfo: + cause: str = "Unknown" + stack: str = "Unknown" + file: str = "Unknown" + line: str = "Unknown" + + +def extract_crash_info(gdb_output: str) -> CrashInfo: + lines = gdb_output.split("\n") + crash_info = CrashInfo() + + try: + for i, line in enumerate(lines): + if line.startswith("Program received signal"): + try: + crash_info.cause = line.split(":", 1)[1].strip() + except IndexError: + crash_info.cause = line.strip() + elif line.startswith("#0"): + crash_info.stack = line + for j in range(i, len(lines)): + if "at" in lines[j]: + try: + _, location = lines[j].split("at", 1) + location = location.strip() + if ":" in location: + crash_info.file, crash_info.line = location.rsplit( + ":", 1 + ) + else: + crash_info.file = location + except ValueError: + pass # If split fails, we keep the default values + break + break + except Exception as e: + print(f"Error parsing GDB output: {e}") + + return crash_info + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Compile and run C++ tests") + parser.add_argument( + "--compile-only", + action="store_true", + help="Only compile the tests without running them", + ) + parser.add_argument( + "--run-only", + action="store_true", + help="Only run the tests without compiling them", + ) + parser.add_argument( + "--only-run-failed-test", + action="store_true", + help="Only run the tests that failed in the previous run", + ) + parser.add_argument( + "--clean", action="store_true", help="Clean build before compiling" + ) + parser.add_argument( + "--test", + help="Specific test to run (without extension)", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose output", + ) + parser.add_argument( + "--show-compile", + action="store_true", + help="Show compilation commands and output", + ) + parser.add_argument( + "--show-link", + action="store_true", + help="Show linking commands and output", + ) + parser.add_argument( + "--parallel", + type=int, + help="Number of parallel test processes to run (default: CPU count - 1)", + ) + parser.add_argument( + "--sequential", + action="store_true", + help="Run tests sequentially (disables parallel execution)", + ) + parser.add_argument( + "--max-memory", + type=int, + help="Maximum memory usage in MB for parallel test execution", + ) + + # Create mutually exclusive group for compiler selection + compiler_group = parser.add_mutually_exclusive_group() + compiler_group.add_argument( + "--clang", + help="Use Clang compiler", + action="store_true", + ) + compiler_group.add_argument( + "--gcc", + help="Use GCC compiler (default on non-Windows)", + action="store_true", + ) + parser.add_argument( + "--check", + action="store_true", + help="Enable static analysis (IWYU, clang-tidy)", + ) + + parser.add_argument( + "--no-unity", + action="store_true", + help="Disable unity builds for cpp tests", + ) + parser.add_argument( + "--no-pch", + action="store_true", + help="Disable precompiled headers (PCH) for unit tests", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Use debug build mode with full debug symbols (default is quick mode with -g0)", + ) + + args, unknown = parser.parse_known_args() + args.unknown = unknown + + return args + + +def main() -> None: + try: + args = parse_args() + + # Get verbosity flags from args + + run_only = args.run_only + compile_only = args.compile_only + specific_test = args.test + # only_run_failed_test feature to be implemented in future + _ = args.only_run_failed_test + use_clang = args.clang + + no_unity = args.no_unity + quick_build = ( + not args.debug + ) # Default to quick mode unless --debug is specified + # use_gcc = args.gcc + + if not run_only: + passthrough_args = args.unknown + if use_clang: + passthrough_args.append("--use-clang") + if args.check: + passthrough_args.append("--check") + if no_unity: + passthrough_args.append("--no-unity") + if args.no_pch: + passthrough_args.append("--no-pch") + # Note: --gcc is handled by not passing --use-clang (GCC is the default in compiler/cpp_test_compile.py) + compile_tests( + clean=args.clean, + unknown_args=passthrough_args, + specific_test=specific_test, + quick_build=quick_build, + verbose=args.verbose, + show_compile=args.show_compile, + show_link=args.show_link, + ) + + if not compile_only: + if specific_test: + run_tests( + specific_test, + verbose=args.verbose, + show_compile=args.show_compile, + show_link=args.show_link, + ) + else: + # Use our own test runner instead of CTest since CTest integration is broken + run_tests( + None, + verbose=args.verbose, + show_compile=args.show_compile, + show_link=args.show_link, + ) + except ( + CompilationFailedException, + TestExecutionFailedException, + TestTimeoutException, + ) as e: + # Print detailed failure information + print("\n" + "=" * 60) + print("FASTLED TEST FAILURE DETAILS") + print("=" * 60) + print(e.get_detailed_failure_info()) + print("=" * 60) + + # Exit with appropriate code + if e.failures: + # Use the return code from the first failure, or 1 if none available + exit_code = ( + e.failures[0].return_code if e.failures[0].return_code != 0 else 1 + ) + else: + exit_code = 1 + sys.exit(exit_code) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/compiler/packages.py b/v1.5/lib/FastLED/ci/compiler/packages.py new file mode 100644 index 0000000..00ce3ca --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/packages.py @@ -0,0 +1,784 @@ +""" +Enhanced Arduino Package Index Implementation with Pydantic + +This module provides a robust, production-ready Arduino package management system +that fully complies with the Arduino CLI Package Index JSON Specification. + +Key Features: +- Pydantic models with comprehensive validation +- Multi-source package index support +- Caching and persistence +- Package installation and dependency resolution +- Search and filtering capabilities +- Checksum validation and error handling +""" + +import asyncio +import hashlib +import json +import shutil +import sys +import tarfile +import zipfile +from datetime import datetime +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import urlparse +from urllib.request import urlopen + +import httpx +from pydantic import ( + BaseModel, + EmailStr, + Field, + HttpUrl, + ValidationError, + field_validator, + model_validator, +) + + +# Custom Exceptions +class PackageParsingError(Exception): + """Error parsing package index data""" + + pass + + +class PackageInstallationError(Exception): + """Error during package installation""" + + pass + + +class PackageValidationError(Exception): + """Error validating package data""" + + pass + + +# Core Pydantic Models with Enhanced Validation + + +class Help(BaseModel): + """Help information with online resources""" + + online: HttpUrl = Field(description="URL to online help resources") + + class Config: + schema_extra = { + "example": {"online": "https://github.com/espressif/arduino-esp32"} + } + + +class Board(BaseModel): + """Board information with complete properties""" + + name: str = Field(min_length=1, max_length=200, description="Board display name") + properties: Dict[str, Any] = Field( + default_factory=dict, description="Board configuration properties" + ) + + @field_validator("name") + @classmethod + def validate_board_name(cls, v: str) -> str: + """Validate board name format""" + if not v.strip(): + raise ValueError("Board name cannot be empty or whitespace") + return v.strip() + + class Config: + schema_extra = { + "example": { + "name": "ESP32 Dev Module", + "properties": { + "upload.tool": "esptool_py", + "upload.maximum_size": "1310720", + "build.mcu": "esp32", + }, + } + } + + +class ToolDependency(BaseModel): + """Tool dependency specification with validation""" + + packager: str = Field( + min_length=1, max_length=100, description="Tool packager name" + ) + name: str = Field(pattern=r"^[a-zA-Z0-9_.-]+$", description="Tool name") + version: str = Field(min_length=1, description="Required tool version") + + @field_validator("version") + @classmethod + def validate_version_format(cls, v: str) -> str: + """Validate version format - supports semantic versioning and Arduino versioning""" + if not v.strip(): + raise ValueError("Version cannot be empty") + # Allow flexible versioning (semantic, date-based, etc.) + return v.strip() + + class Config: + schema_extra = { + "example": { + "packager": "esp32", + "name": "xtensa-esp32-elf-gcc", + "version": "esp-2021r2-patch5-8.4.0", + } + } + + +class Platform(BaseModel): + """Platform specification with comprehensive validation""" + + name: str = Field(min_length=1, max_length=200, description="Platform display name") + architecture: str = Field( + pattern=r"^[a-zA-Z0-9_-]+$", description="Target architecture" + ) + version: str = Field(description="Platform version") + category: str = Field(min_length=1, description="Platform category") + url: HttpUrl = Field(description="Download URL for platform archive") + archive_filename: str = Field( + alias="archiveFileName", description="Archive file name" + ) + checksum: str = Field( + pattern=r"^SHA-256:[a-fA-F0-9]{64}$", description="SHA-256 checksum" + ) + size_mb: float = Field(gt=0, alias="size", description="Archive size in megabytes") + boards: List[Board] = Field( + default_factory=lambda: [], description="Supported boards" + ) + tool_dependencies: List[ToolDependency] = Field( + default_factory=lambda: [], + alias="toolsDependencies", + description="Required tool dependencies", + ) + help: Help = Field(description="Help and documentation links") + + @field_validator("version") + @classmethod + def validate_version_format(cls, v: str) -> str: + """Validate version format""" + if not v.strip(): + raise ValueError("Version cannot be empty") + return v.strip() + + @field_validator("size_mb", mode="before") + @classmethod + def convert_size_from_bytes(cls, v: Union[str, int, float]) -> float: + """Convert size from bytes to megabytes if needed""" + if isinstance(v, str): + try: + size_bytes = int(v) + return size_bytes / (1024 * 1024) + except ValueError: + raise ValueError(f"Invalid size format: {v}") + elif isinstance(v, (int, float)): + if v > 1024 * 1024: # Assume bytes if > 1MB + return v / (1024 * 1024) + return v # Already in MB + return v + + @field_validator("archive_filename") + @classmethod + def validate_archive_filename(cls, v: str) -> str: + """Validate archive filename format""" + valid_extensions = [".zip", ".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst"] + if not any(v.lower().endswith(ext) for ext in valid_extensions): + raise ValueError( + f"Archive must have one of these extensions: {valid_extensions}" + ) + return v + + class Config: + allow_population_by_field_name = True + schema_extra: Dict[str, Any] = { + "example": { + "name": "ESP32 Arduino", + "architecture": "esp32", + "version": "2.0.5", + "category": "ESP32", + "url": "https://github.com/espressif/arduino-esp32/releases/download/2.0.5/esp32-2.0.5.zip", + "archiveFileName": "esp32-2.0.5.zip", + "checksum": "SHA-256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "size": "50000000", + "boards": [], + "toolsDependencies": [], + } + } + + +class SystemDownload(BaseModel): + """System-specific download information for tools""" + + host: str = Field(min_length=1, description="Target host system identifier") + url: HttpUrl = Field(description="Download URL for this system") + archive_filename: str = Field( + alias="archiveFileName", description="Archive file name" + ) + checksum: str = Field( + pattern=r"^SHA-256:[a-fA-F0-9]{64}$", description="SHA-256 checksum" + ) + size_mb: float = Field(gt=0, alias="size", description="Archive size in megabytes") + + @field_validator("size_mb", mode="before") + @classmethod + def convert_size_from_bytes(cls, v: Union[str, int, float]) -> float: + """Convert size from bytes to megabytes if needed""" + if isinstance(v, str): + try: + size_bytes = int(v) + return size_bytes / (1024 * 1024) + except ValueError: + raise ValueError(f"Invalid size format: {v}") + elif isinstance(v, (int, float)): + if v > 1024 * 1024: # Assume bytes if > 1MB + return v / (1024 * 1024) + return v # Already in MB + return v + + @field_validator("host") + @classmethod + def validate_host_format(cls, v: str) -> str: + """Validate host system identifier""" + # Common host patterns: i686-pc-linux-gnu, x86_64-apple-darwin, etc. + if not v.strip(): + raise ValueError("Host identifier cannot be empty") + return v.strip() + + class Config: + allow_population_by_field_name = True + schema_extra = { + "example": { + "host": "x86_64-pc-linux-gnu", + "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-2021r2-patch5/xtensa-esp32-elf-gcc8_4_0-esp-2021r2-patch5-linux-amd64.tar.gz", + "archiveFileName": "xtensa-esp32-elf-gcc8_4_0-esp-2021r2-patch5-linux-amd64.tar.gz", + "checksum": "SHA-256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "size": "150000000", + } + } + + +class Tool(BaseModel): + """Tool with system-specific downloads and enhanced functionality""" + + name: str = Field(pattern=r"^[a-zA-Z0-9_.-]+$", description="Tool name") + version: str = Field(min_length=1, description="Tool version") + systems: List[SystemDownload] = Field( + min_length=1, description="System-specific downloads" + ) + + @field_validator("systems") + @classmethod + def validate_unique_systems(cls, v: List[SystemDownload]) -> List[SystemDownload]: + """Ensure no duplicate host systems""" + hosts = [system.host for system in v] + if len(hosts) != len(set(hosts)): + raise ValueError("Duplicate host systems found in tool downloads") + return v + + def get_system_download(self, host_pattern: str) -> Optional[SystemDownload]: + """Get system download matching host pattern""" + for system in self.systems: + if host_pattern in system.host: + return system + return None + + def get_compatible_systems(self) -> List[str]: + """Get list of compatible host systems""" + return [system.host for system in self.systems] + + class Config: + schema_extra = { + "example": { + "name": "xtensa-esp32-elf-gcc", + "version": "esp-2021r2-patch5-8.4.0", + "systems": [ + { + "host": "x86_64-pc-linux-gnu", + "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-2021r2-patch5/xtensa-esp32-elf-gcc8_4_0-esp-2021r2-patch5-linux-amd64.tar.gz", + "archiveFileName": "xtensa-esp32-elf-gcc8_4_0-esp-2021r2-patch5-linux-amd64.tar.gz", + "checksum": "SHA-256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "size": "150000000", + } + ], + } + } + + +class Package(BaseModel): + """Package containing platforms and tools with comprehensive validation""" + + name: str = Field(pattern=r"^[A-Za-z0-9_.-]+$", description="Package identifier") + maintainer: str = Field( + min_length=1, max_length=200, description="Package maintainer" + ) + website_url: HttpUrl = Field(alias="websiteURL", description="Package website URL") + email: EmailStr = Field(description="Maintainer contact email") + help: Help = Field(description="Help and documentation links") + platforms: List[Platform] = Field( + default_factory=lambda: [], description="Available platforms" + ) + tools: List[Tool] = Field(default_factory=lambda: [], description="Available tools") + + @field_validator("platforms") + @classmethod + def validate_unique_platforms(cls, v: List[Platform]) -> List[Platform]: + """Ensure no duplicate platform architecture/version combinations""" + seen: Set[tuple[str, str]] = set() + for platform in v: + key = (platform.architecture, platform.version) + if key in seen: + raise ValueError( + f"Duplicate platform found: {platform.architecture} v{platform.version}" + ) + seen.add(key) + return v + + @field_validator("tools") + @classmethod + def validate_unique_tools(cls, v: List[Tool]) -> List[Tool]: + """Ensure no duplicate tool name/version combinations""" + seen: Set[tuple[str, str]] = set() + for tool in v: + key = (tool.name, tool.version) + if key in seen: + raise ValueError(f"Duplicate tool found: {tool.name} v{tool.version}") + seen.add(key) + return v + + def find_platform( + self, architecture: str, version: Optional[str] = None + ) -> Optional[Platform]: + """Find platform by architecture and optionally version""" + for platform in self.platforms: + if platform.architecture == architecture: + if version is None or platform.version == version: + return platform + return None + + def find_tool(self, name: str, version: Optional[str] = None) -> Optional[Tool]: + """Find tool by name and optionally version""" + for tool in self.tools: + if tool.name == name: + if version is None or tool.version == version: + return tool + return None + + def get_latest_platform_version(self, architecture: str) -> Optional[str]: + """Get the latest version for a given architecture""" + versions = [p.version for p in self.platforms if p.architecture == architecture] + if not versions: + return None + # Simple version sorting - can be enhanced with proper semver parsing + return sorted(versions)[-1] + + class Config: + allow_population_by_field_name = True + schema_extra: Dict[str, Any] = { + "example": { + "name": "esp32", + "maintainer": "Espressif Systems", + "websiteURL": "https://github.com/espressif/arduino-esp32", + "email": "hr@espressif.com", + "help": {"online": "https://github.com/espressif/arduino-esp32"}, + "platforms": [], + "tools": [], + } + } + + +class PackageIndex(BaseModel): + """Root package index containing multiple packages""" + + packages: List[Package] = Field(min_length=1, description="Available packages") + + @field_validator("packages") + @classmethod + def validate_unique_packages(cls, v: List[Package]) -> List[Package]: + """Ensure no duplicate package names""" + names = [pkg.name for pkg in v] + if len(names) != len(set(names)): + raise ValueError("Duplicate package names found in index") + return v + + def find_package(self, name: str) -> Optional[Package]: + """Find package by name""" + for package in self.packages: + if package.name == name: + return package + return None + + def get_all_platforms(self) -> List[Platform]: + """Get all platforms from all packages""" + platforms: List[Platform] = [] + for package in self.packages: + platforms.extend(package.platforms) + return platforms + + def get_all_tools(self) -> List[Tool]: + """Get all tools from all packages""" + tools: List[Tool] = [] + for package in self.packages: + tools.extend(package.tools) + return tools + + +# Enhanced Parser with Validation and Error Handling + + +class PackageIndexParser: + """Enhanced parser with comprehensive validation and error handling""" + + def __init__(self, timeout: int = 30): + """Initialize parser with timeout configuration""" + self.timeout = timeout + + def parse_package_index(self, json_str: str) -> PackageIndex: + """Parse and validate package index JSON""" + try: + raw_data = json.loads(json_str) + return PackageIndex(**raw_data) + except ValidationError as e: + raise PackageParsingError(f"Invalid package index format: {e}") + except json.JSONDecodeError as e: + raise PackageParsingError(f"Invalid JSON format: {e}") + + def parse_from_url(self, url: str) -> PackageIndex: + """Fetch and parse package index from URL with validation""" + try: + print(f"Fetching package index from: {url}") + with urlopen(url, timeout=self.timeout) as response: + content = response.read() + json_str = content.decode("utf-8") + + return self.parse_package_index(json_str) + + except Exception as e: + raise PackageParsingError(f"Error fetching package index from {url}: {e}") + + async def parse_from_url_async(self, url: str) -> PackageIndex: + """Async version of URL parsing""" + try: + print(f"Fetching package index from: {url}") + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get(url) + response.raise_for_status() + json_str = response.text + + return self.parse_package_index(json_str) + + except Exception as e: + raise PackageParsingError(f"Error fetching package index from {url}: {e}") + + +# Package Manager Configuration + + +class PackageManagerConfig(BaseModel): + """Configuration for package manager with validation""" + + cache_dir: Path = Field(default_factory=lambda: Path.home() / ".arduino_packages") + sources: List[HttpUrl] = Field( + default_factory=lambda: [], description="Package index URLs" + ) + timeout: int = Field( + default=30, gt=0, le=300, description="Request timeout in seconds" + ) + max_retries: int = Field( + default=3, ge=0, le=10, description="Maximum retry attempts" + ) + verify_checksums: bool = Field( + default=True, description="Verify download checksums" + ) + allow_insecure: bool = Field(default=False, description="Allow insecure downloads") + + @field_validator("cache_dir") + @classmethod + def validate_cache_dir(cls, v: Path) -> Path: + """Ensure cache directory is valid""" + if v.exists() and not v.is_dir(): + raise ValueError(f"Cache path exists but is not a directory: {v}") + return v + + class Config: + validate_assignment = True + schema_extra = { + "example": { + "cache_dir": "~/.arduino_packages", + "sources": [ + "https://espressif.github.io/arduino-esp32/package_esp32_index.json" + ], + "timeout": 30, + "max_retries": 3, + "verify_checksums": True, + "allow_insecure": False, + } + } + + +# Utility Functions for Enhanced Functionality + + +def verify_checksum(file_path: Path, expected_checksum: str) -> bool: + """Verify file checksum against expected SHA-256 value""" + if not expected_checksum.startswith("SHA-256:"): + raise ValueError(f"Invalid checksum format: {expected_checksum}") + + expected_hash = expected_checksum[8:] # Remove 'SHA-256:' prefix + + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + + actual_hash = sha256_hash.hexdigest() + return actual_hash.lower() == expected_hash.lower() + + +def extract_archive(archive_path: Path, extract_to: Path) -> bool: + """Extract archive to specified directory with format detection""" + try: + archive_str = str(archive_path) + + if archive_path.suffix == ".zip": + with zipfile.ZipFile(archive_path, "r") as zip_ref: + zip_ref.extractall(extract_to) + elif archive_str.endswith((".tar.gz", ".tgz")): + with tarfile.open(archive_path, "r:gz") as tar_ref: + tar_ref.extractall(extract_to) + elif archive_str.endswith(".tar.bz2"): + with tarfile.open(archive_path, "r:bz2") as tar_ref: + tar_ref.extractall(extract_to) + elif archive_str.endswith(".tar.xz"): + with tarfile.open(archive_path, "r:xz") as tar_ref: + tar_ref.extractall(extract_to) + else: + raise ValueError(f"Unsupported archive format: {archive_path}") + + return True + + except Exception as e: + print(f"Error extracting archive {archive_path}: {e}") + return False + + +def format_size(size_mb: float) -> str: + """Format size in a human-readable way""" + if size_mb < 1: + return f"{size_mb * 1024:.1f} KB" + elif size_mb < 1024: + return f"{size_mb:.1f} MB" + else: + return f"{size_mb / 1024:.1f} GB" + + +# Display Functions with Enhanced Formatting + + +def display_package_info(package: Package) -> None: + """Display package information with enhanced formatting""" + print(f"\n📦 Package: {package.name}") + print(f"👤 Maintainer: {package.maintainer}") + print(f"🌐 Website: {package.website_url}") + print(f"📧 Email: {package.email}") + print(f"📚 Help: {package.help.online}") + print(f"🛠️ Platforms: {len(package.platforms)}") + print(f"🔧 Tools: {len(package.tools)}") + + # Show platform information + for i, platform in enumerate(package.platforms[:3]): # Show first 3 platforms + print(f"\n 📋 Platform {i + 1}: {platform.name} v{platform.version}") + print(f" Architecture: {platform.architecture}") + print(f" Category: {platform.category}") + print(f" Size: {format_size(platform.size_mb)}") + print(f" Archive: {platform.archive_filename}") + print(f" Checksum: {platform.checksum[:24]}...") + print(f" Help: {platform.help.online}") + print(f" Boards: {len(platform.boards)}") + + # Show first 5 boards + for board in platform.boards[:5]: + print(f" • {board.name}") + if len(platform.boards) > 5: + print(f" ... and {len(platform.boards) - 5} more") + + print(f" Tool Dependencies: {len(platform.tool_dependencies)}") + for dep in platform.tool_dependencies[:3]: # Show first 3 dependencies + print(f" • {dep.name} v{dep.version} ({dep.packager})") + if len(platform.tool_dependencies) > 3: + print(f" ... and {len(platform.tool_dependencies) - 3} more") + + if len(package.platforms) > 3: + print(f"\n ... and {len(package.platforms) - 3} more platforms") + + +def display_validation_summary(package_index: PackageIndex) -> None: + """Display validation summary for the package index""" + print(f"\n✅ VALIDATION SUMMARY") + print(f" 📦 Total packages: {len(package_index.packages)}") + + total_platforms = sum(len(pkg.platforms) for pkg in package_index.packages) + total_tools = sum(len(pkg.tools) for pkg in package_index.packages) + total_boards = sum( + len(platform.boards) + for pkg in package_index.packages + for platform in pkg.platforms + ) + + print(f" 🛠️ Total platforms: {total_platforms}") + print(f" 🔧 Total tools: {total_tools}") + print(f" 💾 Total boards: {total_boards}") + + # Show architectures + architectures: Set[str] = set() + for pkg in package_index.packages: + for platform in pkg.platforms: + architectures.add(platform.architecture) + + print(f" 🏗️ Architectures: {', '.join(sorted(architectures))}") + + +# Demonstration Functions + + +def demo_esp32_parsing() -> Optional[PackageIndex]: + """Demonstrate parsing ESP32 package index with enhanced validation""" + ESP32_URL = "https://espressif.github.io/arduino-esp32/package_esp32_index.json" + + try: + parser = PackageIndexParser(timeout=30) + package_index = parser.parse_from_url(ESP32_URL) + + print("🎉 Successfully parsed ESP32 package index with Pydantic validation!") + display_validation_summary(package_index) + + # Display first package + if package_index.packages: + display_package_info(package_index.packages[0]) + + return package_index + + except PackageParsingError as e: + print(f"❌ Package parsing error: {e}") + sys.exit(1) + except Exception as e: + print(f"❌ Unexpected error: {e}") + sys.exit(1) + + +def demo_model_validation() -> None: + """Demonstrate Pydantic model validation capabilities""" + print("\n🧪 TESTING PYDANTIC MODEL VALIDATION") + + # Define valid platform data + valid_platform_data: Dict[str, Any] = { + "name": "ESP32 Arduino", + "architecture": "esp32", + "version": "2.0.5", + "category": "ESP32", + "url": "https://github.com/espressif/arduino-esp32/releases/download/2.0.5/esp32-2.0.5.zip", + "archiveFileName": "esp32-2.0.5.zip", + "checksum": "SHA-256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "size": "50000000", # Should convert to MB + "boards": [], + "toolsDependencies": [], + "help": {"online": "https://github.com/espressif/arduino-esp32"}, + } + + # Test valid platform + try: + platform = Platform(**valid_platform_data) + print(f"✅ Valid platform created: {platform.name} v{platform.version}") + print(f" Size converted: {platform.size_mb:.1f} MB") + + except ValidationError as e: + print(f"❌ Unexpected validation error: {e}") + + # Test invalid platform + try: + invalid_platform_data = valid_platform_data.copy() + invalid_platform_data["checksum"] = "invalid-checksum-format" + + platform = Platform(**invalid_platform_data) + print("❌ Should have failed validation!") + + except ValidationError as e: + print(f"✅ Correctly caught invalid checksum: {e.errors()[0]['msg']}") + + # Test invalid URL + try: + invalid_url_data = valid_platform_data.copy() + invalid_url_data["url"] = "not-a-valid-url" + + platform = Platform(**invalid_url_data) + print("❌ Should have failed URL validation!") + + except ValidationError as e: + print(f"✅ Correctly caught invalid URL: {e.errors()[0]['msg']}") + + +def main() -> None: + """Main function demonstrating enhanced package index functionality""" + print("🚀 ENHANCED ARDUINO PACKAGE INDEX WITH PYDANTIC") + print("=" * 60) + + try: + # Demo model validation + demo_model_validation() + + # Demo ESP32 parsing + package_index = demo_esp32_parsing() + + # Interactive options + try: + print(f"\n📋 AVAILABLE OPTIONS:") + print("1. Show detailed tools information") + print("2. Search for specific architecture") + print("3. Exit") + + choice = input("\nEnter your choice (1-3): ").strip() + + if choice == "1" and package_index and package_index.packages: + # Simple tools info display (without importing original) + pkg = package_index.packages[0] + print(f"\n🔧 TOOLS INFORMATION for {pkg.name}") + print(f"Total tools: {len(pkg.tools)}") + for tool in pkg.tools[:3]: # Show first 3 tools + print( + f" • {tool.name} v{tool.version} ({len(tool.systems)} systems)" + ) + if len(pkg.tools) > 3: + print(f" ... and {len(pkg.tools) - 3} more tools") + + elif choice == "2" and package_index: + arch = input("Enter architecture to search for: ").strip() + platforms = [ + p + for pkg in package_index.packages + for p in pkg.platforms + if p.architecture == arch + ] + if platforms: + print( + f"\n🔍 Found {len(platforms)} platforms for architecture '{arch}':" + ) + for platform in platforms: + print( + f" • {platform.name} v{platform.version} ({format_size(platform.size_mb)})" + ) + else: + print(f"❌ No platforms found for architecture '{arch}'") + else: + print("👋 Goodbye!") + + except KeyboardInterrupt: + print("\n👋 Interrupted by user") + + except KeyboardInterrupt: + print("\n👋 Interrupted by user") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/compiler/pio.py b/v1.5/lib/FastLED/ci/compiler/pio.py new file mode 100644 index 0000000..fc8cb0e --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/pio.py @@ -0,0 +1,1950 @@ +#!/usr/bin/env python3 +""" +PlatformIO Builder for FastLED + +Provides a clean interface for building FastLED projects with PlatformIO. +""" + +import json +import os +import platform +import re +import shutil +import subprocess +import threading +import time +import urllib.request +import warnings +from concurrent.futures import Future, ThreadPoolExecutor +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Dict + +import fasteners +from dirsync import sync # type: ignore + +from ci.boards import ALL, Board, create_board +from ci.compiler.compiler import CacheType, Compiler, InitResult, SketchResult +from ci.util.create_build_dir import insert_tool_aliases +from ci.util.output_formatter import create_sketch_path_formatter +from ci.util.running_process import EndOfStream, RunningProcess + + +_HERE = Path(__file__).parent.resolve() +_PROJECT_ROOT = _HERE.parent.parent.resolve() + + +assert (_PROJECT_ROOT / "library.json").exists(), ( + f"Library JSON not found at {_PROJECT_ROOT / 'library.json'}" +) + + +def _ensure_platform_installed(board: Board) -> bool: + """Ensure the required platform is installed for the board.""" + if not board.platform_needs_install: + return True + + # Platform installation is handled by existing platform management code + # This is a placeholder for future platform installation logic + print(f"Platform installation needed for {board.board_name}: {board.platform}") + return True + + +def _generate_build_info_json_from_existing_build( + build_dir: Path, board: Board +) -> bool: + """Generate build_info.json from an existing PlatformIO build. + + Args: + build_dir: Build directory containing the PlatformIO project + board: Board configuration + + Returns: + True if build_info.json was successfully generated + """ + try: + # Use existing project to get metadata (no temporary project needed) + metadata_cmd = ["pio", "project", "metadata", "--json-output"] + metadata_result = subprocess.run( + metadata_cmd, + capture_output=True, + text=True, + cwd=build_dir, + timeout=60, + ) + + if metadata_result.returncode != 0: + print( + f"Warning: Failed to get metadata for build_info.json: {metadata_result.stderr}" + ) + return False + + # Parse and save the metadata + try: + data = json.loads(metadata_result.stdout) + + # Add tool aliases for symbol analysis and debugging + insert_tool_aliases(data) + + # Save to build_info.json + build_info_path = build_dir / "build_info.json" + with open(build_info_path, "w") as f: + json.dump(data, f, indent=4, sort_keys=True) + + print(f"✅ Generated build_info.json at {build_info_path}") + return True + + except json.JSONDecodeError as e: + print(f"Warning: Failed to parse metadata JSON for build_info.json: {e}") + return False + + except subprocess.TimeoutExpired: + print(f"Warning: Timeout generating build_info.json") + return False + except Exception as e: + print(f"Warning: Exception generating build_info.json: {e}") + return False + + +def _apply_board_specific_config( + board: Board, + platformio_ini_path: Path, + example: str, + paths: "FastLEDPaths", + additional_defines: list[str] | None = None, + additional_include_dirs: list[str] | None = None, + additional_libs: list[str] | None = None, + cache_type: CacheType = CacheType.NO_CACHE, +) -> bool: + """Apply board-specific build configuration from Board class.""" + # Use provided paths object (which may have overrides) + paths.ensure_directories_exist() + + # Generate platformio.ini content using the enhanced Board method + config_content = board.to_platformio_ini( + additional_defines=additional_defines, + additional_include_dirs=additional_include_dirs, + additional_libs=additional_libs, + include_platformio_section=True, + core_dir=str(paths.core_dir), + packages_dir=str(paths.packages_dir), + project_root=str(_PROJECT_ROOT), + build_cache_dir=str(paths.build_cache_dir), + extra_scripts=["post:cache_setup.scons"] + if cache_type != CacheType.NO_CACHE + else None, + ) + + # Apply PlatformIO cache optimization to speed up builds + try: + from ci.compiler.platformio_cache import PlatformIOCache + from ci.compiler.platformio_ini import PlatformIOIni + + # Parse the generated INI content + pio_ini = PlatformIOIni.parseString(config_content) + + # Set up global PlatformIO cache + cache = PlatformIOCache(paths.global_platformio_cache_dir) + + # Optimize by downloading and caching packages, replacing URLs with local file:// paths + pio_ini.optimize(cache) + + # Use the optimized content + config_content = str(pio_ini) + print( + f"Applied PlatformIO cache optimization using cache directory: {paths.global_platformio_cache_dir}" + ) + + except Exception as e: + # Graceful fallback to original URLs on cache failures + print( + f"Warning: PlatformIO cache optimization failed, using original URLs: {e}" + ) + # config_content remains unchanged (original URLs) + + platformio_ini_path.write_text(config_content) + + # Log applied configurations for debugging + if board.build_flags: + print(f"Applied build_flags: {board.build_flags}") + if board.defines: + print(f"Applied defines: {board.defines}") + if additional_defines: + print(f"Applied additional defines: {additional_defines}") + if additional_include_dirs: + print(f"Applied additional include dirs: {additional_include_dirs}") + if board.platform_packages: + print(f"Using platform_packages: {board.platform_packages}") + + return True + + +def _setup_ccache_environment(board_name: str) -> bool: + """Set up ccache environment variables for the current process.""" + import shutil + + # Check if ccache is available + ccache_path = shutil.which("ccache") + if not ccache_path: + print("CCACHE not found in PATH, compilation will proceed without caching") + return False + + print(f"Setting up CCACHE environment: {ccache_path}") + + # Set up ccache directory in the global .fastled directory + # Shared across all boards for maximum cache efficiency + paths = FastLEDPaths(board_name) + ccache_dir = paths.fastled_root / "ccache" + ccache_dir.mkdir(parents=True, exist_ok=True) + + # Configure ccache environment variables + os.environ["CCACHE_DIR"] = str(ccache_dir) + os.environ["CCACHE_MAXSIZE"] = "2G" + + print(f"CCACHE cache directory: {ccache_dir}") + + # Set compiler wrapper environment variables that PlatformIO will use + # PlatformIO respects these environment variables for compiler selection + original_cc = os.environ.get("CC", "") + original_cxx = os.environ.get("CXX", "") + + # Only wrap if not already wrapped + if "ccache" not in original_cc: + # Set environment variables that PlatformIO/SCons will use + os.environ["CC"] = ( + f'"{ccache_path}" {original_cc}' if original_cc else f'"{ccache_path}" gcc' + ) + os.environ["CXX"] = ( + f'"{ccache_path}" {original_cxx}' + if original_cxx + else f'"{ccache_path}" g++' + ) + + print(f"Set CC environment variable: {os.environ['CC']}") + print(f"Set CXX environment variable: {os.environ['CXX']}") + + print(f"CCACHE cache directory: {ccache_dir}") + print(f"CCACHE cache size limit: 2G") + + # Show ccache statistics if available + try: + import subprocess + + result = subprocess.run( + [ccache_path, "--show-stats"], capture_output=True, text=True, check=False + ) + if result.returncode == 0: + print("CCACHE Statistics:") + for line in result.stdout.strip().split("\n"): + if line.strip(): + print(f" {line}") + else: + print("CCACHE stats not available (cache empty or first run)") + except Exception as e: + print(f"Could not retrieve CCACHE stats: {e}") + + return True + + +def _copy_cache_build_script(build_dir: Path, cache_config: dict[str, str]) -> None: + """Copy the standalone cache setup script and set environment variables for configuration.""" + import shutil + + # Source script location + project_root = _resolve_project_root() + source_script = project_root / "ci" / "compiler" / "cache_setup.scons" + dest_script = build_dir / "cache_setup.scons" + + # Copy the standalone script + if not source_script.exists(): + raise RuntimeError(f"Cache setup script not found: {source_script}") + + shutil.copy2(source_script, dest_script) + print(f"Copied cache setup script: {source_script} -> {dest_script}") + + # Set environment variables for cache configuration + # These will be read by the cache_setup.scons script + cache_type = cache_config.get("CACHE_TYPE", "sccache") + + os.environ["FASTLED_CACHE_TYPE"] = cache_type + os.environ["FASTLED_SCCACHE_DIR"] = cache_config.get("SCCACHE_DIR", "") + os.environ["FASTLED_SCCACHE_CACHE_SIZE"] = cache_config.get( + "SCCACHE_CACHE_SIZE", "2G" + ) + os.environ["FASTLED_CACHE_DEBUG"] = ( + "1" if os.environ.get("XCACHE_DEBUG") == "1" else "0" + ) + + if cache_type == "xcache": + os.environ["FASTLED_CACHE_EXECUTABLE"] = cache_config.get( + "CACHE_EXECUTABLE", "" + ) + os.environ["FASTLED_SCCACHE_PATH"] = cache_config.get("SCCACHE_PATH", "") + os.environ["FASTLED_XCACHE_PATH"] = cache_config.get("XCACHE_PATH", "") + elif cache_type == "sccache": + os.environ["FASTLED_CACHE_EXECUTABLE"] = cache_config.get("SCCACHE_PATH", "") + os.environ["FASTLED_SCCACHE_PATH"] = cache_config.get("SCCACHE_PATH", "") + else: + os.environ["FASTLED_CACHE_EXECUTABLE"] = cache_config.get("CCACHE_PATH", "") + + print(f"Set cache environment variables for {cache_type} configuration") + + +def _find_platform_path_from_board( + board: "Board", paths: "FastLEDPaths" +) -> Path | None: + """Find the platform path from board's platform URL using cache directory naming.""" + from ci.boards import Board + from ci.util.url_utils import sanitize_url_for_path + + if not board.platform: + print(f"No platform URL defined for board {board.board_name}") + return None + + print(f"Looking for platform cache: {board.platform}") + + # Convert platform URL to expected cache directory name + expected_cache_name = sanitize_url_for_path(board.platform) + print(f"Expected cache directory: {expected_cache_name}") + + # Search in global cache directory + cache_dir = paths.global_platformio_cache_dir + expected_cache_path = cache_dir / expected_cache_name / "extracted" + + if ( + expected_cache_path.exists() + and (expected_cache_path / "platform.json").exists() + ): + print(f"Found platform cache: {expected_cache_path}") + return expected_cache_path + + # Fallback: search for any directory that contains the platform name + # Extract platform name from URL (e.g., "platform-espressif32" from github URL) + platform_name = None + if "platform-" in board.platform: + # Extract platform name from URL path + parts = board.platform.split("/") + for part in parts: + if ( + "platform-" in part + and not part.endswith(".git") + and not part.endswith(".zip") + ): + platform_name = part + break + + if platform_name: + print(f"Searching for platform by name: {platform_name}") + for cache_item in cache_dir.glob(f"*{platform_name}*"): + extracted_path = cache_item / "extracted" + if extracted_path.exists() and (extracted_path / "platform.json").exists(): + print(f"Found platform by name search: {extracted_path}") + return extracted_path + + print(f"Platform cache not found for {board.board_name}") + return None + + +def get_platform_required_packages(platform_path: Path) -> list[str]: + """Extract required package names from platform.json.""" + import json + + try: + platform_json = platform_path / "platform.json" + if not platform_json.exists(): + return [] + + with open(platform_json, "r", encoding="utf-8") as f: + data = json.load(f) + + packages = data.get("packages", {}) + # Return all package names from the platform + return list(packages.keys()) + except Exception as e: + print(f"Warning: Could not parse platform.json: {e}") + return [] + + +def get_installed_packages_from_pio() -> Dict[str, str]: + """Get installed packages using PlatformIO CLI.""" + import re + import subprocess + + try: + result = subprocess.run( + ["pio", "pkg", "list", "--global"], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + + if result.returncode != 0: + print(f"Warning: pio pkg list failed: {result.stderr}") + return {} + + packages: Dict[str, str] = {} + # Parse output like: "├── framework-arduinoespressif32-libs @ 5.3.0+sha.083aad99cf" + for line in result.stdout.split("\n"): + match = re.search(r"[├└]── ([^@\s]+)\s*@\s*([^\s]+)", line) + if match: + package_name, version = match.groups() + packages[package_name] = version + + return packages + except Exception as e: + print(f"Warning: Could not get installed packages: {e}") + return {} + + +def detect_and_fix_corrupted_packages_dynamic( + paths: "FastLEDPaths", board_name: str, platform_path: Path | None = None +) -> Dict[str, bool]: + """Dynamically detect and fix corrupted packages based on platform requirements.""" + import shutil + + print("=== Dynamic Package Corruption Detection & Fix ===") + print(f"Board: {board_name}") + print(f"Packages dir: {paths.packages_dir}") + + results: Dict[str, bool] = {} + + # Get required packages from platform.json if available + platform_packages = [] + if platform_path and platform_path.exists(): + platform_packages = get_platform_required_packages(platform_path) + print(f"Platform packages found: {len(platform_packages)}") + if platform_packages: + print( + f" Required packages: {', '.join(platform_packages[:5])}{'...' if len(platform_packages) > 5 else ''}" + ) + + # Get installed packages from PIO CLI + installed_packages = get_installed_packages_from_pio() + print(f"Installed packages found: {len(installed_packages)}") + + # If we have platform info, focus on those packages, otherwise scan all installed + packages_to_check = [] + if platform_packages: + # Check intersection of platform requirements and installed packages + packages_to_check = [ + pkg for pkg in platform_packages if pkg in installed_packages + ] + print( + f"Checking {len(packages_to_check)} packages that are both required and installed" + ) + else: + # Fallback: check all installed packages that look like frameworks + packages_to_check = [ + pkg + for pkg in installed_packages.keys() + if "framework" in pkg.lower() or "toolchain" in pkg.lower() + ] + print( + f"Fallback: Checking {len(packages_to_check)} framework/toolchain packages" + ) + + if not packages_to_check: + print("No packages to check - using fallback hardcoded list") + packages_to_check = ["framework-arduinoespressif32-libs"] + + # Check each package for corruption + for package_name in packages_to_check: + print(f"Checking package: {package_name}") + package_path = paths.packages_dir / package_name + print(f" Package path: {package_path}") + + exists = package_path.exists() + piopm_exists = (package_path / ".piopm").exists() if exists else False + manifest_exists = (package_path / "package.json").exists() if exists else False + + print(f" Package exists: {exists}") + print(f" .piopm exists: {piopm_exists}") + print(f" package.json exists: {manifest_exists}") + + is_corrupted = exists and piopm_exists and not manifest_exists + if is_corrupted: + print(f" -> CORRUPTED: Has .piopm but missing package.json") + print(f" -> FIXING: Removing corrupted package...") + try: + # Safe deletion with lock already held by caller + shutil.rmtree(package_path) + print(f" -> SUCCESS: Removed {package_name}") + print(f" -> PlatformIO will re-download package automatically") + results[package_name] = True # Was corrupted, now fixed + except Exception as e: + print(f" -> ERROR: Failed to remove {package_name}: {e}") + results[package_name] = False # Still corrupted + else: + print(f" -> OK: Not corrupted") + results[package_name] = False # Not corrupted + + print("=== Dynamic Detection & Fix Complete ===") + return results + + +class FastLEDPaths: + """Centralized path management for FastLED board-specific directories and files.""" + + def __init__(self, board_name: str, project_root: Path | None = None) -> None: + self.board_name = board_name + self.project_root = project_root or _resolve_project_root() + self.home_dir = Path.home() + + # Base FastLED directory + self.fastled_root = self.home_dir / ".fastled" + # Initialize the optional cache directory override + self._global_platformio_cache_dir: Path | None = None + + @property + def build_dir(self) -> Path: + """Project-local build directory for this board.""" + return self.project_root / ".build" / "pio" / self.board_name + + @property + def build_cache_dir(self) -> Path: + """Project-local build cache directory for this board.""" + return self.build_dir / "build_cache" + + @property + def platform_lock_file(self) -> Path: + """Platform-specific build lock file.""" + return self.build_dir.parent / f"{self.board_name}.lock" + + @property + def global_package_lock_file(self) -> Path: + """Global package installation lock file.""" + packages_lock_root = self.fastled_root / "pio" / "packages" + return packages_lock_root / f"{self.board_name}_global.lock" + + @property + def core_dir(self) -> Path: + """PlatformIO core directory (build cache, platforms).""" + return self.fastled_root / "compile" / "pio" / self.board_name + + @property + def packages_dir(self) -> Path: + """PlatformIO packages directory (toolchains, frameworks).""" + return self.home_dir / ".platformio" / "packages" + + @property + def global_platformio_cache_dir(self) -> Path: + """Global PlatformIO package cache directory (shared across all boards).""" + if self._global_platformio_cache_dir is not None: + return self._global_platformio_cache_dir + return self.fastled_root / "platformio_cache" + + def ensure_directories_exist(self) -> None: + """Create all necessary directories.""" + self.build_dir.mkdir(parents=True, exist_ok=True) + self.global_package_lock_file.parent.mkdir(parents=True, exist_ok=True) + self.core_dir.mkdir(parents=True, exist_ok=True) + self.packages_dir.mkdir(parents=True, exist_ok=True) + self.global_platformio_cache_dir.mkdir(parents=True, exist_ok=True) + + +class GlobalPackageLock: + """A process lock for global package installation per board. Acquired once during first build and released after completion.""" + + def __init__(self, platform_name: str) -> None: + self.platform_name = platform_name + + # Use centralized path management + self.paths = FastLEDPaths(platform_name) + self.lock_file = self.paths.global_package_lock_file + self._file_lock = fasteners.InterProcessLock(str(self.lock_file)) + self._is_acquired = False + + def acquire(self) -> None: + """Acquire the global package installation lock for this board.""" + if self._is_acquired: + return # Already acquired + + start_time = time.time() + warning_shown = False + + while True: + # Try to acquire with very short timeout (non-blocking) + try: + success = self._file_lock.acquire(blocking=True, timeout=0.1) + if success: + self._is_acquired = True + print( + f"Acquired global package lock for platform {self.platform_name}" + ) + return + except Exception: + # Handle timeout or other exceptions as failed acquisition (continue loop) + pass # Continue the loop to check elapsed time and try again + + # Check if we should show warning (after 1 second) + elapsed = time.time() - start_time + if not warning_shown and elapsed >= 1.0: + yellow = "\033[33m" + reset = "\033[0m" + print( + f"{yellow}Platform {self.platform_name} is waiting to acquire global package lock at {self.lock_file.parent}{reset}" + ) + warning_shown = True + + # Check for timeout (after 10 seconds - longer for package installation) + if elapsed >= 10.0: + raise TimeoutError( + f"Failed to acquire global package lock for platform {self.platform_name} within 10 seconds. " + f"Lock file: {self.lock_file}. " + f"This may indicate another process is installing packages or a deadlock occurred." + ) + + # Small sleep to prevent excessive CPU usage while allowing interrupts + time.sleep(0.1) + + def release(self) -> None: + """Release the global package installation lock.""" + if not self._is_acquired: + return # Not acquired + + try: + self._file_lock.release() + self._is_acquired = False + print(f"Released global package lock for platform {self.platform_name}") + except Exception as e: + warnings.warn( + f"Failed to release global package lock for {self.platform_name}: {e}" + ) + + def is_acquired(self) -> bool: + """Check if the lock is currently acquired.""" + return self._is_acquired + + +# Remove duplicate dataclass definitions - use the ones from compiler.py + + +def _resolve_project_root() -> Path: + """Resolve the FastLED project root directory.""" + current = Path( + __file__ + ).parent.parent.parent.resolve() # Go up from ci/compiler/pio.py + while current != current.parent: + if (current / "src" / "FastLED.h").exists(): + return current + current = current.parent + raise RuntimeError("Could not find FastLED project root") + + +def _create_building_banner(example: str) -> str: + """Create a building banner for the given example.""" + banner_text = f"BUILDING {example}" + border_char = "=" + padding = 2 + text_width = len(banner_text) + total_width = text_width + (padding * 2) + + top_border = border_char * (total_width + 4) + middle_line = ( + f"{border_char} {' ' * padding}{banner_text}{' ' * padding} {border_char}" + ) + bottom_border = border_char * (total_width + 4) + + banner = f"{top_border}\n{middle_line}\n{bottom_border}" + + # Apply blue color using ANSI escape codes + blue_color = "\033[34m" + reset_color = "\033[0m" + return f"{blue_color}{banner}{reset_color}" + + +def _get_example_error_message(project_root: Path, example: str) -> str: + """Generate appropriate error message for missing example. + + Args: + project_root: FastLED project root directory + example: Example name or path that was not found + + Returns: + Error message describing where the example was expected + """ + example_path = Path(example) + + if example_path.is_absolute(): + return f"Example directory not found: {example}" + elif "/" in example or "\\" in example: + return f"Example directory not found: {example_path.resolve()}" + else: + return f"Example not found: {project_root / 'examples' / example}" + + +def _copy_example_source(project_root: Path, build_dir: Path, example: str) -> bool: + """Copy example source to the build directory with sketch subdirectory structure. + + Args: + project_root: FastLED project root directory + build_dir: Build directory for the target + example: Name of the example to copy, or path to example directory + + Returns: + True if successful, False if example not found + """ + # Configure example source - handle both names and paths + example_path = Path(example) + + if example_path.is_absolute(): + # Absolute path - use as-is + if not example_path.exists(): + return False + elif "/" in example or "\\" in example: + # Relative path - resolve relative to current directory + example_path = example_path.resolve() + if not example_path.exists(): + return False + else: + # Just a name - resolve to examples directory + example_path = project_root / "examples" / example + if not example_path.exists(): + return False + + # Create src and sketch directories (PlatformIO requirement with sketch subdirectory) + src_dir = build_dir / "src" + sketch_dir = src_dir / "sketch" + + # Create directories if they don't exist, but don't remove existing src_dir + src_dir.mkdir(exist_ok=True) + + # Clean and recreate sketch subdirectory for fresh .ino files + if sketch_dir.exists(): + shutil.rmtree(sketch_dir) + sketch_dir.mkdir(parents=True, exist_ok=True) + + # Copy all files and subdirectories from example directory to sketch subdirectory + ino_files: list[str] = [] + for file_path in example_path.iterdir(): + if "fastled_js" in str(file_path): + # skip fastled_js output folder. + continue + + if file_path.is_file(): + shutil.copy2(file_path, sketch_dir) + # Calculate relative paths for cleaner output + try: + rel_source = file_path.relative_to(Path.cwd()) + rel_dest = sketch_dir.relative_to(Path.cwd()) + print(f"Copied {rel_source} to {rel_dest}") + except ValueError: + # Fallback to absolute paths if relative calculation fails + print(f"Copied {file_path} to {sketch_dir}") + if file_path.suffix == ".ino": + ino_files.append(file_path.name) + elif file_path.is_dir(): + # Recursively sync subdirectories for better caching + dest_subdir = sketch_dir / file_path.name + dest_subdir.mkdir(parents=True, exist_ok=True) + sync(str(file_path), str(dest_subdir), "sync", purge=True) + try: + rel_source = file_path.relative_to(Path.cwd()) + rel_dest = dest_subdir.relative_to(Path.cwd()) + print(f"Synced directory {rel_source} to {rel_dest}") + except ValueError: + print(f"Synced directory {file_path} to {dest_subdir}") + + # espidf builds create the CMakeLists.txt automatically if not present + # need to delete the old file to ensure that all folders are included in the new file + + oldCMakelist = ".build/pio/esp32c2/src/CMakeLists.txt" + if os.path.exists(oldCMakelist): + os.remove(oldCMakelist) + print(f"Removed old CMakeList.txt: {oldCMakelist}") + + # Create or update stub main.cpp that includes the .ino files + main_cpp_content = _generate_main_cpp(ino_files) + main_cpp_path = src_dir / "main.cpp" + + # Only write main.cpp if content has changed to avoid triggering rebuilds + should_write = True + if main_cpp_path.exists(): + try: + existing_content = main_cpp_path.read_text(encoding="utf-8") + should_write = existing_content != main_cpp_content + except (OSError, UnicodeDecodeError): + # If we can't read the existing file, write new content + should_write = True + + if should_write: + main_cpp_path.write_text(main_cpp_content, encoding="utf-8") + + return True + + +def _generate_main_cpp(ino_files: list[str]) -> str: + """Generate stub main.cpp content that includes .ino files from sketch directory. + + Args: + ino_files: List of .ino filenames to include + + Returns: + Content for main.cpp file + """ + includes: list[str] = [] + for ino_file in sorted(ino_files): + includes.append(f"#include ") + includes.append(f'#include "sketch/{ino_file}"') + + include_lines = "\n".join(includes) + + int_main = """ +__attribute__((weak)) int main() {{ + setup(); + while (true) {{ + loop(); + }} +}} +""" + + main_cpp_content = f"""// Auto-generated main.cpp stub for PlatformIO +// This file includes all .ino files from the sketch directory + +{include_lines} + +// main.cpp is required by PlatformIO but Arduino-style sketches +// use setup() and loop() functions which are called automatically +// by the FastLED/Arduino framework +// +// +{int_main} +""" + return main_cpp_content + + +def _copy_boards_directory(project_root: Path, build_dir: Path) -> bool: + """Copy boards directory to the build directory.""" + boards_src = project_root / "ci" / "boards" + boards_dst = build_dir / "boards" + + if not boards_src.exists(): + warnings.warn(f"Boards directory not found: {boards_src}") + return False + + try: + # Ensure target directory exists for dirsync + boards_dst.mkdir(parents=True, exist_ok=True) + + # Use sync for better caching - purge=True removes extra files + sync(str(boards_src), str(boards_dst), "sync", purge=True) + except Exception as e: + warnings.warn(f"Failed to sync boards directory: {e}") + return False + + return True + + +def _get_cache_build_flags(board_name: str, cache_type: CacheType) -> dict[str, str]: + """Get environment variables for compiler cache configuration.""" + if cache_type == CacheType.NO_CACHE: + print("No compiler cache configured") + return {} + elif cache_type == CacheType.SCCACHE: + return _get_sccache_build_flags(board_name) + elif cache_type == CacheType.CCACHE: + return _get_ccache_build_flags(board_name) + else: + print(f"Unknown cache type: {cache_type}") + return {} + + +def _get_sccache_build_flags(board_name: str) -> dict[str, str]: + """Get build flags for SCCACHE configuration with xcache wrapper support.""" + import shutil + from pathlib import Path + + # Check if sccache is available + sccache_path = shutil.which("sccache") + if not sccache_path: + print("SCCACHE not found in PATH, compilation will proceed without caching") + return {} + + print(f"Setting up SCCACHE build flags: {sccache_path}") + + # Set up sccache directory in the global .fastled directory + # Shared across all boards for maximum cache efficiency + paths = FastLEDPaths(board_name) + sccache_dir = paths.fastled_root / "sccache" + sccache_dir.mkdir(parents=True, exist_ok=True) + + print(f"SCCACHE cache directory: {sccache_dir}") + print(f"SCCACHE cache size limit: 2G") + + # Get xcache wrapper path + project_root = _resolve_project_root() + xcache_path = project_root / "ci" / "util" / "xcache.py" + + if xcache_path.exists(): + print(f"Using xcache wrapper for ESP32S3 response file support: {xcache_path}") + cache_type = "xcache" + cache_executable_path = f"python {xcache_path}" + else: + print(f"xcache not found at {xcache_path}, using direct sccache") + cache_type = "sccache" + cache_executable_path = sccache_path + + # Return the cache configuration + config = { + "CACHE_TYPE": cache_type, + "SCCACHE_DIR": str(sccache_dir), + "SCCACHE_CACHE_SIZE": "2G", + "SCCACHE_PATH": sccache_path, + "XCACHE_PATH": str(xcache_path) if xcache_path.exists() else "", + "CACHE_EXECUTABLE": cache_executable_path, + } + + return config + + +def _get_ccache_build_flags(board_name: str) -> dict[str, str]: + """Get environment variables for CCACHE configuration.""" + import shutil + + # Check if ccache is available + ccache_path = shutil.which("ccache") + if not ccache_path: + print("CCACHE not found in PATH, compilation will proceed without caching") + return {} + + print(f"Setting up CCACHE build environment: {ccache_path}") + + # Set up ccache directory in the global .fastled directory + # Shared across all boards for maximum cache efficiency + paths = FastLEDPaths(board_name) + ccache_dir = paths.fastled_root / "ccache" + ccache_dir.mkdir(parents=True, exist_ok=True) + + print(f"CCACHE cache directory: {ccache_dir}") + print(f"CCACHE cache size limit: 2G") + + # Return environment variables that PlatformIO will use + env_vars = { + "CACHE_TYPE": "ccache", + "CCACHE_DIR": str(ccache_dir), + "CCACHE_MAXSIZE": "2G", + "CCACHE_PATH": ccache_path, + } + + return env_vars + + +def _setup_sccache_environment(board_name: str) -> bool: + """Set up sccache environment variables for the current process.""" + import shutil + + # Check if sccache is available + sccache_path = shutil.which("sccache") + if not sccache_path: + print("SCCACHE not found in PATH, compilation will proceed without caching") + return False + + print(f"Setting up SCCACHE environment: {sccache_path}") + + # Set up sccache directory in the global .fastled directory + # Shared across all boards for maximum cache efficiency + paths = FastLEDPaths(board_name) + sccache_dir = paths.fastled_root / "sccache" + sccache_dir.mkdir(parents=True, exist_ok=True) + + # Configure sccache environment variables + os.environ["SCCACHE_DIR"] = str(sccache_dir) + os.environ["SCCACHE_CACHE_SIZE"] = "2G" + + print(f"SCCACHE cache directory: {sccache_dir}") + + # Set compiler wrapper environment variables that PlatformIO will use + # PlatformIO respects these environment variables for compiler selection + original_cc = os.environ.get("CC", "") + original_cxx = os.environ.get("CXX", "") + + # Only wrap if not already wrapped + if "sccache" not in original_cc: + # Set environment variables that PlatformIO/SCons will use + os.environ["CC"] = ( + f'"{sccache_path}" {original_cc}' + if original_cc + else f'"{sccache_path}" gcc' + ) + os.environ["CXX"] = ( + f'"{sccache_path}" {original_cxx}' + if original_cxx + else f'"{sccache_path}" g++' + ) + + print(f"Set CC environment variable: {os.environ['CC']}") + print(f"Set CXX environment variable: {os.environ['CXX']}") + + print(f"SCCACHE cache directory: {sccache_dir}") + print(f"SCCACHE cache size limit: 2G") + + # Show sccache statistics if available + try: + import subprocess + + result = subprocess.run( + [sccache_path, "--show-stats"], capture_output=True, text=True, check=False + ) + if result.returncode == 0: + print("SCCACHE Statistics:") + for line in result.stdout.strip().split("\n"): + if line.strip(): + print(f" {line}") + else: + print("SCCACHE stats not available (cache empty or first run)") + except Exception as e: + print(f"Could not retrieve SCCACHE stats: {e}") + + return True + + +def _copy_fastled_library(project_root: Path, build_dir: Path) -> bool: + """Copy FastLED library to build directory with proper library.json structure.""" + lib_dir = build_dir / "lib" / "FastLED" + lib_parent = build_dir / "lib" + + # Remove existing FastLED directory if it exists + if lib_dir.exists(): + if lib_dir.is_symlink(): + lib_dir.unlink() + else: + shutil.rmtree(lib_dir) + + # Create lib directory if it doesn't exist + lib_parent.mkdir(parents=True, exist_ok=True) + + # Copy src/ directory into lib/FastLED using dirsync for better caching + fastled_src_path = project_root / "src" + try: + # Ensure target directory exists for dirsync + lib_dir.mkdir(parents=True, exist_ok=True) + + # Use dirsync.sync for efficient incremental synchronization + sync(str(fastled_src_path), str(lib_dir), "sync", purge=True) + + # Copy library.json to the root of lib/FastLED + library_json_src = project_root / "library.json" + library_json_dst = lib_dir / "library.json" + if library_json_src.exists(): + shutil.copy2(library_json_src, library_json_dst) + + # Calculate relative paths for cleaner output + try: + rel_lib_dir = lib_dir.relative_to(Path.cwd()) + rel_src_path = fastled_src_path.relative_to(Path.cwd()) + print(f"Synced FastLED library: {rel_src_path} -> {rel_lib_dir}") + if library_json_src.exists(): + print(f"Copied library.json to {rel_lib_dir}") + except ValueError: + # Fallback to absolute paths if relative calculation fails + print(f"Synced FastLED library to {lib_dir}") + if library_json_src.exists(): + print(f"Copied library.json to {lib_dir}") + except Exception as sync_error: + warnings.warn(f"Failed to sync FastLED library: {sync_error}") + return False + + return True + + +def _init_platformio_build( + board: Board, + verbose: bool, + example: str, + paths: "FastLEDPaths", + additional_defines: list[str] | None = None, + additional_include_dirs: list[str] | None = None, + additional_libs: list[str] | None = None, + cache_type: CacheType = CacheType.NO_CACHE, +) -> InitResult: + """Initialize the PlatformIO build directory. Assumes lock is already held by caller.""" + project_root = _resolve_project_root() + build_dir = project_root / ".build" / "pio" / board.board_name + + # Check for and fix corrupted packages before building + # Find platform path based on board's platform URL (works for any platform) + platform_path = _find_platform_path_from_board(board, paths) + fixed_packages = detect_and_fix_corrupted_packages_dynamic( + paths, board.board_name, platform_path + ) + + # Lock is already held by build() - no need to acquire again + + # Setup the build directory. + build_dir.mkdir(parents=True, exist_ok=True) + platformio_ini = build_dir / "platformio.ini" + + # Ensure platform is installed if needed + if not _ensure_platform_installed(board): + return InitResult( + success=False, + output=f"Failed to install platform for {board.board_name}", + build_dir=build_dir, + ) + + # Clone board and add sketch directory include path (enables "shared/file.h" style includes) + board_with_sketch_include = board.clone() + if board_with_sketch_include.build_flags is None: + board_with_sketch_include.build_flags = [] + else: + board_with_sketch_include.build_flags = list( + board_with_sketch_include.build_flags + ) + board_with_sketch_include.build_flags.append("-Isrc/sketch") + + # Set up compiler cache through build_flags if enabled and available + cache_config = _get_cache_build_flags(board.board_name, cache_type) + if cache_config: + print(f"Applied cache configuration: {list(cache_config.keys())}") + + # Add compiler launcher build flags directly + sccache_path = cache_config.get("SCCACHE_PATH") + if sccache_path: + # Add build flags to use sccache as compiler launcher + launcher_flags = [f'-DCACHE_LAUNCHER="{sccache_path}"'] + board_with_sketch_include.build_flags.extend(launcher_flags) + print(f"Added cache launcher flags: {launcher_flags}") + + # Create build script that will set up cache environment + _copy_cache_build_script(build_dir, cache_config) + + # Optimization report generation is available but OFF by default + # To enable optimization reports, add these flags to your board configuration: + # - "-fopt-info-all=optimization_report.txt" for detailed optimization info + # - "-Wl,-Map,firmware.map" for memory map analysis + # + # Note: The infrastructure is in place to support optimization reports when needed + + # Always generate optimization artifacts into the board build directory + # Use absolute paths to ensure GCC/LD write into a known location even when the + # working directory changes inside PlatformIO builds. + try: + opt_report_path = (build_dir / "optimization_report.txt").resolve() + # GCC writes reports relative to the current working directory; provide absolute path + + # ESP32-C2 platform cannot work with -fopt-info-all, suppress it for this platform + if board.board_name != "esp32c2": + board_with_sketch_include.build_flags.append( + f"-fopt-info-all={opt_report_path.as_posix()}" + ) + + # Generate linker map in the board directory (file name is sufficient; PIO writes here) + board_with_sketch_include.build_flags.append("-Wl,-Map,firmware.map") + except Exception as _e: + # Non-fatal: continue without optimization artifacts if path resolution fails + pass + + # Apply board-specific configuration + if not _apply_board_specific_config( + board_with_sketch_include, + platformio_ini, + example, + paths, + additional_defines, + additional_include_dirs, + additional_libs, + cache_type, + ): + return InitResult( + success=False, + output=f"Failed to apply board configuration for {board.board_name}", + build_dir=build_dir, + ) + + # Print building banner first + print(_create_building_banner(example)) + + ok_copy_src = _copy_example_source(project_root, build_dir, example) + if not ok_copy_src: + error_msg = _get_example_error_message(project_root, example) + warnings.warn(error_msg) + return InitResult( + success=False, + output=error_msg, + build_dir=build_dir, + ) + + # Copy FastLED library + ok_copy_fastled = _copy_fastled_library(project_root, build_dir) + if not ok_copy_fastled: + warnings.warn(f"Failed to copy FastLED library") + return InitResult( + success=False, output=f"Failed to copy FastLED library", build_dir=build_dir + ) + + # Copy boards directory + ok_copy_boards = _copy_boards_directory(project_root, build_dir) + if not ok_copy_boards: + warnings.warn(f"Failed to copy boards directory") + return InitResult( + success=False, + output=f"Failed to copy boards directory", + build_dir=build_dir, + ) + + # Create sdkconfig.defaults if framework has "espidf" in it for esp32c2 board + frameworks = {f.strip() for f in (board.framework or "").split(",")} + if {"arduino", "espidf"}.issubset(frameworks): + sdkconfig_path = build_dir / "sdkconfig.defaults" + print(f"Creating sdkconfig.defaults file") + try: + sdkconfig_path.write_text( + "CONFIG_FREERTOS_HZ=1000\r\nCONFIG_AUTOSTART_ARDUINO=y" + ) + with open(sdkconfig_path, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + print(line, end="") + except Exception as e: + warnings.warn(f"Failed to write sdkconfig: {e}") + + # Final platformio.ini is already written by _apply_board_specific_config + # No need to write it again + + # Run initial build with LDF enabled to set up the environment + run_cmd: list[str] = ["pio", "run", "--project-dir", str(build_dir)] + if verbose: + run_cmd.append("--verbose") + + # Print platformio.ini content for the initialization build + platformio_ini_path = build_dir / "platformio.ini" + if platformio_ini_path.exists(): + print() # Add newline before configuration section + print("=" * 60) + print("PLATFORMIO.INI CONFIGURATION:") + print("=" * 60) + ini_content = platformio_ini_path.read_text() + print(ini_content) + print("=" * 60) + print() # Add newline after configuration section + + print(f"Running initial build command: {subprocess.list2cmdline(run_cmd)}") + + # Start timer for this example + start_time = time.time() + + # Create formatter for path substitution and timestamping + formatter = create_sketch_path_formatter(example) + + running_process = RunningProcess( + run_cmd, cwd=build_dir, auto_run=True, output_formatter=formatter + ) + # Output is transformed by the formatter, but we need to print it + while line := running_process.get_next_line(): + if isinstance(line, EndOfStream): + break + # Print the transformed line + print(line) + + running_process.wait() + + if running_process.returncode != 0: + return InitResult( + success=False, + output=f"Initial build failed: {running_process.stdout}", + build_dir=build_dir, + ) + + # After successful build, configuration is already properly set up + # Board configuration includes all necessary settings + + # Generate build_info.json after successful initialization build + _generate_build_info_json_from_existing_build(build_dir, board) + + return InitResult(success=True, output="", build_dir=build_dir) + + +class PlatformLock: + def __init__(self, lock_file: Path) -> None: + self.lock_file_path = lock_file + self.lock = fasteners.InterProcessLock(str(self.lock_file_path)) + self.is_locked = False + + def acquire(self) -> None: + self.is_locked = True + self.lock.acquire(blocking=True, timeout=5) + + def release(self) -> None: + if self.is_locked: + self.lock.release() + self.is_locked = False + + +class PioCompiler(Compiler): + def __init__( + self, + board: Board | str, + verbose: bool, + global_cache_dir: Path, + additional_defines: list[str] | None = None, + additional_include_dirs: list[str] | None = None, + additional_libs: list[str] | None = None, + cache_type: CacheType = CacheType.NO_CACHE, + ) -> None: + # Call parent constructor + super().__init__() + + # Convert string to Board object if needed + if isinstance(board, str): + self.board = create_board(board) + else: + self.board = board + self.verbose = verbose + self.additional_defines = additional_defines + self.additional_include_dirs = additional_include_dirs + self.additional_libs = additional_libs + self.cache_type = cache_type + + # Global cache directory is already resolved by caller + self.global_cache_dir = global_cache_dir + + # Use centralized path management + self.paths = FastLEDPaths(self.board.board_name) + self.platform_lock = PlatformLock(self.paths.platform_lock_file) + + # Always override the cache directory with our resolved path + self.paths._global_platformio_cache_dir = self.global_cache_dir + self.build_dir: Path = self.paths.build_dir + + # Ensure all directories exist + self.paths.ensure_directories_exist() + + # Create the global package installation lock + self._global_package_lock = GlobalPackageLock(self.board.board_name) + + self.initialized = False + self.executor = ThreadPoolExecutor(max_workers=1) + + def _internal_init_build_no_lock(self, example: str) -> InitResult: + """Initialize the PlatformIO build directory once with the first example.""" + if self.initialized: + return InitResult( + success=True, output="Already initialized", build_dir=self.build_dir + ) + + # Initialize with the actual first example being built + result = _init_platformio_build( + self.board, + self.verbose, + example, + self.paths, + self.additional_defines, + self.additional_include_dirs, + self.additional_libs, + self.cache_type, + ) + if result.success: + self.initialized = True + return result + + def cancel_all(self) -> None: + """Cancel all builds.""" + self.executor.shutdown(wait=False, cancel_futures=True) + + def build(self, examples: list[str]) -> list[Future[SketchResult]]: + """Build a list of examples with proper lock management.""" + if not examples: + return [] + + # Acquire the global package lock for the first build (package installation) + + count = len(examples) + + def release_platform_lock(fut: Future[SketchResult]) -> None: + """Release the platform lock when all builds complete.""" + nonlocal count + count -= 1 + if count == 0: + print(f"Releasing platform lock: {self.platform_lock.lock_file_path}") + self.platform_lock.release() + + futures: list[Future[SketchResult]] = [] + + # Submit all builds + self._global_package_lock.acquire() + cancelled = threading.Event() + try: + for example in examples: + future = self.executor.submit( + self._internal_build_no_lock, example, cancelled + ) + future.add_done_callback(release_platform_lock) + futures.append(future) + except KeyboardInterrupt: + print("KeyboardInterrupt: Cancelling all builds") + cancelled.set() + for future in futures: + future.cancel() + import _thread + + _thread.interrupt_main() + raise + except Exception as e: + print(f"Exception: {e}") + for future in futures: + future.cancel() + raise + + return futures + + def _build_internal(self, example: str) -> SketchResult: + """Internal build method without lock management.""" + # Print building banner first + print(_create_building_banner(example)) + + # Copy example source to build directory + project_root = _resolve_project_root() + ok_copy_src = _copy_example_source(project_root, self.build_dir, example) + if not ok_copy_src: + error_msg = _get_example_error_message(project_root, example) + return SketchResult( + success=False, + output=error_msg, + build_dir=self.build_dir, + example=example, + ) + + # Cache configuration is handled through build flags during initialization + + # Run PlatformIO build + run_cmd: list[str] = [ + "pio", + "run", + "--project-dir", + str(self.build_dir), + "--disable-auto-clean", + ] + if self.verbose: + run_cmd.append("--verbose") + + print(f"Running command: {subprocess.list2cmdline(run_cmd)}") + + # Create formatter for path substitution and timestamping + formatter = create_sketch_path_formatter(example) + + running_process = RunningProcess( + run_cmd, cwd=self.build_dir, auto_run=True, output_formatter=formatter + ) + try: + # Output is transformed by the formatter, but we need to print it + while line := running_process.get_next_line(timeout=60): + if isinstance(line, EndOfStream): + break + # Print the transformed line + print(line) + except KeyboardInterrupt: + print("KeyboardInterrupt: Cancelling build") + running_process.terminate() + raise + except OSError as e: + # Handle output encoding issues on Windows + print(f"Output encoding issue: {e}") + pass + + running_process.wait() + + success = running_process.returncode == 0 + + # Print SUCCESS/FAILED message immediately in worker thread to avoid race conditions + if success: + green_color = "\033[32m" + reset_color = "\033[0m" + print(f"{green_color}SUCCESS: {example}{reset_color}") + else: + red_color = "\033[31m" + reset_color = "\033[0m" + print(f"{red_color}FAILED: {example}{reset_color}") + + # Check if build was successful + build_success = running_process.returncode == 0 + + # Generate build_info.json after successful build + if build_success: + _generate_build_info_json_from_existing_build(self.build_dir, self.board) + + return SketchResult( + success=success, + output=running_process.stdout, + build_dir=self.build_dir, + example=example, + ) + + def get_cache_stats(self) -> str: + """Get compiler statistics as a formatted string. + + For PIO compiler, this includes cache statistics (sccache/ccache). + """ + if self.cache_type == CacheType.NO_CACHE: + return "" + + import shutil + import subprocess + + cache_name = "sccache" if self.cache_type == CacheType.SCCACHE else "ccache" + cache_path = shutil.which(cache_name) + + if not cache_path: + return f"{cache_name.upper()} not found in PATH" + + try: + result = subprocess.run( + [cache_path, "--show-stats"], + capture_output=True, + text=True, + check=False, + ) + + if result.returncode == 0: + stats_lines: list[str] = [] + for line in result.stdout.strip().split("\n"): + if line.strip(): + stats_lines.append(line) + + # Add header with cache type + stats_output = f"{cache_name.upper()} STATISTICS:\n" + stats_output += "\n".join(stats_lines) + return stats_output + else: + return f"Failed to get {cache_name.upper()} statistics: {result.stderr}" + + except Exception as e: + return f"Error retrieving {cache_name.upper()} statistics: {e}" + + def _internal_build_no_lock( + self, example: str, cancelled: threading.Event + ) -> SketchResult: + """Build a specific example without lock management. Only call from build().""" + if cancelled.is_set(): + return SketchResult( + success=False, + output="Cancelled", + build_dir=self.build_dir, + example=example, + ) + try: + if not self.initialized: + init_result = self._internal_init_build_no_lock(example) + if not init_result.success: + # Print FAILED message immediately in worker thread + red_color = "\033[31m" + reset_color = "\033[0m" + print(f"{red_color}FAILED: {example}{reset_color}") + + return SketchResult( + success=False, + output=init_result.output, + build_dir=init_result.build_dir, + example=example, + ) + # If initialization succeeded and we just built the example, return success + # Print SUCCESS message immediately in worker thread + green_color = "\033[32m" + reset_color = "\033[0m" + print(f"{green_color}SUCCESS: {example}{reset_color}") + + return SketchResult( + success=True, + output="Built during initialization", + build_dir=self.build_dir, + example=example, + ) + + # No lock management - caller (build) handles locks + return self._build_internal(example) + except KeyboardInterrupt: + print("KeyboardInterrupt: Cancelling build") + cancelled.set() + import _thread + + _thread.interrupt_main() + raise + + def clean(self) -> None: + """Clean build artifacts for this platform (acquire platform lock).""" + print(f"Cleaning build artifacts for platform {self.board.board_name}...") + + try: + # Remove the local build directory + if self.build_dir.exists(): + print(f"Removing build directory: {self.build_dir}") + shutil.rmtree(self.build_dir) + print(f"✅ Cleaned local build artifacts for {self.board.board_name}") + else: + print( + f"✅ No build directory found for {self.board.board_name} (already clean)" + ) + finally: + pass # we used to release the platform lock here, but we disabled it + + def clean_all(self) -> None: + """Clean all build artifacts (local and global packages) for this platform.""" + print(f"Cleaning all artifacts for platform {self.board.board_name}...") + + # Acquire both platform and global package locks + self._global_package_lock.acquire() + + try: + # Clean local build artifacts first + if self.build_dir.exists(): + print(f"Removing build directory: {self.build_dir}") + shutil.rmtree(self.build_dir) + print(f"✅ Cleaned local build artifacts for {self.board.board_name}") + else: + print(f"✅ No build directory found for {self.board.board_name}") + + # Clean global packages directory + if self.paths.packages_dir.exists(): + print(f"Removing global packages directory: {self.paths.packages_dir}") + shutil.rmtree(self.paths.packages_dir) + print(f"✅ Cleaned global packages for {self.board.board_name}") + else: + print( + f"✅ No global packages directory found for {self.board.board_name}" + ) + + # Clean global core directory (build cache, platforms) + if self.paths.core_dir.exists(): + print(f"Removing global core directory: {self.paths.core_dir}") + shutil.rmtree(self.paths.core_dir) + print(f"✅ Cleaned global core cache for {self.board.board_name}") + else: + print(f"✅ No global core directory found for {self.board.board_name}") + + finally: + # Always release locks + self._global_package_lock.release() + + def deploy( + self, example: str, upload_port: str | None = None, monitor: bool = False + ) -> SketchResult: + """Deploy (upload) a specific example to the target device. + + Args: + example: Name of the example to deploy + upload_port: Optional specific port for upload (e.g., "/dev/ttyUSB0", "COM3") + monitor: If True, attach to device monitor after successful upload + """ + print(f"Deploying {example} to {self.board.board_name}...") + + try: + # Ensure the build is initialized and the example is built + if not self.initialized: + init_result = self._internal_init_build_no_lock(example) + if not init_result.success: + return SketchResult( + success=False, + output=init_result.output, + build_dir=init_result.build_dir, + example=example, + ) + else: + # Build the example first (ensures it's up to date) + build_result = self._build_internal(example) + if not build_result.success: + return build_result + + # Run PlatformIO upload command + upload_cmd: list[str] = [ + "pio", + "run", + "--project-dir", + str(self.build_dir), + "--target", + "upload", + ] + + if upload_port: + upload_cmd.extend(["--upload-port", upload_port]) + + if self.verbose: + upload_cmd.append("--verbose") + + print(f"Running upload command: {subprocess.list2cmdline(upload_cmd)}") + + # Create formatter for upload output + formatter = create_sketch_path_formatter(example) + + running_process = RunningProcess( + upload_cmd, + cwd=self.build_dir, + auto_run=True, + output_formatter=formatter, + ) + try: + # Output is transformed by the formatter, but we need to print it + while line := running_process.get_next_line(timeout=60): + if isinstance(line, EndOfStream): + break + # Print the transformed line + print(line) + except OSError as e: + # Handle output encoding issues on Windows + print(f"Upload encoding issue: {e}") + pass + + running_process.wait() + + # Check if upload was successful + upload_success = running_process.returncode == 0 + upload_output = running_process.stdout + + if not upload_success: + return SketchResult( + success=False, + output=upload_output, + build_dir=self.build_dir, + example=example, + ) + + # Upload completed successfully - release the lock before monitor + print(f"✅ Upload completed successfully for {example}") + + # If monitor is requested and upload was successful, start monitor + if monitor: + print( + f"📡 Starting monitor for {example} on {self.board.board_name}..." + ) + print("📝 Press Ctrl+C to exit monitor") + + monitor_cmd: list[str] = [ + "pio", + "device", + "monitor", + "--project-dir", + str(self.build_dir), + ] + + if upload_port: + monitor_cmd.extend(["--port", upload_port]) + + print( + f"Running monitor command: {subprocess.list2cmdline(monitor_cmd)}" + ) + + # Start monitor process (no lock needed for monitoring) + monitor_process = RunningProcess( + monitor_cmd, cwd=self.build_dir, auto_run=True + ) + try: + while line := monitor_process.get_next_line( + timeout=None + ): # No timeout for monitor + if isinstance(line, EndOfStream): + break + print(line) # No timestamp for monitor output + except KeyboardInterrupt: + print("\n📡 Monitor stopped by user") + monitor_process.terminate() + except OSError as e: + print(f"Monitor encoding issue: {e}") + pass + + monitor_process.wait() + + return SketchResult( + success=True, + output=upload_output, + build_dir=self.build_dir, + example=example, + ) + + finally: + # Only decrement the lock if it hasn't been released yet + pass # we used to release the platform lock here, but we disabled it + + def check_usb_permissions(self) -> tuple[bool, str]: + """Check if USB device access is properly configured on Linux. + + Checks multiple methods for USB device access: + 1. PlatformIO udev rules + 2. User group membership (dialout, uucp, plugdev) + 3. Alternative udev rules files + + Returns: + Tuple of (has_access, status_message) + """ + if platform.system() != "Linux": + return True, "Not applicable on non-Linux systems" + + access_methods: list[str] = [] + + # Check 1: PlatformIO udev rules + udev_rules_path = Path("/etc/udev/rules.d/99-platformio-udev.rules") + if udev_rules_path.exists(): + access_methods.append("PlatformIO udev rules") + + # Check 2: User group membership + user_groups = self._get_user_groups() + usb_groups = ["dialout", "uucp", "plugdev", "tty"] + user_usb_groups = [group for group in usb_groups if group in user_groups] + if user_usb_groups: + access_methods.append(f"Group membership: {', '.join(user_usb_groups)}") + + # Check 3: Alternative udev rules + alt_udev_files = [ + "/etc/udev/rules.d/99-arduino.rules", + "/etc/udev/rules.d/50-platformio-udev.rules", + "/lib/udev/rules.d/99-platformio-udev.rules", + ] + for alt_file in alt_udev_files: + if Path(alt_file).exists(): + access_methods.append(f"Alternative udev rules: {Path(alt_file).name}") + + # Check 4: Root user (always has access) + if self._is_root_user(): + access_methods.append("Root user privileges") + + if access_methods: + status = f"USB access available via: {'; '.join(access_methods)}" + return True, status + else: + return False, "No USB device access methods detected" + + def _get_user_groups(self) -> list[str]: + """Get list of groups the current user belongs to.""" + try: + result = subprocess.run(["groups"], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip().split() + return [] + except Exception: + return [] + + def _is_root_user(self) -> bool: + """Check if running as root user.""" + try: + import os + + return os.geteuid() == 0 + except Exception: + return False + + def check_udev_rules(self) -> bool: + """Check if PlatformIO udev rules are installed on Linux. + + DEPRECATED: Use check_usb_permissions() instead for comprehensive checking. + + Returns: + True if any USB access method is available, False otherwise + """ + has_access, _ = self.check_usb_permissions() + return has_access + + def install_usb_permissions(self) -> bool: + """Install platform-specific USB permissions (e.g., udev rules on Linux). + + Returns: + True if installation succeeded, False otherwise + """ + if platform.system() != "Linux": + print("INFO: udev rules are only needed on Linux systems") + return True + + udev_url = "https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules" + udev_rules_path = "/etc/udev/rules.d/99-platformio-udev.rules" + + print("📡 Downloading PlatformIO udev rules...") + + try: + # Download the udev rules + with urllib.request.urlopen(udev_url) as response: + udev_content = response.read().decode("utf-8") + + # Write to temporary file first + temp_file = "/tmp/99-platformio-udev.rules" + with open(temp_file, "w") as f: + f.write(udev_content) + + print("💾 Installing udev rules (requires sudo)...") + + # Use sudo to copy to system location + result = subprocess.run( + ["sudo", "cp", temp_file, udev_rules_path], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + print(f"ERROR: Failed to install udev rules: {result.stderr}") + return False + + # Clean up temp file + os.unlink(temp_file) + + print("✅ PlatformIO udev rules installed successfully!") + print("⚠️ To complete the installation, run one of the following:") + print(" sudo service udev restart") + print(" # or") + print(" sudo udevadm control --reload-rules") + print(" sudo udevadm trigger") + print( + "⚠️ You may also need to restart your system for the changes to take effect." + ) + + return True + + except Exception as e: + print(f"ERROR: Failed to install udev rules: {e}") + return False + + +def run_pio_build( + board: Board | str, + examples: list[str], + verbose: bool = False, + additional_defines: list[str] | None = None, + additional_include_dirs: list[str] | None = None, + cache_type: CacheType = CacheType.NO_CACHE, +) -> list[Future[SketchResult]]: + """Run build for specified examples and platform using new PlatformIO system. + + Args: + board: Board class instance or board name string (resolved via create_board()) + examples: List of example names to build + verbose: Enable verbose output + additional_defines: Additional defines to add to build flags (e.g., ["FASTLED_DEFINE=0", "DEBUG=1"]) + additional_include_dirs: Additional include directories to add to build flags (e.g., ["src/platforms/sub", "external/libs"]) + """ + pio = PioCompiler( + board, + verbose, + Path.home() / ".fastled" / "platformio_cache", + additional_defines, + additional_include_dirs, + None, + cache_type, + ) + futures = pio.build(examples) + + # Show cache statistics after all builds complete + if cache_type != CacheType.NO_CACHE: + # Create a callback to show stats when all builds complete + def add_stats_callback(): + completed_count = 0 + total_count = len(futures) + + def on_future_complete(future: Any) -> None: + nonlocal completed_count + completed_count += 1 + + # When all futures complete, show compiler statistics + if completed_count == total_count: + try: + stats = pio.get_cache_stats() + if stats: + print("\n" + "=" * 60) + print("COMPILER STATISTICS") + print("=" * 60) + print(stats) + print("=" * 60) + except Exception as e: + print(f"Warning: Could not retrieve compiler statistics: {e}") + + # Add callback to all futures + for future in futures: + future.add_done_callback(on_future_complete) + + add_stats_callback() + + return futures diff --git a/v1.5/lib/FastLED/ci/compiler/platformio_cache.py b/v1.5/lib/FastLED/ci/compiler/platformio_cache.py new file mode 100644 index 0000000..9728e34 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/platformio_cache.py @@ -0,0 +1,937 @@ +#!/usr/bin/env python3 +""" +PlatformIO artifact cache implementation for speeding up CI builds. + +This module implements the cache mechanism described in FEATURE_PIO_SPEEDUP.md, +providing functionality to: +1. Parse platformio.ini files with zip URLs +2. Download and cache platform/framework artifacts +3. Install artifacts via PlatformIO CLI +4. Modify platformio.ini in-place with resolved local paths +""" + +import _thread +import configparser +import json +import logging +import os +import shutil +import subprocess +import tempfile +import threading +import time +import urllib.parse +import zipfile +from concurrent.futures import Future, ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from types import TracebackType +from typing import Any, Dict, List, Optional, Tuple, Union, cast + +import fasteners +import httpx + +from ci.compiler.platformio_ini import PlatformIOIni +from ci.util.running_process import RunningProcess +from ci.util.url_utils import sanitize_url_for_path + + +@dataclass +class DownloadResult: + """Result of a download operation.""" + + url: str + temp_path: Path + exception: Optional[BaseException] = None + + @property + def success(self) -> bool: + """True if download succeeded.""" + return self.exception is None + + +@dataclass +class ArtifactProcessingResult: + """Result of processing an artifact.""" + + url: str + is_framework: bool + env_section: str + resolved_path: Optional[str] = None + exception: Optional[BaseException] = None + + @property + def success(self) -> bool: + """True if processing succeeded.""" + return self.exception is None and self.resolved_path is not None + + +@dataclass +class ZipUrlInfo: + """Information about a zip URL found in platformio.ini.""" + + url: str + section_name: str + option_name: str + + +@dataclass +class ManifestResult: + """Result of manifest validation and type detection.""" + + is_valid: bool + manifest_path: Optional[Path] + is_framework: bool + + +def _get_remote_file_size(url: str) -> Optional[int]: + """Get file size from URL using HEAD request.""" + try: + parsed_url = urllib.parse.urlparse(url) + if parsed_url.scheme in ("http", "https"): + with httpx.Client(follow_redirects=True) as client: + response = client.head( + url, headers={"User-Agent": "PlatformIO-Cache/1.0"} + ) + response.raise_for_status() + content_length = response.headers.get("Content-Length") + if content_length: + return int(content_length) + except Exception as e: + logger.debug(f"Failed to get file size for {url}: {e}") + return None + + +def _format_file_size(size_bytes: Optional[int]) -> str: + """Format file size in human-readable format.""" + if size_bytes is None: + return "unknown size" + + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f} KB" + elif size_bytes < 1024 * 1024 * 1024: + return f"{size_bytes / (1024 * 1024):.1f} MB" + else: + return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB" + + +# Global cache to track PlatformIO installations in this session +_session_installation_cache: set[str] = set() + +# Global cancellation event for handling keyboard interrupts +_global_cancel_event = threading.Event() + +# Lock for thread-safe operations +_download_lock = threading.Lock() + + +def _get_status_file(artifact_dir: Path, cache_key: str) -> Path: + """Get the JSON status file path for an artifact.""" + # Use simple descriptive filename since artifacts are already in unique directories + return artifact_dir / "info.json" + + +def _read_status(status_file: Path) -> Optional[Dict[str, Any]]: + """Read status from JSON file.""" + if not status_file.exists(): + return None + + try: + with open(status_file, "r") as f: + return json.load(f) + except (json.JSONDecodeError, OSError): + return None + + +def _write_status(status_file: Path, status: Dict[str, Any]) -> None: + """Write status to JSON file.""" + try: + with open(status_file, "w") as f: + json.dump(status, f, indent=2) + except OSError as e: + logger.warning(f"Failed to write status file {status_file}: {e}") + + +def _is_processing_complete(status_file: Path) -> bool: + """Check if processing is complete based on status file.""" + status = _read_status(status_file) + return status is not None and status.get("status") == "complete" + + +def _download_with_progress( + url: str, temp_path: Path, cancel_event: threading.Event +) -> DownloadResult: + """Download HTTP/HTTPS file with cancellation support.""" + try: + # HTTP/HTTPS download using httpx with streaming + with httpx.Client(follow_redirects=True, timeout=30.0) as client: + with client.stream( + "GET", url, headers={"User-Agent": "PlatformIO-Cache/1.0"} + ) as response: + response.raise_for_status() + + with open(temp_path, "wb") as f: + for chunk in response.iter_bytes(chunk_size=8192): + # Check for cancellation periodically + if cancel_event.is_set(): + cancelled_error = RuntimeError( + f"Download cancelled for {url}" + ) + logger.warning( + f"Download cancelled for {url}", + exc_info=cancelled_error, + ) + return DownloadResult(url, temp_path, cancelled_error) + f.write(chunk) + + return DownloadResult(url, temp_path) # Success + + except KeyboardInterrupt as e: + # Set cancel event and interrupt main thread + cancel_event.set() + _thread.interrupt_main() + logger.warning( + f"Download interrupted by KeyboardInterrupt for {url}", exc_info=e + ) + return DownloadResult(url, temp_path, e) + except Exception as e: + if not cancel_event.is_set(): + logger.error(f"Download failed for {url}: {e}", exc_info=e) + else: + logger.warning(f"Download failed for {url} (cancelled): {e}", exc_info=e) + return DownloadResult(url, temp_path, e) + + +def _copy_file_with_progress( + url: str, temp_path: Path, cancel_event: threading.Event +) -> DownloadResult: + """Copy local file with cancellation support.""" + try: + parsed_url = urllib.parse.urlparse(url) + + # File URL - copy local file + logger.debug(f"Parsing file URL: {url}") + logger.debug(f"Parsed URL path: {parsed_url.path}") + + # Handle both Unix and Windows file URLs + if os.name == "nt": # Windows + # On Windows, file:///C:/path becomes /C:/path, so remove leading slash + if ( + parsed_url.path.startswith("/") + and len(parsed_url.path) > 3 + and parsed_url.path[2] == ":" + ): + source_path = Path(parsed_url.path[1:]) + else: + source_path = Path(parsed_url.path) + else: # Unix-like + source_path = Path(parsed_url.path) + + logger.debug(f"Resolved file path: {source_path}") + + if not source_path.exists(): + raise FileNotFoundError(f"Source file not found: {source_path}") + + # Check for cancellation before copy + if cancel_event.is_set(): + cancelled_error = RuntimeError(f"Copy cancelled for {url}") + logger.warning(f"Copy cancelled for {url}", exc_info=cancelled_error) + return DownloadResult(url, temp_path, cancelled_error) + + shutil.copy2(source_path, temp_path) + return DownloadResult(url, temp_path) # Success + + except KeyboardInterrupt as e: + # Set cancel event and interrupt main thread + cancel_event.set() + _thread.interrupt_main() + logger.warning(f"Copy interrupted by KeyboardInterrupt for {url}", exc_info=e) + return DownloadResult(url, temp_path, e) + except Exception as e: + if not cancel_event.is_set(): + logger.error(f"Copy failed for {url}: {e}", exc_info=e) + else: + logger.warning(f"Copy failed for {url} (cancelled): {e}", exc_info=e) + return DownloadResult(url, temp_path, e) + + +def clear_session_cache() -> None: + """Clear the session installation cache. Useful for testing.""" + global _session_installation_cache + _session_installation_cache.clear() + logger.debug("Cleared session installation cache") + + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Reduce httpx verbosity to avoid log spew from HTTP requests +logging.getLogger("httpx").setLevel(logging.WARNING) + + +class PlatformIOCache: + """Enhanced cache manager for PlatformIO artifacts.""" + + def __init__(self, cache_dir: Path): + """Initialize cache manager with directory structure.""" + self.cache_dir = cache_dir + # Simplified structure: each artifact gets its own directory directly in cache root + # containing both the .zip and extracted/ folder + + # Create directory structure + self.cache_dir.mkdir(parents=True, exist_ok=True) + + def _get_cache_key(self, url: str) -> str: + """Generate cache key from URL - sanitized for filesystem use.""" + return str(sanitize_url_for_path(url)) + + def download_artifact(self, url: str) -> str: + """ + Download and cache an artifact from the given URL. + Returns the absolute path to the cached zip file. + """ + cache_key = self._get_cache_key(url) + # Each artifact gets its own directory + artifact_dir = self.cache_dir / cache_key + artifact_dir.mkdir(parents=True, exist_ok=True) + cached_path = artifact_dir / "artifact.zip" + + # Use read-write locking for concurrent safety + # Start with write lock for downloading, can upgrade to read if cache hit + lock_path = str(artifact_dir / "artifact.lock") + rw_lock = fasteners.InterProcessReaderWriterLock(lock_path) + with rw_lock.write_lock(): + if cached_path.exists(): + # Check if processing was completed successfully + status_file = _get_status_file(artifact_dir, cache_key) + if not _is_processing_complete(status_file): + logger.warning( + f"Cache incomplete (no completion status), re-downloading: {url}" + ) + # Remove incomplete zip file + cached_path.unlink() + # Also remove any partial extraction + extracted_dir = artifact_dir / "extracted" + if extracted_dir.exists(): + shutil.rmtree(extracted_dir) + else: + print(f"Using cached artifact: {cached_path}") + # Cache hit - return the cached path (write lock will be released) + return str(cached_path) + + # Check URL scheme to determine action + parsed_url = urllib.parse.urlparse(url) + + if parsed_url.scheme in ("http", "https"): + # Download to temporary file first (atomic operation) + file_size = _get_remote_file_size(url) + size_str = _format_file_size(file_size) + print(f"Downloading: {url} ({size_str})") + temp_file_handle = tempfile.NamedTemporaryFile( + delete=False, suffix=".zip" + ) + temp_path = Path(temp_file_handle.name) + temp_file_handle.close() # Close immediately to avoid Windows file locking issues + + # Create a thread-local cancel event + thread_cancel_event = threading.Event() + + # Download directly (we're already in a thread from the main pool) + download_result = _download_with_progress( + url, temp_path, thread_cancel_event + ) + elif parsed_url.scheme == "file": + # File URL - copy from local file + print(f"Copying from local file: {url}") + temp_file_handle = tempfile.NamedTemporaryFile( + delete=False, suffix=".zip" + ) + temp_path = Path(temp_file_handle.name) + temp_file_handle.close() # Close immediately to avoid Windows file locking issues + + # Create a thread-local cancel event + thread_cancel_event = threading.Event() + + # Copy directly (we're already in a thread from the main pool) + download_result = _copy_file_with_progress( + url, temp_path, thread_cancel_event + ) + else: + raise ValueError(f"Unsupported URL scheme: {parsed_url.scheme}") + + try: + # Check if global cancellation was requested + if _global_cancel_event.is_set(): + raise KeyboardInterrupt( + "Download cancelled due to global interrupt" + ) + + # Process the download result + if not download_result.success: + if download_result.exception is not None: + raise download_result.exception + else: + raise RuntimeError( + f"Download failed for unknown reason: {download_result.url}" + ) + else: + print(f"Download completed successfully: {download_result.url}") + + # Atomic move to final location + shutil.move(str(temp_path), str(cached_path)) + print(f"Successfully cached: {cached_path}") + return str(cached_path) + + except Exception as e: + logger.error(f"Download failed: {e}") + if temp_path.exists(): + temp_path.unlink() + raise + + +def _is_zip_web_url(value: str) -> bool: + """Enhanced URL detection for zip artifacts.""" + if not isinstance(value, str): + return False + + parsed = urllib.parse.urlparse(value) + # Direct zip URLs + if value.endswith(".zip"): + return parsed.scheme in ("http", "https") + return False + + +def validate_and_detect_manifest( + content_path: Path, +) -> ManifestResult: + """ + Validate manifest files and auto-detect artifact type. + """ + # Try framework manifests first + framework_manifests = ["framework.json", "package.json"] + for manifest in framework_manifests: + manifest_path = content_path / manifest + if manifest_path.exists(): + try: + with open(manifest_path, "r") as f: + json.load(f) # Validate JSON syntax + print(f"Found valid framework manifest: {manifest_path}") + return ManifestResult( + is_valid=True, manifest_path=manifest_path, is_framework=True + ) + except json.JSONDecodeError as e: + logger.warning(f"Invalid JSON in {manifest_path}: {e}") + + # Try platform manifest + platform_manifest = content_path / "platform.json" + if platform_manifest.exists(): + try: + with open(platform_manifest, "r") as f: + json.load(f) # Validate JSON syntax + print(f"Found valid platform manifest: {platform_manifest}") + return ManifestResult( + is_valid=True, manifest_path=platform_manifest, is_framework=False + ) + except json.JSONDecodeError as e: + logger.warning(f"Invalid JSON in {platform_manifest}: {e}") + + return ManifestResult(is_valid=False, manifest_path=None, is_framework=False) + + +def get_platformio_command_path(path: Path) -> str: + """Get path format for PlatformIO command line usage.""" + import platform + + resolved_path = path.resolve() + + # On Windows, PlatformIO has issues with file:// URLs for local directories + # Use native Windows paths instead + if platform.system() == "Windows": + return str(resolved_path) + else: + # On Unix systems, use proper file:// URLs + posix_path = resolved_path.as_posix() + return f"file://{posix_path}" + + +def get_proper_file_url(path: Path) -> str: + """Convert a path to a proper file:// URL for platformio.ini files.""" + # For PlatformIO on Windows, use the Windows path directly (not file:// URL) + # This avoids issues with path parsing in PlatformIO + import platform + + resolved_path = path.resolve() + + if platform.system() == "Windows": + # On Windows, PlatformIO handles Windows paths directly better than file:// URLs + # Convert to Windows path format + return str(resolved_path) + else: + # On Unix systems, use proper file:// URL format + posix_path = resolved_path.as_posix() + return f"file://{posix_path}" + + +def unzip_and_install( + cached_zip_path: Path, + cache_manager: PlatformIOCache, + is_framework: bool, + env_section: str, +) -> bool: + """ + Enhanced unzip and install with manifest validation and cleanup. + """ + cached_zip_path_obj = cached_zip_path + # Extract to a directory alongside the zip file + artifact_dir = cached_zip_path_obj.parent + extracted_dir = artifact_dir / "extracted" + temp_unzip_dir = artifact_dir / "temp_extract" + + try: + # Clean extraction directory + if temp_unzip_dir.exists(): + shutil.rmtree(temp_unzip_dir) + temp_unzip_dir.mkdir(parents=True) + + print(f"Extracting {cached_zip_path_obj} to {temp_unzip_dir}") + + # Extract with better error handling + try: + with zipfile.ZipFile(cached_zip_path_obj, "r") as zip_ref: + # Check for zip bombs (basic protection) + total_size = sum(info.file_size for info in zip_ref.infolist()) + if total_size > 500 * 1024 * 1024: # 500MB limit + raise ValueError("Archive too large, possible zip bomb") + + zip_ref.extractall(temp_unzip_dir) + print("Extraction completed successfully") + + except zipfile.BadZipFile: + logger.error(f"Invalid zip file: {cached_zip_path_obj}") + return False + except Exception as e: + logger.error(f"Extraction failed: {e}") + return False + + # Find content directory (handle nested structures like GitHub archives) + content_items = list(temp_unzip_dir.iterdir()) + if len(content_items) == 1 and content_items[0].is_dir(): + # Single root directory (common with GitHub/GitLab archives) - use it + unzipped_content_path = content_items[0] + print(f"Found nested directory structure: {content_items[0].name}") + else: + # Multiple items at root - use temp dir + unzipped_content_path = temp_unzip_dir + print("Using flat directory structure") + + # Validate manifest files and auto-detect type + manifest_result = validate_and_detect_manifest(unzipped_content_path) + if not manifest_result.is_valid: + logger.error(f"No valid manifest found in {unzipped_content_path}") + return False + + # Move to final location + if extracted_dir.exists(): + shutil.rmtree(extracted_dir) + shutil.move(unzipped_content_path, extracted_dir) + + # Install via PlatformIO + return install_with_platformio( + extracted_dir, manifest_result.is_framework, env_section + ) + + finally: + # Cleanup temporary extraction + if temp_unzip_dir.exists(): + print(f"Cleaning up extraction directory: {temp_unzip_dir}") + shutil.rmtree(temp_unzip_dir) + + +def install_with_platformio( + content_path: Path, is_framework: bool, env_section: str +) -> bool: + """Install extracted content using appropriate PlatformIO command.""" + content_path_obj = Path(content_path) + command_path = get_platformio_command_path(content_path_obj) + + # Create a cache key based on the installation type and path + cache_key = f"{'framework' if is_framework else 'platform'}:{command_path}" + + # Check if we've already installed this in this session + if cache_key in _session_installation_cache: + print( + f"Skipping installation for {env_section}: already installed in this session ({command_path})" + ) + return True + + if is_framework: + # Framework installation - use pkg install + command = ["pio", "pkg", "install", "--global", command_path] + else: + # Platform installation - also use pkg install (new recommended way) + command = ["pio", "pkg", "install", "--global", "--platform", command_path] + + try: + print(f"Installing for {env_section}: {' '.join(command)}") + + # Use RunningProcess for streaming output + process = RunningProcess( + command, + check=False, # We'll handle errors ourselves + timeout=300, # 5 minute timeout + ) + + # Stream output in real-time + for line in process.line_iter(timeout=300): + line_str = cast(str, line) + print(f"PIO: {line_str}") + + # Wait for completion and check result + process.wait() + + if process.returncode == 0: + print("PlatformIO installation successful") + # Add to session cache to avoid redundant installations + _session_installation_cache.add(cache_key) + return True + else: + logger.error( + f"PlatformIO installation failed with return code: {process.returncode}" + ) + return False + + except Exception as e: + if "No such file or directory" in str(e) or "not found" in str(e).lower(): + logger.error("PlatformIO CLI not found. Is it installed and in PATH?") + else: + logger.error(f"PlatformIO installation failed: {e}") + logger.error(f"Command: {' '.join(command)}") + return False + + +def handle_zip_artifact( + zip_source: str, + cache_manager: PlatformIOCache, + env_section: str, +) -> str: + """ + Enhanced artifact handler with validation and error recovery. + Returns the resolved local path for the artifact. + """ + try: + # Download and cache the artifact + cached_zip_path = cache_manager.download_artifact(zip_source) + + if not cached_zip_path or not Path(cached_zip_path).exists(): + raise FileNotFoundError(f"Download failed for {zip_source}") + + # Extract and get the content path + cache_key = cache_manager._get_cache_key(zip_source) + cached_zip_path_obj = Path(cached_zip_path) + + # Extract to a directory alongside the zip file + artifact_dir = cached_zip_path_obj.parent + extracted_dir = artifact_dir / "extracted" + temp_unzip_dir = artifact_dir / "temp_extract" + + # Check if already extracted + if extracted_dir.exists(): + # Validate existing extraction and auto-detect type + manifest_result = validate_and_detect_manifest(extracted_dir) + if manifest_result.is_valid: + print(f"Using existing extraction: {extracted_dir}") + + # Check for completion status using URL-based cache key (consistent with status file creation) + url_cache_key = cache_manager._get_cache_key(zip_source) + status_file = _get_status_file(artifact_dir, url_cache_key) + if _is_processing_complete(status_file): + print( + f"Skipping PlatformIO installation for {env_section}: found completion status" + ) + return get_proper_file_url(extracted_dir) + + # Create a session cache key for this artifact + session_cache_key = f"{'framework' if manifest_result.is_framework else 'platform'}:{get_platformio_command_path(extracted_dir)}" + + # If we've already handled this exact artifact in this session, skip PlatformIO entirely + if session_cache_key in _session_installation_cache: + print( + f"Skipping PlatformIO installation for {env_section}: already processed in this session" + ) + return get_proper_file_url(extracted_dir) + + # Otherwise, install via PlatformIO + install_success = install_with_platformio( + extracted_dir, manifest_result.is_framework, env_section + ) + # Status file will be created later using URL-based cache key + + if install_success: + print(f"Successfully installed {zip_source} for {env_section}") + else: + logger.warning( + f"Installation completed with warnings for {zip_source}" + ) + return get_proper_file_url(extracted_dir) + + # Clean extraction directory for fresh extraction + if temp_unzip_dir.exists(): + shutil.rmtree(temp_unzip_dir) + temp_unzip_dir.mkdir(parents=True) + + # Extract the zip + with zipfile.ZipFile(cached_zip_path, "r") as zip_ref: + zip_ref.extractall(temp_unzip_dir) + + # Find content directory (handle nested structures) + content_items = list(temp_unzip_dir.iterdir()) + if len(content_items) == 1 and content_items[0].is_dir(): + unzipped_content_path = content_items[0] + else: + unzipped_content_path = temp_unzip_dir + + # Validate manifest files and auto-detect type + manifest_result = validate_and_detect_manifest(unzipped_content_path) + if not manifest_result.is_valid: + raise ValueError(f"No valid manifest found in {unzipped_content_path}") + + # Move to final location + if extracted_dir.exists(): + shutil.rmtree(extracted_dir) + shutil.move(unzipped_content_path, extracted_dir) + + # Clean up temp directory + if temp_unzip_dir.exists(): + shutil.rmtree(temp_unzip_dir) + + # Install via PlatformIO + install_success = install_with_platformio( + extracted_dir, manifest_result.is_framework, env_section + ) + + # Create status file with processing results + cache_key = cache_manager._get_cache_key(zip_source) + status_file = _get_status_file(artifact_dir, cache_key) + + # Get zip file size + zip_file = artifact_dir / "artifact.zip" + zip_size = zip_file.stat().st_size if zip_file.exists() else 0 + + status = { + "status": "complete" if install_success else "warning", + "timestamp": datetime.now().isoformat(), + "url": zip_source, + "env_section": env_section, + "extracted_dir": str(extracted_dir.relative_to(artifact_dir)), + "zip_size_bytes": zip_size, + } + _write_status(status_file, status) + + if install_success: + print(f"Successfully installed {zip_source} for {env_section}") + else: + logger.warning(f"Installation completed with warnings for {zip_source}") + + # Return the file URL for the extracted directory + return get_proper_file_url(extracted_dir) + + except Exception as e: + import traceback + + traceback.print_exc() + logger.error(f"Failed to handle artifact {zip_source}: {e}") + raise + + +def _process_artifact( + artifact_url: str, + env_section: str, + cache_manager: "PlatformIOCache", +) -> ArtifactProcessingResult: + """Process a single artifact (download, extract, install).""" + try: + resolved_path = handle_zip_artifact(artifact_url, cache_manager, env_section) + return ArtifactProcessingResult( + url=artifact_url, + is_framework=False, # This will be determined during processing + env_section=env_section, + resolved_path=resolved_path, + ) + except Exception as e: + logger.error(f"Failed to process {artifact_url}: {e}", exc_info=e) + return ArtifactProcessingResult( + url=artifact_url, + is_framework=False, # This will be determined during processing + env_section=env_section, + exception=e, + ) + + +def _collect_all_zip_urls(pio_ini: PlatformIOIni) -> List[ZipUrlInfo]: + """ + Collect all zip URLs from platformio.ini in a single pass. + """ + all_urls: List[ZipUrlInfo] = [] + + # Collect platform URLs + for section_name, option_name, url in pio_ini.get_platform_urls(): + if _is_zip_web_url(url): + print(f"Found platform zip: {url} in {section_name}") + all_urls.append(ZipUrlInfo(url, section_name, option_name)) + + # Collect framework URLs + for section_name, option_name, url in pio_ini.get_framework_urls(): + if _is_zip_web_url(url): + print(f"Found framework zip: {url} in {section_name}") + all_urls.append(ZipUrlInfo(url, section_name, option_name)) + + return all_urls + + +def _dedupe_urls( + all_urls: List[ZipUrlInfo], +) -> Dict[str, str]: + """ + Deduplicate URLs, keeping the first occurrence of each unique URL. + Returns dict mapping url -> env_section. + """ + unique_urls: Dict[str, str] = {} + for url_info in all_urls: + if url_info.url not in unique_urls: + unique_urls[url_info.url] = url_info.section_name + return unique_urls + + +def _download_and_process_urls( + unique_urls: Dict[str, str], cache_manager: PlatformIOCache +) -> Dict[str, str]: + """ + Download and process all URLs concurrently. + Returns dict mapping original_url -> resolved_local_path. + Raises exception immediately if any download fails. + """ + if not unique_urls: + return {} + + max_workers = min(4, len(unique_urls)) # Don't create more threads than needed + with ThreadPoolExecutor( + max_workers=max_workers, thread_name_prefix="download" + ) as executor: + # Submit all download tasks + futures: List[Future[ArtifactProcessingResult]] = [] + for url, env_section in unique_urls.items(): + future = executor.submit(_process_artifact, url, env_section, cache_manager) + futures.append(future) + + replacements: Dict[str, str] = {} + + try: + for future in as_completed(futures): + try: + result = future.result() + if result.success and result.resolved_path is not None: + replacements[result.url] = result.resolved_path + print(f"✅ Resolved {result.url} -> {result.resolved_path}") + else: + logger.error( + f"❌ Failed to process {result.url}: {result.exception}" + ) + # Re-raise all exceptions - no downloads should fail silently + if result.exception: + raise result.exception + except Exception as e: + logger.error( + f"Future failed with unexpected error: {e}", exc_info=e + ) + raise # Re-raise unexpected exceptions + except KeyboardInterrupt: + logger.warning("Processing interrupted, cancelling remaining downloads...") + _global_cancel_event.set() + # The context manager will handle cleanup of the executor + raise + + return replacements + + +def _replace_all_urls( + pio_ini: PlatformIOIni, + all_urls: List[ZipUrlInfo], + replacements: Dict[str, str], +) -> None: + """ + Replace all URLs in platformio.ini with their resolved local paths. + """ + for url_info in all_urls: + if url_info.url in replacements: + pio_ini.replace_url( + url_info.section_name, + url_info.option_name, + url_info.url, + replacements[url_info.url], + ) + + +def _apply_board_specific_config( + platformio_ini_path: Path, custom_zip_cache_dir: Path +) -> None: + """ + Enhanced config parser with improved zip detection and error handling. + Modifies platformio.ini in-place with resolved local paths. + Uses concurrent downloads for better performance. + """ + cache_manager = PlatformIOCache(custom_zip_cache_dir) + + try: + pio_ini = PlatformIOIni.parseFile(platformio_ini_path) + print(f"Parsed platformio.ini: {platformio_ini_path}") + except (configparser.Error, FileNotFoundError) as e: + logger.error(f"Error reading platformio.ini: {e}") + return + + # Step 1: Collect all URLs from platformio.ini + all_urls = _collect_all_zip_urls(pio_ini) + if not all_urls: + print("No zip artifacts found to process") + return + + # Step 2: Deduplicate URLs + unique_urls = _dedupe_urls(all_urls) + print(f"Found {len(all_urls)} total URLs, {len(unique_urls)} unique") + + # Step 3: Download and process all unique URLs + replacements = _download_and_process_urls(unique_urls, cache_manager) + + # Step 4: Replace all URLs in platformio.ini with resolved local paths + if replacements: + _replace_all_urls(pio_ini, all_urls, replacements) + + # Write back atomically + try: + pio_ini.dump(platformio_ini_path) + print(f"Updated platformio.ini with {len(replacements)} resolved artifacts") + except Exception as e: + logger.error(f"Failed to update platformio.ini: {e}") + raise + + +# Public API function +def resolve_and_cache_platform_artifacts( + platformio_ini_path: Path, cache_dir: Path +) -> None: + """ + Main entry point for resolving and caching PlatformIO platform artifacts. + + Args: + platformio_ini_path: Path to the platformio.ini file to process + cache_dir: Cache directory for storing artifacts + """ + print(f"Starting platform artifact resolution for {platformio_ini_path}") + print(f"Using cache directory: {cache_dir}") + + _apply_board_specific_config(platformio_ini_path, cache_dir) + + print("Platform artifact resolution completed") diff --git a/v1.5/lib/FastLED/ci/compiler/platformio_ini.py b/v1.5/lib/FastLED/ci/compiler/platformio_ini.py new file mode 100644 index 0000000..34dcdb4 --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/platformio_ini.py @@ -0,0 +1,2170 @@ +#!/usr/bin/env python3 +""" +PlatformIO INI file parser and writer. + +This module provides a clean interface for parsing, manipulating, and writing +platformio.ini files. It's designed to be a general-purpose utility that can +be used by various tools that need to work with PlatformIO configuration files. +""" + +import configparser +import io +import json +import logging +import os +import re +import subprocess +import tempfile +import time +import uuid +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union + + +if TYPE_CHECKING: + from ci.compiler.platformio_cache import PlatformIOCache + + +# Configure logging +logger = logging.getLogger(__name__) + + +@dataclass +class PlatformIOSection: + """ + Typed representation of the [platformio] section. + Contains generic project settings and directory options. + """ + + # Generic Options + name: Optional[str] = None + description: Optional[str] = None + default_envs: Optional[List[str]] = None + extra_configs: Optional[List[str]] = None + + # Directory Options + core_dir: Optional[str] = None + globallib_dir: Optional[str] = None + platforms_dir: Optional[str] = None + packages_dir: Optional[str] = None + cache_dir: Optional[str] = None + build_cache_dir: Optional[str] = None + workspace_dir: Optional[str] = None + build_dir: Optional[str] = None + libdeps_dir: Optional[str] = None + include_dir: Optional[str] = None + src_dir: Optional[str] = None + lib_dir: Optional[str] = None + data_dir: Optional[str] = None + test_dir: Optional[str] = None + boards_dir: Optional[str] = None + monitor_dir: Optional[str] = None + shared_dir: Optional[str] = None + + +@dataclass +class EnvironmentSection: + """ + Typed representation of an [env:*] section. + Contains environment-specific build and configuration settings. + """ + + # Required/Core Options + name: str = "" # The environment name (e.g., "esp32s3" from "env:esp32s3") + + # Platform Options + platform: Optional[str] = None + framework: Optional[str] = None + board: Optional[str] = None + + # Inheritance + extends: Optional[str] = None + + # Build Options + build_type: Optional[str] = None + build_flags: List[str] = field(default_factory=lambda: []) + build_src_filter: List[str] = field(default_factory=lambda: []) + targets: List[str] = field(default_factory=lambda: []) + + # Library Options + lib_deps: List[str] = field(default_factory=lambda: []) + lib_ignore: List[str] = field(default_factory=lambda: []) + lib_extra_dirs: List[str] = field(default_factory=lambda: []) + lib_ldf_mode: Optional[str] = None + + # Upload Options + upload_port: Optional[str] = None + upload_protocol: Optional[str] = None + upload_speed: Optional[str] = None + + # Monitor Options + monitor_port: Optional[str] = None + monitor_speed: Optional[str] = None + monitor_filters: List[str] = field(default_factory=lambda: []) + + # Board-specific Options + board_build_mcu: Optional[str] = None + board_build_f_cpu: Optional[str] = None + board_build_partitions: Optional[str] = None + + # Extra Scripts and Tools + extra_scripts: List[str] = field(default_factory=lambda: []) + check_tool: Optional[str] = None + + # Custom Options (for non-standard options) + custom_options: Dict[str, str] = field(default_factory=lambda: {}) + + +@dataclass +class GlobalEnvSection: + """ + Typed representation of the [env] section. + Contains global settings that apply to all environments. + """ + + # Build Options + build_flags: List[str] = field(default_factory=lambda: []) + build_src_filter: List[str] = field(default_factory=lambda: []) + + # Library Options + lib_deps: List[str] = field(default_factory=lambda: []) + lib_ignore: List[str] = field(default_factory=lambda: []) + lib_extra_dirs: List[str] = field(default_factory=lambda: []) + lib_ldf_mode: Optional[str] = None + + # Monitor Options + monitor_speed: Optional[str] = None + monitor_filters: List[str] = field(default_factory=lambda: []) + + # Extra Scripts + extra_scripts: List[str] = field(default_factory=lambda: []) + + # Custom Options (for non-standard options) + custom_options: Dict[str, str] = field(default_factory=lambda: {}) + + +@dataclass +class ParsedPlatformIOConfig: + """ + Complete typed representation of a parsed platformio.ini file. + """ + + platformio_section: Optional[PlatformIOSection] = None + global_env_section: Optional[GlobalEnvSection] = None + environments: Dict[str, EnvironmentSection] = field(default_factory=lambda: {}) + + +@dataclass +class PackageInfo: + """ + Information about a package/dependency. + """ + + name: str + type: str = "" + requirements: str = "" + url: str = "" + optional: bool = True + version: Optional[str] = None + description: Optional[str] = None + + +@dataclass +class PlatformUrlResolution: + """ + Multi-value resolution result for a platform with different URL types. + """ + + name: str + git_url: Optional[str] = None + zip_url: Optional[str] = None + local_path: Optional[str] = None + version: Optional[str] = None + frameworks: List[str] = field(default_factory=lambda: []) + packages: List[PackageInfo] = field(default_factory=lambda: []) + homepage: Optional[str] = None + + @property + def preferred_url(self) -> Optional[str]: + """Get the preferred URL for downloading (zip preferred for speed).""" + return self.zip_url or self.git_url + + @property + def has_downloadable_url(self) -> bool: + """Check if there's a URL available for downloading.""" + return bool(self.git_url or self.zip_url) + + +@dataclass +class FrameworkUrlResolution: + """ + Multi-value resolution result for a framework with different URL types. + """ + + name: str + git_url: Optional[str] = None + zip_url: Optional[str] = None + local_path: Optional[str] = None + homepage: Optional[str] = None + platforms: List[str] = field(default_factory=lambda: []) + version: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + url: Optional[str] = None # Original URL from CLI response + + @property + def preferred_url(self) -> Optional[str]: + """Get the preferred URL for downloading (zip preferred for speed).""" + return self.zip_url or self.git_url or self.url + + +@dataclass +class PlatformShowResponse: + """ + Strongly typed representation of PlatformIO 'pio platform show' command response. + """ + + name: str + title: Optional[str] = None + version: Optional[str] = None + repository: Optional[str] = None + frameworks: List[str] = field(default_factory=list) # type: ignore + packages: List[Dict[str, Any]] = field( # type: ignore[reportUnknownVariableType] + default_factory=list + ) # Raw package data from CLI + homepage: Optional[str] = None + description: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PlatformShowResponse": + """Create PlatformShowResponse from raw CLI JSON response.""" + return cls( + name=data.get("name", ""), + title=data.get("title"), + version=data.get("version"), + repository=data.get("repository"), + frameworks=data.get("frameworks", []), + packages=data.get("packages", []), + homepage=data.get("homepage"), + description=data.get("description"), + ) + + +@dataclass +class FrameworkInfo: + """ + Strongly typed representation of framework information from PlatformIO CLI. + """ + + name: str + title: Optional[str] = None + description: Optional[str] = None + url: Optional[str] = None + homepage: Optional[str] = None + platforms: List[str] = field(default_factory=list) # type: ignore + version: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "FrameworkInfo": + """Create FrameworkInfo from raw CLI JSON response.""" + return cls( + name=data.get("name", ""), + title=data.get("title"), + description=data.get("description"), + url=data.get("url"), + homepage=data.get("homepage"), + platforms=data.get("platforms", []), + version=data.get("version"), + ) + + +@dataclass +class PlatformCacheEntry: + """ + Strongly typed representation of a platform cache entry for inspection. + """ + + repository_url: Optional[str] + version: Optional[str] + frameworks: List[str] + resolved_at: Optional[str] + expires_at: Optional[str] + + +@dataclass +class FrameworkCacheEntry: + """ + Strongly typed representation of a framework cache entry for inspection. + """ + + url: Optional[str] + homepage: Optional[str] + platforms: List[str] + resolved_at: Optional[str] + expires_at: Optional[str] + + +@dataclass +class ResolvedUrlsCache: + """ + Strongly typed representation of the complete resolved URLs cache. + """ + + platforms: Dict[str, PlatformCacheEntry] = field(default_factory=dict) # type: ignore + frameworks: Dict[str, FrameworkCacheEntry] = field(default_factory=dict) # type: ignore + + +@dataclass +class PlatformResolution: + """ + Cached resolution of a platform shorthand name to its metadata. + """ + + name: str + repository_url: Optional[str] = None # Kept for backward compatibility + packages_url: Optional[str] = None # Kept for backward compatibility + version: Optional[str] = None + frameworks: List[str] = field(default_factory=lambda: []) + resolved_at: Optional[datetime] = None + ttl_hours: int = 24 + + # Enhanced multi-value fields + enhanced_resolution: Optional[PlatformUrlResolution] = None + + @property + def git_url(self) -> Optional[str]: + """Get git URL from enhanced resolution or fall back to repository_url.""" + if self.enhanced_resolution: + return self.enhanced_resolution.git_url + return ( + self.repository_url + if self.repository_url and self.repository_url.endswith(".git") + else None + ) + + @property + def zip_url(self) -> Optional[str]: + """Get zip URL from enhanced resolution.""" + if self.enhanced_resolution: + return self.enhanced_resolution.zip_url + return ( + self.repository_url + if self.repository_url and self.repository_url.endswith(".zip") + else None + ) + + +@dataclass +class FrameworkResolution: + """ + Cached resolution of a framework shorthand name to its metadata. + """ + + name: str + url: Optional[str] = None # Kept for backward compatibility + homepage: Optional[str] = None + platforms: List[str] = field(default_factory=lambda: []) + resolved_at: Optional[datetime] = None + ttl_hours: int = 24 + + # Enhanced multi-value fields + enhanced_resolution: Optional[FrameworkUrlResolution] = None + + @property + def git_url(self) -> Optional[str]: + """Get git URL from enhanced resolution.""" + if self.enhanced_resolution: + return self.enhanced_resolution.git_url + return self.url if self.url and self.url.endswith(".git") else None + + @property + def zip_url(self) -> Optional[str]: + """Get zip URL from enhanced resolution.""" + if self.enhanced_resolution: + return self.enhanced_resolution.zip_url + return self.url if self.url and self.url.endswith(".zip") else None + + +def _resolve_variable_substitution( + value: str, config: configparser.ConfigParser +) -> str: + """ + Resolve variable substitution in a configuration value. + Supports patterns like ${env:generic-esp.build_flags} and ${platformio.build_cache_dir}. + + Args: + value: Value potentially containing variable references + config: ConfigParser to resolve variables from + + Returns: + Value with variables resolved + """ + if not value or "${" not in value: + return value + + # Pattern to match ${section.option} or ${section:subsection.option} + pattern = r"\$\{([^}]+)\}" + + def replace_variable(match: re.Match[str]) -> str: + var_ref = match.group(1) + + # Handle section:subsection.option format (like env:generic-esp.build_flags) + if ":" in var_ref: + section_part, option = var_ref.split(".", 1) + section = section_part.replace(":", ":") + else: + # Handle section.option format (like platformio.build_cache_dir) + section, option = var_ref.split(".", 1) + + try: + if config.has_option(section, option): + return config.get(section, option) + else: + # Return original if not found + return match.group(0) + except (configparser.NoSectionError, configparser.NoOptionError): + # Return original if resolution fails + return match.group(0) + + return re.sub(pattern, replace_variable, value) + + +def _resolve_list_variables( + values: List[str], config: configparser.ConfigParser +) -> List[str]: + """ + Resolve variable substitution in a list of values. + """ + resolved: List[str] = [] + for value in values: + resolved_value = _resolve_variable_substitution(value, config) + # If the resolved value contains newlines or commas, parse it as a list + if resolved_value != value and ( + "\n" in resolved_value or "," in resolved_value + ): + resolved.extend(_parse_list_value(resolved_value)) + else: + resolved.append(resolved_value) + return resolved + + +def _parse_list_value(value: str) -> List[str]: + """ + Parse a configuration value that can be either comma-separated or multi-line. + + Args: + value: Raw configuration value + + Returns: + List of parsed values + """ + if not value or not value.strip(): + return [] + + # Handle multi-line format (values on separate lines) + if "\n" in value: + lines = [line.strip() for line in value.split("\n") if line.strip()] + return lines + + # Handle comma-separated format + if "," in value: + return [item.strip() for item in value.split(",") if item.strip()] + + # Single value + return [value.strip()] if value.strip() else [] + + +def _parse_platformio_section( + config: configparser.ConfigParser, +) -> Optional[PlatformIOSection]: + """ + Parse the [platformio] section into a typed dataclass. + """ + if not config.has_section("platformio"): + return None + + section = config["platformio"] + + # Parse default_envs and extra_configs as lists + default_envs = None + if "default_envs" in section: + default_envs = _parse_list_value(section["default_envs"]) + + extra_configs = None + if "extra_configs" in section: + extra_configs = _parse_list_value(section["extra_configs"]) + + return PlatformIOSection( + name=section.get("name"), + description=section.get("description"), + default_envs=default_envs, + extra_configs=extra_configs, + core_dir=section.get("core_dir"), + globallib_dir=section.get("globallib_dir"), + platforms_dir=section.get("platforms_dir"), + packages_dir=section.get("packages_dir"), + cache_dir=section.get("cache_dir"), + build_cache_dir=section.get("build_cache_dir"), + workspace_dir=section.get("workspace_dir"), + build_dir=section.get("build_dir"), + libdeps_dir=section.get("libdeps_dir"), + include_dir=section.get("include_dir"), + src_dir=section.get("src_dir"), + lib_dir=section.get("lib_dir"), + data_dir=section.get("data_dir"), + test_dir=section.get("test_dir"), + boards_dir=section.get("boards_dir"), + monitor_dir=section.get("monitor_dir"), + shared_dir=section.get("shared_dir"), + ) + + +def _merge_env_sections( + base_env: EnvironmentSection, child_env: EnvironmentSection +) -> EnvironmentSection: + """ + Merge a base environment section with a child that extends it. + Child values override base values, except for lists which are merged. + """ + from dataclasses import replace + + # Start with the child environment + merged = replace(child_env) + + # Override with base values only if child doesn't have them + if not merged.platform and base_env.platform: + merged.platform = base_env.platform + if not merged.framework and base_env.framework: + merged.framework = base_env.framework + if not merged.board and base_env.board: + merged.board = base_env.board + if not merged.build_type and base_env.build_type: + merged.build_type = base_env.build_type + if not merged.lib_ldf_mode and base_env.lib_ldf_mode: + merged.lib_ldf_mode = base_env.lib_ldf_mode + if not merged.upload_port and base_env.upload_port: + merged.upload_port = base_env.upload_port + if not merged.upload_protocol and base_env.upload_protocol: + merged.upload_protocol = base_env.upload_protocol + if not merged.upload_speed and base_env.upload_speed: + merged.upload_speed = base_env.upload_speed + if not merged.monitor_port and base_env.monitor_port: + merged.monitor_port = base_env.monitor_port + if not merged.monitor_speed and base_env.monitor_speed: + merged.monitor_speed = base_env.monitor_speed + if not merged.board_build_mcu and base_env.board_build_mcu: + merged.board_build_mcu = base_env.board_build_mcu + if not merged.board_build_f_cpu and base_env.board_build_f_cpu: + merged.board_build_f_cpu = base_env.board_build_f_cpu + if not merged.board_build_partitions and base_env.board_build_partitions: + merged.board_build_partitions = base_env.board_build_partitions + if not merged.check_tool and base_env.check_tool: + merged.check_tool = base_env.check_tool + + # For list fields, prepend base values to child values (child has priority) + if base_env.build_flags: + merged.build_flags = base_env.build_flags + merged.build_flags + if base_env.build_src_filter: + merged.build_src_filter = base_env.build_src_filter + merged.build_src_filter + if base_env.targets: + merged.targets = base_env.targets + merged.targets + if base_env.lib_deps: + merged.lib_deps = base_env.lib_deps + merged.lib_deps + if base_env.lib_ignore: + merged.lib_ignore = base_env.lib_ignore + merged.lib_ignore + if base_env.lib_extra_dirs: + merged.lib_extra_dirs = base_env.lib_extra_dirs + merged.lib_extra_dirs + if base_env.monitor_filters: + merged.monitor_filters = base_env.monitor_filters + merged.monitor_filters + if base_env.extra_scripts: + merged.extra_scripts = base_env.extra_scripts + merged.extra_scripts + + # Merge custom options (child overrides base) + if base_env.custom_options: + merged_custom = base_env.custom_options.copy() + merged_custom.update(merged.custom_options) + merged.custom_options = merged_custom + + return merged + + +def _parse_env_section( + config: configparser.ConfigParser, section_name: str +) -> EnvironmentSection: + """ + Parse an environment section into a typed dataclass. + """ + section = config[section_name] + + # Extract environment name from section name (e.g., "env:esp32s3" -> "esp32s3") + env_name = section_name[4:] if section_name.startswith("env:") else section_name + + # Parse list-based options with variable resolution + build_flags = _resolve_list_variables( + _parse_list_value(section.get("build_flags", "")), config + ) + build_src_filter = _resolve_list_variables( + _parse_list_value(section.get("build_src_filter", "")), config + ) + targets = _resolve_list_variables( + _parse_list_value(section.get("targets", "")), config + ) + lib_deps = _resolve_list_variables( + _parse_list_value(section.get("lib_deps", "")), config + ) + lib_ignore = _resolve_list_variables( + _parse_list_value(section.get("lib_ignore", "")), config + ) + lib_extra_dirs = _resolve_list_variables( + _parse_list_value(section.get("lib_extra_dirs", "")), config + ) + monitor_filters = _resolve_list_variables( + _parse_list_value(section.get("monitor_filters", "")), config + ) + extra_scripts = _resolve_list_variables( + _parse_list_value(section.get("extra_scripts", "")), config + ) + + # Handle custom options (anything not in standard fields) + standard_options = { + "platform", + "framework", + "board", + "extends", + "build_type", + "build_flags", + "build_src_filter", + "targets", + "lib_deps", + "lib_ignore", + "lib_extra_dirs", + "lib_ldf_mode", + "upload_port", + "upload_protocol", + "upload_speed", + "monitor_port", + "monitor_speed", + "monitor_filters", + "board_build.mcu", + "board_build.f_cpu", + "board_build.partitions", + "extra_scripts", + "check_tool", + "custom_sdkconfig", # Add known custom options + } + + custom_options: Dict[str, str] = {} + for key, value in section.items(): + if key not in standard_options: + custom_options[key] = value + + return EnvironmentSection( + name=env_name, + platform=_resolve_variable_substitution(section.get("platform", ""), config) + or None, + framework=_resolve_variable_substitution(section.get("framework", ""), config) + or None, + board=_resolve_variable_substitution(section.get("board", ""), config) or None, + extends=section.get("extends"), + build_type=section.get("build_type"), + build_flags=build_flags, + build_src_filter=build_src_filter, + targets=targets, + lib_deps=lib_deps, + lib_ignore=lib_ignore, + lib_extra_dirs=lib_extra_dirs, + lib_ldf_mode=section.get("lib_ldf_mode"), + upload_port=section.get("upload_port"), + upload_protocol=section.get("upload_protocol"), + upload_speed=section.get("upload_speed"), + monitor_port=section.get("monitor_port"), + monitor_speed=section.get("monitor_speed"), + monitor_filters=monitor_filters, + board_build_mcu=section.get("board_build.mcu"), + board_build_f_cpu=section.get("board_build.f_cpu"), + board_build_partitions=section.get("board_build.partitions"), + extra_scripts=extra_scripts, + check_tool=section.get("check_tool"), + custom_options=custom_options, + ) + + +def _parse_global_env_section( + config: configparser.ConfigParser, +) -> Optional[GlobalEnvSection]: + """ + Parse the [env] section into a typed dataclass. + """ + if not config.has_section("env"): + return None + + section = config["env"] + + # Parse list-based options + build_flags = _parse_list_value(section.get("build_flags", "")) + build_src_filter = _parse_list_value(section.get("build_src_filter", "")) + lib_deps = _parse_list_value(section.get("lib_deps", "")) + lib_ignore = _parse_list_value(section.get("lib_ignore", "")) + lib_extra_dirs = _parse_list_value(section.get("lib_extra_dirs", "")) + monitor_filters = _parse_list_value(section.get("monitor_filters", "")) + extra_scripts = _parse_list_value(section.get("extra_scripts", "")) + + # Handle custom options + standard_options = { + "build_flags", + "build_src_filter", + "lib_deps", + "lib_ignore", + "lib_extra_dirs", + "lib_ldf_mode", + "monitor_speed", + "monitor_filters", + "extra_scripts", + } + + custom_options: Dict[str, str] = {} + for key, value in section.items(): + if key not in standard_options: + custom_options[key] = value + + return GlobalEnvSection( + build_flags=build_flags, + build_src_filter=build_src_filter, + lib_deps=lib_deps, + lib_ignore=lib_ignore, + lib_extra_dirs=lib_extra_dirs, + lib_ldf_mode=section.get("lib_ldf_mode"), + monitor_speed=section.get("monitor_speed"), + monitor_filters=monitor_filters, + extra_scripts=extra_scripts, + custom_options=custom_options, + ) + + +class PlatformIOIni: + """ + A class for parsing and dumping platformio.ini files. + Provides a clean interface for managing INI file operations. + + Use static factory methods to create instances: + - PlatformIOIni.parseFile(file_path) - Parse from file + - PlatformIOIni.parseString(content) - Parse from string + - PlatformIOIni.create() - Create empty instance + """ + + config: configparser.ConfigParser + file_path: Optional[Path] + _parsed_config: Optional[ParsedPlatformIOConfig] = None + + @staticmethod + def parseFile(file_path: Path) -> "PlatformIOIni": + """ + Static factory method to parse a platformio.ini file and return a PlatformIOIni instance. + + Args: + file_path: Path to the platformio.ini file + + Returns: + PlatformIOIni instance with the parsed configuration + + Raises: + FileNotFoundError: If the file doesn't exist + configparser.Error: If the file is malformed + """ + instance = object.__new__(PlatformIOIni) + instance.config = configparser.ConfigParser() + instance.file_path = file_path + + if not file_path.exists(): + raise FileNotFoundError(f"platformio.ini not found: {file_path}") + + try: + instance.config.read(file_path) + instance._parsed_config = None # Will be lazily parsed + logger.debug(f"Successfully parsed platformio.ini: {file_path}") + except configparser.Error as e: + logger.error(f"Error parsing platformio.ini: {e}") + raise + + return instance + + @staticmethod + def parseString(content: str) -> "PlatformIOIni": + """ + Parse platformio.ini content from a string and return a fully formed instance. + + Args: + content: INI content as string + + Returns: + PlatformIOIni instance with the parsed configuration + + Raises: + configparser.Error: If the content is malformed + """ + instance = object.__new__(PlatformIOIni) + instance.config = configparser.ConfigParser() + instance.file_path = None + + try: + instance.config.read_string(content) + instance._parsed_config = None # Will be lazily parsed + logger.debug("Successfully parsed platformio.ini from string") + except configparser.Error as e: + logger.error(f"Error parsing platformio.ini from string: {e}") + raise + + return instance + + @staticmethod + def create() -> "PlatformIOIni": + """ + Static factory method to create an empty PlatformIOIni instance. + + Returns: + Empty PlatformIOIni instance + """ + instance = object.__new__(PlatformIOIni) + instance.config = configparser.ConfigParser() + instance.file_path = None + instance._parsed_config = None + return instance + + def dump(self, file_path: Path) -> None: + """ + Write the configuration to a platformio.ini file. + + Args: + file_path: Path to write the configuration to + """ + + # Write atomically using a temporary file + temp_file = file_path.with_suffix(".tmp") + try: + with open(temp_file, "w", encoding="utf-8") as f: + self.config.write(f) + temp_file.replace(file_path) + logger.debug(f"Successfully wrote platformio.ini: {file_path}") + except Exception as e: + if temp_file.exists(): + temp_file.unlink() + logger.error(f"Failed to write platformio.ini: {e}") + raise + + def get_sections(self) -> List[str]: + """ + Get all section names in the configuration. + + Returns: + List of section names + """ + return self.config.sections() + + def get_env_sections(self) -> List[str]: + """ + Get all environment section names (sections starting with 'env:'). + + Returns: + List of environment section names + """ + return [ + section for section in self.config.sections() if section.startswith("env:") + ] + + def has_section(self, section: str) -> bool: + """ + Check if a section exists. + + Args: + section: Section name to check + + Returns: + True if section exists, False otherwise + """ + return self.config.has_section(section) + + def has_option(self, section: str, option: str) -> bool: + """ + Check if an option exists in a section. + + Args: + section: Section name + option: Option name to check + + Returns: + True if option exists in section, False otherwise + """ + return self.config.has_option(section, option) + + def get_option( + self, section: str, option: str, fallback: Optional[str] = None + ) -> Optional[str]: + """ + Get an option value from a section. + + Args: + section: Section name + option: Option name + fallback: Default value if option doesn't exist + + Returns: + Option value or fallback if not found + """ + if not self.config.has_section(section): + return fallback + return self.config.get(section, option, fallback=fallback) + + def set_option(self, section: str, option: str, value: str) -> None: + """ + Set an option value in a section. + + Args: + section: Section name + option: Option name + value: Option value + """ + if not self.config.has_section(section): + self.config.add_section(section) + self.config.set(section, option, value) + self.invalidate_cache() # Invalidate cache when config changes + + def remove_option(self, section: str, option: str) -> bool: + """ + Remove an option from a section. + + Args: + section: Section name + option: Option name + + Returns: + True if option was removed, False if it didn't exist + """ + result = self.config.remove_option(section, option) + if result: + self.invalidate_cache() # Invalidate cache when config changes + return result + + def get_platform_urls(self) -> List[Tuple[str, str, str]]: + """ + Get all platform URLs from environment sections. + + Returns: + List of tuples: (section_name, option_name, url) + """ + urls: List[Tuple[str, str, str]] = [] + for section in self.get_env_sections(): + platform_value = self.get_option(section, "platform") + if platform_value: + urls.append((section, "platform", platform_value)) + return urls + + def get_framework_urls(self) -> List[Tuple[str, str, str]]: + """ + Get all framework URLs from environment sections. + + Returns: + List of tuples: (section_name, option_name, url) + """ + urls: List[Tuple[str, str, str]] = [] + for section in self.get_env_sections(): + framework_value = self.get_option(section, "framework") + if framework_value: + urls.append((section, "framework", framework_value)) + return urls + + def replace_url( + self, section: str, option: str, old_url: str, new_url: str + ) -> bool: + """ + Replace a URL in the configuration. + + Args: + section: Section name + option: Option name + old_url: URL to replace + new_url: New URL value + + Returns: + True if replacement was made, False otherwise + """ + replacement_made = False + current_value = self.get_option(section, option) + if current_value == old_url: + self.set_option(section, option, new_url) + replacement_made = True + return replacement_made + + def validate_structure(self) -> List[str]: + """ + Validate the platformio.ini structure and return any issues. + + Returns: + List of validation issues (empty if valid) + """ + issues: List[str] = [] + + # Check for at least one environment section + env_sections = self.get_env_sections() + if not env_sections: + issues.append( + "No environment sections found (sections starting with 'env:')" + ) + + # Validate each environment section + for section in env_sections: + # Check for required fields + if not self.has_option(section, "platform"): + issues.append(f"Section '{section}' missing required 'platform' option") + + # Check for board specification + if not self.has_option(section, "board"): + issues.append(f"Section '{section}' missing 'board' option") + + return issues + + def to_dict(self) -> Dict[str, Dict[str, str]]: + """ + Convert the configuration to a dictionary. + + Returns: + Dictionary representation of the configuration + """ + result: Dict[str, Dict[str, str]] = {} + for section_name in self.config.sections(): + result[section_name] = dict(self.config[section_name]) + return result + + def from_dict(self, data: Dict[str, Dict[str, str]]) -> None: + """ + Load configuration from a dictionary. + + Args: + data: Dictionary with section names as keys and option dictionaries as values + """ + self.config.clear() + self._parsed_config = None # Invalidate parsed cache + for section_name, options in data.items(): + self.config.add_section(section_name) + for option, value in options.items(): + self.config.set(section_name, option, value) + + @property + def parsed(self) -> ParsedPlatformIOConfig: + """ + Get the parsed, typed configuration. This property is lazily computed and cached. + + Returns: + ParsedPlatformIOConfig with all sections parsed into typed dataclasses + """ + if self._parsed_config is None: + self._parsed_config = self._parse_config() + return self._parsed_config + + def _parse_config(self) -> ParsedPlatformIOConfig: + """ + Parse the raw configparser data into typed dataclasses. + + Returns: + ParsedPlatformIOConfig with all sections parsed + """ + # Parse [platformio] section + platformio_section = _parse_platformio_section(self.config) + + # Parse [env] section + global_env_section = _parse_global_env_section(self.config) + + # Parse all [env:*] sections + environments: Dict[str, EnvironmentSection] = {} + for section_name in self.get_env_sections(): + env_name = section_name[4:] # Remove "env:" prefix + environments[env_name] = _parse_env_section(self.config, section_name) + + # Resolve inheritance (extends) + resolved_environments: Dict[str, EnvironmentSection] = {} + for env_name, env_section in environments.items(): + resolved_env = self._resolve_inheritance( + env_section, environments, global_env_section + ) + resolved_environments[env_name] = resolved_env + + return ParsedPlatformIOConfig( + platformio_section=platformio_section, + global_env_section=global_env_section, + environments=resolved_environments, + ) + + def _resolve_inheritance( + self, + env_section: EnvironmentSection, + all_environments: Dict[str, EnvironmentSection], + global_env: Optional[GlobalEnvSection], + ) -> EnvironmentSection: + """ + Resolve inheritance for an environment section. + """ + if not env_section.extends: + # Apply global environment settings if no inheritance + return self._apply_global_env(env_section, global_env) + + # Parse extends value (remove "env:" prefix if present) + base_env_name = env_section.extends + if base_env_name.startswith("env:"): + base_env_name = base_env_name[4:] + + # Get the base environment + if base_env_name not in all_environments: + logger.warning( + f"Environment '{env_section.name}' extends non-existent environment '{base_env_name}'" + ) + return self._apply_global_env(env_section, global_env) + + # Recursively resolve base environment inheritance + base_env = self._resolve_inheritance( + all_environments[base_env_name], all_environments, global_env + ) + + # Merge base and child environments + merged = _merge_env_sections(base_env, env_section) + return merged + + def _apply_global_env( + self, env_section: EnvironmentSection, global_env: Optional[GlobalEnvSection] + ) -> EnvironmentSection: + """ + Apply global environment settings to an environment section. + """ + if not global_env: + return env_section + + from dataclasses import replace + + # Apply global settings where environment doesn't have them + merged = replace(env_section) + + # String fields + if not merged.lib_ldf_mode and global_env.lib_ldf_mode: + merged.lib_ldf_mode = global_env.lib_ldf_mode + if not merged.monitor_speed and global_env.monitor_speed: + merged.monitor_speed = global_env.monitor_speed + + # List fields - prepend global values + if global_env.build_flags: + merged.build_flags = global_env.build_flags + merged.build_flags + if global_env.build_src_filter: + merged.build_src_filter = ( + global_env.build_src_filter + merged.build_src_filter + ) + if global_env.lib_deps: + merged.lib_deps = global_env.lib_deps + merged.lib_deps + if global_env.lib_ignore: + merged.lib_ignore = global_env.lib_ignore + merged.lib_ignore + if global_env.lib_extra_dirs: + merged.lib_extra_dirs = global_env.lib_extra_dirs + merged.lib_extra_dirs + if global_env.monitor_filters: + merged.monitor_filters = global_env.monitor_filters + merged.monitor_filters + if global_env.extra_scripts: + merged.extra_scripts = global_env.extra_scripts + merged.extra_scripts + + # Custom options + if global_env.custom_options: + merged_custom = global_env.custom_options.copy() + merged_custom.update(merged.custom_options) + merged.custom_options = merged_custom + + return merged + + def get_platformio_section(self) -> Optional[PlatformIOSection]: + """ + Get the typed [platformio] section. + + Returns: + PlatformIOSection instance or None if section doesn't exist + """ + return self.parsed.platformio_section + + def get_global_env_section(self) -> Optional[GlobalEnvSection]: + """ + Get the typed [env] section. + + Returns: + GlobalEnvSection instance or None if section doesn't exist + """ + return self.parsed.global_env_section + + def get_environment(self, env_name: str) -> Optional[EnvironmentSection]: + """ + Get a typed environment section by name. + + Args: + env_name: Environment name (without "env:" prefix) + + Returns: + EnvironmentSection instance or None if environment doesn't exist + """ + return self.parsed.environments.get(env_name) + + def get_all_environments(self) -> Dict[str, EnvironmentSection]: + """ + Get all typed environment sections. + + Returns: + Dictionary mapping environment names to EnvironmentSection instances + """ + return self.parsed.environments.copy() + + def dump_all_attributes(self) -> Dict[str, Any]: + """ + Dump all parsed attributes as a dictionary for inspection. + + Returns: + Dictionary containing all parsed configuration data + """ + from dataclasses import asdict + + parsed = self.parsed + result: Dict[str, Any] = {} + + if parsed.platformio_section: + result["platformio"] = asdict(parsed.platformio_section) + + if parsed.global_env_section: + result["env"] = asdict(parsed.global_env_section) + + result["environments"] = {} + for env_name, env_section in parsed.environments.items(): + result["environments"][env_name] = asdict(env_section) + + return result + + def invalidate_cache(self) -> None: + """ + Invalidate the parsed configuration cache. + Call this method after making changes to the underlying configparser. + """ + self._parsed_config = None + + def _is_url(self, value: str) -> bool: + """Check if a value is already a URL (not a shorthand name).""" + if not value: + return False + return any( + value.startswith(scheme) for scheme in ("http://", "https://", "file://") + ) + + def _is_builtin_framework(self, framework_name: str) -> bool: + """Check if a framework is a built-in PlatformIO framework that should not be resolved to URLs.""" + if not framework_name: + return False + # List of built-in PlatformIO frameworks that should remain as names, not URLs + builtin_frameworks = { + "arduino", + "espidf", + "cmsis", + "libopencm3", + "mbed", + "freertos", + "simba", + "wiringpi", + "pumbaa", + "energia", + "spl", + "stm32cube", + "zephyr", + "framework-arduinoespressif32", + "framework-espidf", + } + return framework_name.lower() in builtin_frameworks + + def _is_git_url(self, url: str) -> bool: + """Check if a URL is a git repository URL.""" + if not url: + return False + # Git URLs typically end with .git or start with git:// + return url.endswith(".git") or url.startswith("git://") + + def _is_zip_url(self, url: str) -> bool: + """Check if a URL points to a zip file.""" + if not url: + return False + return url.endswith(".zip") or ".zip?" in url + + def _classify_url_type(self, url: str) -> str: + """Classify a URL as 'git', 'zip', 'file', or 'unknown'.""" + if not url: + return "unknown" + if self._is_zip_url(url): + return "zip" + elif self._is_git_url(url): + return "git" + elif url.startswith("file://") or ( + not self._is_url(url) and ("/" in url or "\\" in url) + ): + return "file" + else: + return "unknown" + + def _extract_packages_from_platform_data( + self, platform_data: Dict[str, Any] + ) -> List[PackageInfo]: + """Extract package information from PlatformIO platform data (legacy method).""" + packages: List[PackageInfo] = [] + packages_data = platform_data.get("packages", []) + + for pkg_data in packages_data: + if not isinstance(pkg_data, dict): + continue + + package = PackageInfo( + name=str(pkg_data.get("name", "")), # type: ignore + type=str(pkg_data.get("type", "")), # type: ignore + requirements=str(pkg_data.get("requirements", "")), # type: ignore + url=str(pkg_data.get("url", "")), # type: ignore + optional=bool(pkg_data.get("optional", True)), # type: ignore + version=pkg_data.get("version"), # type: ignore + description=pkg_data.get("description"), # type: ignore + ) + packages.append(package) + + return packages + + def _extract_packages_from_platform_response( + self, platform_show: PlatformShowResponse + ) -> List[PackageInfo]: + """Extract package information from typed PlatformShowResponse.""" + packages: List[PackageInfo] = [] + + for pkg_data in platform_show.packages: + if not isinstance(pkg_data, dict): + continue + + package = PackageInfo( + name=str(pkg_data.get("name", "")), # type: ignore + type=str(pkg_data.get("type", "")), # type: ignore + requirements=str(pkg_data.get("requirements", "")), # type: ignore + url=str(pkg_data.get("url", "")), # type: ignore + optional=bool(pkg_data.get("optional", True)), # type: ignore + version=pkg_data.get("version"), # type: ignore + description=pkg_data.get("description"), # type: ignore + ) + packages.append(package) + + return packages + + def _is_platform_cached(self, platform_name: str) -> bool: + """Check if platform resolution is cached and still valid.""" + if not hasattr(self, "_platform_cache"): + self._platform_cache: Dict[str, PlatformResolution] = {} + + if platform_name not in self._platform_cache: + return False + + resolution = self._platform_cache[platform_name] + if not resolution.resolved_at: + return False + + age = datetime.now() - resolution.resolved_at + return age < timedelta(hours=resolution.ttl_hours) + + def _is_framework_cached(self, framework_name: str) -> bool: + """Check if framework resolution is cached and still valid.""" + if not hasattr(self, "_framework_cache"): + self._framework_cache: Dict[str, FrameworkResolution] = {} + + if framework_name not in self._framework_cache: + return False + + resolution = self._framework_cache[framework_name] + if not resolution.resolved_at: + return False + + age = datetime.now() - resolution.resolved_at + return age < timedelta(hours=resolution.ttl_hours) + + def _run_pio_command( + self, args: List[str] + ) -> Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]: + """Run a PlatformIO CLI command and return JSON output.""" + try: + cmd = ["pio"] + args + logger.debug(f"Running PlatformIO command: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + check=True, + ) + + if result.stdout: + try: + parsed_output = json.loads(result.stdout) + # Return parsed output if it's a dict or list + if isinstance(parsed_output, dict): + return parsed_output # type: ignore[reportUnknownVariableType] + elif isinstance(parsed_output, list): + return parsed_output # type: ignore[reportUnknownVariableType] + else: + logger.warning(f"Unexpected JSON type: {type(parsed_output)}") + return None + except json.JSONDecodeError as e: + logger.error(f"Failed to parse PlatformIO JSON output: {e}") + logger.debug(f"Raw output: {result.stdout[:500]}") + return None + + return None + + except subprocess.TimeoutExpired: + cmd_str = " ".join(["pio"] + args) + logger.error(f"PlatformIO command timed out: {cmd_str}") + except subprocess.CalledProcessError as e: + cmd_str = " ".join(["pio"] + args) + logger.error(f"PlatformIO command failed: {e}") + logger.debug(f"Command output: {e.stdout}, Error: {e.stderr}") + except FileNotFoundError: + logger.error("PlatformIO CLI not found. Is it installed and in PATH?") + except Exception as e: + cmd_str = " ".join(["pio"] + args) + logger.error(f"Unexpected error running PlatformIO command: {e}") + + return None + + def _get_platform_show_typed( + self, platform_name: str + ) -> Optional[PlatformShowResponse]: + """Get typed platform information from PlatformIO CLI.""" + raw_data = self._run_pio_command( + ["platform", "show", platform_name, "--json-output"] + ) + if raw_data and isinstance(raw_data, dict): + try: + return PlatformShowResponse.from_dict(raw_data) # type: ignore + except Exception as e: + logger.error( + f"Failed to parse platform show response for {platform_name}: {e}" + ) + return None + return None + + def _get_frameworks_list_typed(self) -> List[FrameworkInfo]: + """Get typed list of available frameworks from PlatformIO CLI.""" + raw_data = self._run_pio_command(["platform", "frameworks", "--json-output"]) + if not raw_data: + return [] + + frameworks_list: List[FrameworkInfo] = [] + try: + # Handle both dict and list responses + if isinstance(raw_data, dict): + frameworks_data = [raw_data] + elif isinstance(raw_data, list): + frameworks_data = raw_data + else: + logger.warning("Unexpected frameworks data type from PlatformIO") + return [] + + for fw_data in frameworks_data: + if isinstance(fw_data, dict): + try: + framework = FrameworkInfo.from_dict(fw_data) + frameworks_list.append(framework) + except Exception as e: + logger.warning(f"Failed to parse framework data: {e}") + continue + + except Exception as e: + logger.error(f"Failed to parse frameworks list response: {e}") + return [] + + return frameworks_list + + def resolve_platform_url(self, platform_name: str) -> Optional[str]: + """ + Resolve a platform shorthand name to its repository URL. + + Args: + platform_name: Platform name like 'espressif32', 'atmelavr', etc. + + Returns: + The repository URL for the platform, or None if resolution fails. + """ + if self._is_url(platform_name): + return platform_name + + # Check cache first + if self._is_platform_cached(platform_name): + cached = self._platform_cache[platform_name] + logger.debug(f"Using cached platform resolution for {platform_name}") + return cached.repository_url + + # Resolve via PlatformIO CLI + platform_data = self._run_pio_command( + ["platform", "show", platform_name, "--json-output"] + ) + + if not platform_data or not isinstance(platform_data, dict): + logger.warning(f"Failed to resolve platform: {platform_name}") + return None + + # Extract repository URL from platform data + repository_url = platform_data.get("repository") + version = platform_data.get("version") + frameworks = platform_data.get("frameworks", []) + + # Cache the resolution + if not hasattr(self, "_platform_cache"): + self._platform_cache: Dict[str, PlatformResolution] = {} + + self._platform_cache[platform_name] = PlatformResolution( + name=platform_name, + repository_url=repository_url, + version=version, + frameworks=frameworks, + resolved_at=datetime.now(), + ) + + logger.debug(f"Resolved platform {platform_name} -> {repository_url}") + return repository_url + + def resolve_platform_url_enhanced( + self, platform_name: str + ) -> Optional[PlatformUrlResolution]: + """ + Resolve a platform shorthand name to comprehensive URL information. + + Args: + platform_name: Platform name like 'espressif32', 'atmelavr', etc. + + Returns: + PlatformUrlResolution with git_url, zip_url, packages, etc. or None if resolution fails. + """ + # Check if it's already a URL or local path + url_type = self._classify_url_type(platform_name) + if url_type in ("git", "zip", "file"): + resolution = PlatformUrlResolution(name=platform_name) + + if url_type == "git": + resolution.git_url = platform_name + elif url_type == "zip": + resolution.zip_url = platform_name + elif url_type == "file": + resolution.local_path = platform_name + + return resolution + + # Check cache first + if self._is_platform_cached(platform_name): + cached = self._platform_cache[platform_name] + logger.debug( + f"Using cached enhanced platform resolution for {platform_name}" + ) + if cached.enhanced_resolution: + return cached.enhanced_resolution + # Fall back to creating resolution from cached data + return PlatformUrlResolution( + name=platform_name, + git_url=cached.git_url, + zip_url=cached.zip_url, + version=cached.version, + frameworks=cached.frameworks, + ) + + # Resolve via PlatformIO CLI using typed response + platform_show = self._get_platform_show_typed(platform_name) + if not platform_show: + logger.warning(f"Failed to resolve platform: {platform_name}") + return None + + # Extract package information from typed response + packages = self._extract_packages_from_platform_response(platform_show) + + # Classify the repository URL type + git_url = None + zip_url = None + + if platform_show.repository: + url_type = self._classify_url_type(platform_show.repository) + if url_type == "git": + git_url = platform_show.repository + elif url_type == "zip": + zip_url = platform_show.repository + + # Look for additional zip URLs in packages (some platforms have main zip URLs in packages) + for package in packages: + if package.requirements and self._is_zip_url(package.requirements): + # Prefer platform-level zip URLs over individual package URLs + if not zip_url and package.type in ("", "framework"): + zip_url = package.requirements + + resolution = PlatformUrlResolution( + name=platform_name, + git_url=git_url, + zip_url=zip_url, + version=platform_show.version, + frameworks=platform_show.frameworks, + packages=packages, + homepage=platform_show.homepage, + ) + + # Cache the enhanced resolution + if not hasattr(self, "_platform_cache"): + self._platform_cache: Dict[str, PlatformResolution] = {} + + cached_resolution = PlatformResolution( + name=platform_name, + repository_url=git_url or zip_url, # For backward compatibility + version=platform_show.version, + frameworks=platform_show.frameworks, + resolved_at=datetime.now(), + enhanced_resolution=resolution, + ) + self._platform_cache[platform_name] = cached_resolution + + logger.debug( + f"Enhanced resolution for {platform_name}: git={git_url}, zip={zip_url}, packages={len(packages)}" + ) + return resolution + + def resolve_framework_url(self, framework_name: str) -> Optional[str]: + """ + Resolve a framework shorthand name to its URL. + + Args: + framework_name: Framework name like 'arduino', 'espidf', etc. + + Returns: + The URL for the framework, or None if resolution fails. + """ + if self._is_url(framework_name): + return framework_name + + # Special case for espidf - it's not in the global frameworks list but is commonly used + if framework_name == "espidf": + return "https://github.com/espressif/esp-idf" + + # Check cache first + if self._is_framework_cached(framework_name): + cached = self._framework_cache[framework_name] + logger.debug(f"Using cached framework resolution for {framework_name}") + return cached.url + + # Resolve via PlatformIO CLI using typed response + frameworks_list = self._get_frameworks_list_typed() + + if not frameworks_list: + logger.warning("Failed to get frameworks list from PlatformIO") + return None + + # Find the specific framework + framework_info: Optional[FrameworkInfo] = None + for fw in frameworks_list: + if fw.name == framework_name: + framework_info = fw + break + + if not framework_info: + logger.warning(f"Framework not found: {framework_name}") + return None + + # Extract URL from framework data + url = framework_info.url or framework_info.homepage + homepage = framework_info.homepage + platforms = framework_info.platforms + + # Cache the resolution + if not hasattr(self, "_framework_cache"): + self._framework_cache: Dict[str, FrameworkResolution] = {} + + self._framework_cache[framework_name] = FrameworkResolution( + name=framework_name, + url=url, + homepage=homepage, + platforms=platforms, + resolved_at=datetime.now(), + ) + + logger.debug(f"Resolved framework {framework_name} -> {url}") + return url + + def resolve_framework_url_enhanced( + self, framework_name: str + ) -> Optional[FrameworkUrlResolution]: + """ + Resolve a framework shorthand name to comprehensive URL information. + + Args: + framework_name: Framework name like 'arduino', 'espidf', etc. + + Returns: + FrameworkUrlResolution with git_url, zip_url, homepage, etc. or None if resolution fails. + """ + if self._is_url(framework_name): + # If it's already a URL, create a resolution based on URL type + url_type = self._classify_url_type(framework_name) + resolution = FrameworkUrlResolution(name=framework_name) + + if url_type == "git": + resolution.git_url = framework_name + elif url_type == "zip": + resolution.zip_url = framework_name + + return resolution + + # Check cache first + if self._is_framework_cached(framework_name): + cached = self._framework_cache[framework_name] + logger.debug( + f"Using cached enhanced framework resolution for {framework_name}" + ) + if cached.enhanced_resolution: + return cached.enhanced_resolution + # Fall back to creating resolution from cached data + return FrameworkUrlResolution( + name=framework_name, + git_url=cached.git_url, + zip_url=cached.zip_url, + homepage=cached.homepage, + platforms=cached.platforms, + url=cached.url, # Include the original URL + ) + + # Resolve via PlatformIO CLI using typed response + frameworks_list = self._get_frameworks_list_typed() + + if not frameworks_list: + logger.warning("Failed to get frameworks list from PlatformIO") + return None + + # Find the specific framework + framework_info: Optional[FrameworkInfo] = None + for fw in frameworks_list: + if fw.name == framework_name: + framework_info = fw + break + + if not framework_info: + logger.warning(f"Framework not found: {framework_name}") + return None + + # Classify URLs by type + git_url = None + zip_url = None + + # Check the main URL + if framework_info.url: + url_type = self._classify_url_type(framework_info.url) + if url_type == "git": + git_url = framework_info.url + elif url_type == "zip": + zip_url = framework_info.url + + # Check homepage URL as potential git repository + if framework_info.homepage and not git_url: + homepage_type = self._classify_url_type(framework_info.homepage) + if homepage_type == "git": + git_url = framework_info.homepage + + resolution = FrameworkUrlResolution( + name=framework_name, + git_url=git_url, + zip_url=zip_url, + homepage=framework_info.homepage, + platforms=framework_info.platforms, + version=framework_info.version, + title=framework_info.title, + description=framework_info.description, + url=framework_info.url, # Include the original URL from CLI response + ) + + # Cache the enhanced resolution + if not hasattr(self, "_framework_cache"): + self._framework_cache: Dict[str, FrameworkResolution] = {} + + cached_resolution = FrameworkResolution( + name=framework_name, + url=git_url or zip_url or framework_info.url, # For backward compatibility + homepage=framework_info.homepage, + platforms=framework_info.platforms, + resolved_at=datetime.now(), + enhanced_resolution=resolution, + ) + self._framework_cache[framework_name] = cached_resolution + + logger.debug( + f"Enhanced resolution for framework {framework_name}: git={git_url}, zip={zip_url}, homepage={framework_info.homepage}" + ) + return resolution + + def resolve_platform_urls(self) -> Dict[str, Optional[str]]: + """ + Resolve all platform shorthand names in the configuration to URLs. + + Returns: + Dictionary mapping platform names to their resolved URLs. + """ + resolutions: Dict[str, Optional[str]] = {} + + for section_name, option_name, platform_value in self.get_platform_urls(): + if platform_value and not self._is_url(platform_value): + if platform_value not in resolutions: + resolutions[platform_value] = self.resolve_platform_url( + platform_value + ) + + return resolutions + + def resolve_framework_urls(self) -> Dict[str, Optional[str]]: + """ + Resolve all framework shorthand names in the configuration to URLs. + + Returns: + Dictionary mapping framework names to their resolved URLs. + """ + resolutions: Dict[str, Optional[str]] = {} + + for section_name, option_name, framework_value in self.get_framework_urls(): + if ( + framework_value + and not self._is_url(framework_value) + and not self._is_builtin_framework(framework_value) + ): + if framework_value not in resolutions: + resolutions[framework_value] = self.resolve_framework_url( + framework_value + ) + + return resolutions + + def get_resolved_urls_cache(self) -> ResolvedUrlsCache: + """ + Get cached resolution results for inspection. + + Returns: + Strongly typed cache data with platform and framework resolutions. + """ + result = ResolvedUrlsCache() + + if hasattr(self, "_platform_cache"): + for name, resolution in self._platform_cache.items(): + cache_entry = PlatformCacheEntry( + repository_url=resolution.repository_url, + version=resolution.version, + frameworks=resolution.frameworks, + resolved_at=resolution.resolved_at.isoformat() + if resolution.resolved_at + else None, + expires_at=( + resolution.resolved_at + timedelta(hours=resolution.ttl_hours) + ).isoformat() + if resolution.resolved_at + else None, + ) + result.platforms[name] = cache_entry + + if hasattr(self, "_framework_cache"): + for name, resolution in self._framework_cache.items(): + cache_entry = FrameworkCacheEntry( + url=resolution.url, + homepage=resolution.homepage, + platforms=resolution.platforms, + resolved_at=resolution.resolved_at.isoformat() + if resolution.resolved_at + else None, + expires_at=( + resolution.resolved_at + timedelta(hours=resolution.ttl_hours) + ).isoformat() + if resolution.resolved_at + else None, + ) + result.frameworks[name] = cache_entry + + return result + + def invalidate_resolution_cache(self) -> None: + """Clear cached URL resolutions.""" + if hasattr(self, "_platform_cache"): + self._platform_cache.clear() + if hasattr(self, "_framework_cache"): + self._framework_cache.clear() + logger.debug("Cleared URL resolution cache") + + def __str__(self) -> str: + """Return string representation of the configuration.""" + output = io.StringIO() + self.config.write(output) + return output.getvalue() + + def optimize(self, cache: "PlatformIOCache", full: bool = False) -> None: + """ + Download all packages and swap URLs for local file path URLs. + + This method will: + 1. Resolve platform shorthand names to URLs using PlatformIO CLI (only for *.zip URLs when full=False) + 2. Replace shorthand names with resolved URLs in-place (only when full=True for frameworks) + 3. Find all platform and framework URLs that point to zip files + 4. Download and cache them using the provided PlatformIO cache system + 5. Replace the URLs in-place with local file:// URLs + + Args: + cache: PlatformIOCache instance to use for downloading and caching packages. + full: When True, resolve all platform/framework shorthand names to URLs. + When False (default), only optimize *.zip URLs for caching. + """ + # Import here to avoid circular dependencies + from ci.compiler.platformio_cache import _is_zip_web_url, handle_zip_artifact + + cache_manager = cache + + # Step 1: Resolve shorthand platform names to URLs (always happens) + logger.debug("Resolving platform shorthand names to URLs...") + platform_resolutions = self.resolve_platform_urls() + platform_replacements_made = 0 + + for platform_name, resolved_url in platform_resolutions.items(): + if resolved_url: + # Replace shorthand names with resolved URLs (regardless of whether they're zip URLs) + for ( + section_name, + option_name, + current_url, + ) in self.get_platform_urls(): + if current_url == platform_name: + self.replace_url( + section_name, option_name, platform_name, resolved_url + ) + platform_replacements_made += 1 + logger.debug( + f"Resolved platform {platform_name} -> {resolved_url} in {section_name}" + ) + + if platform_replacements_made > 0: + logger.info( + f"Resolved {platform_replacements_made} platform shorthand names" + ) + + # Step 2: Resolve shorthand framework names to URLs (always happens) + logger.debug("Resolving framework shorthand names to URLs...") + framework_resolutions = self.resolve_framework_urls() + framework_replacements_made = 0 + + for framework_name, resolved_url in framework_resolutions.items(): + if resolved_url: + # Replace shorthand names with resolved URLs (regardless of whether they're zip URLs) + for ( + section_name, + option_name, + current_url, + ) in self.get_framework_urls(): + if current_url == framework_name: + self.replace_url( + section_name, option_name, framework_name, resolved_url + ) + framework_replacements_made += 1 + logger.debug( + f"Resolved framework {framework_name} -> {resolved_url} in {section_name}" + ) + + if framework_replacements_made > 0: + logger.info( + f"Resolved {framework_replacements_made} framework shorthand names" + ) + + # Step 3: Find all platform and framework URLs that need optimization (*.zip caching always happens) + zip_artifacts: List[Tuple[str, bool, str]] = [] + + # Scan platform URLs + for section_name, option_name, url in self.get_platform_urls(): + if _is_zip_web_url(url): + zip_artifacts.append((url, False, section_name)) # False = platform + + # Scan framework URLs + for section_name, option_name, url in self.get_framework_urls(): + if _is_zip_web_url(url): + zip_artifacts.append((url, True, section_name)) # True = framework + + if not zip_artifacts: + logger.debug("No zip artifacts found to cache after URL resolution") + return + + # Step 4: Deduplicate artifacts by URL to avoid redundant processing + unique_artifacts: Dict[ + str, Tuple[bool, str] + ] = {} # url -> (is_framework, env_section) + for artifact_url, is_framework, env_section in zip_artifacts: + if artifact_url not in unique_artifacts: + unique_artifacts[artifact_url] = (is_framework, env_section) + + logger.info( + f"Processing {len(unique_artifacts)} unique zip artifacts for caching" + ) + + # Step 5: Process each unique artifact and collect URL replacements + replacements: Dict[str, str] = {} + for artifact_url, (is_framework, env_section) in unique_artifacts.items(): + resolved_path = handle_zip_artifact( + artifact_url, cache_manager, env_section + ) + replacements[artifact_url] = resolved_path + + # Step 6: Apply replacements to the configuration + if replacements: + # Replace platform URLs + for section_name, option_name, url in self.get_platform_urls(): + if url in replacements: + self.replace_url(section_name, option_name, url, replacements[url]) + + # Replace framework URLs + for section_name, option_name, url in self.get_framework_urls(): + if url in replacements: + self.replace_url(section_name, option_name, url, replacements[url]) + + logger.info(f"Replaced {len(replacements)} URLs with cached local paths") + + def optimize_enhanced(self, cache: "PlatformIOCache") -> None: + """ + Enhanced optimization using multi-value URL resolution. + + This method leverages the enhanced resolution system to: + 1. Resolve shorthand names to multiple URL types (git, zip) + 2. Prefer zip URLs for faster caching when available + 3. Fall back to git URLs when zip URLs are not available + 4. Update local_path in resolution results after caching + + Args: + cache: PlatformIOCache instance to use for downloading and caching packages. + """ + # Import here to avoid circular dependencies + from ci.compiler.platformio_cache import _is_zip_web_url, handle_zip_artifact + + cache_manager = cache + + # Step 1: Resolve shorthand platform names using enhanced resolution + logger.debug("Resolving platform shorthand names using enhanced resolution...") + platform_replacements_made = 0 + platform_resolutions: Dict[str, PlatformUrlResolution] = {} + + for section_name, option_name, platform_value in self.get_platform_urls(): + if platform_value and not self._is_url(platform_value): + if platform_value not in platform_resolutions: + resolution = self.resolve_platform_url_enhanced(platform_value) + if resolution: + platform_resolutions[platform_value] = resolution + + # Replace shorthand names with preferred URLs and track for caching + for platform_name, resolution in platform_resolutions.items(): + preferred_url = resolution.preferred_url + if preferred_url: + for section_name, option_name, current_url in self.get_platform_urls(): + if current_url == platform_name: + self.replace_url( + section_name, option_name, platform_name, preferred_url + ) + platform_replacements_made += 1 + logger.debug( + f"Enhanced platform resolution {platform_name} -> {preferred_url} in {section_name}" + ) + + if platform_replacements_made > 0: + logger.info( + f"Enhanced resolution of {platform_replacements_made} platform shorthand names" + ) + + # Step 2: Resolve shorthand framework names using enhanced resolution + logger.debug("Resolving framework shorthand names using enhanced resolution...") + framework_replacements_made = 0 + framework_resolutions: Dict[str, FrameworkUrlResolution] = {} + + for section_name, option_name, framework_value in self.get_framework_urls(): + if ( + framework_value + and not self._is_url(framework_value) + and not self._is_builtin_framework(framework_value) + ): + if framework_value not in framework_resolutions: + resolution = self.resolve_framework_url_enhanced(framework_value) + if resolution: + framework_resolutions[framework_value] = resolution + + # Replace shorthand names with preferred URLs + for framework_name, resolution in framework_resolutions.items(): + preferred_url = resolution.preferred_url + if preferred_url: + for section_name, option_name, current_url in self.get_framework_urls(): + if current_url == framework_name: + self.replace_url( + section_name, option_name, framework_name, preferred_url + ) + framework_replacements_made += 1 + logger.debug( + f"Enhanced framework resolution {framework_name} -> {preferred_url} in {section_name}" + ) + + if framework_replacements_made > 0: + logger.info( + f"Enhanced resolution of {framework_replacements_made} framework shorthand names" + ) + + # Step 3: Cache downloadable URLs and update local paths + zip_artifacts: List[Tuple[str, bool, str]] = [] + + # Scan for zip URLs that can be cached + for section_name, option_name, url in self.get_platform_urls(): + if _is_zip_web_url(url): + zip_artifacts.append((url, False, section_name)) + + for section_name, option_name, url in self.get_framework_urls(): + if _is_zip_web_url(url): + zip_artifacts.append((url, True, section_name)) + + if not zip_artifacts: + logger.debug("No zip URLs found to cache after enhanced resolution") + return + + # Process caching as before + unique_artifacts: Dict[str, Tuple[bool, str]] = {} + for artifact_url, is_framework, env_section in zip_artifacts: + if artifact_url not in unique_artifacts: + unique_artifacts[artifact_url] = (is_framework, env_section) + + logger.info( + f"Processing {len(unique_artifacts)} unique zip artifacts for enhanced caching" + ) + + # Cache artifacts and collect replacements + replacements: Dict[str, str] = {} + for artifact_url, (is_framework, env_section) in unique_artifacts.items(): + resolved_path = handle_zip_artifact( + artifact_url, cache_manager, env_section + ) + replacements[artifact_url] = resolved_path + + # Apply replacements and update resolution local_path fields + if replacements: + # Update platform resolutions with local paths + for platform_name, resolution in platform_resolutions.items(): + if resolution.zip_url in replacements: + resolution.local_path = replacements[resolution.zip_url] + elif resolution.git_url in replacements: + resolution.local_path = replacements[resolution.git_url] + + # Update framework resolutions with local paths + for framework_name, resolution in framework_resolutions.items(): + if resolution.zip_url in replacements: + resolution.local_path = replacements[resolution.zip_url] + elif resolution.git_url in replacements: + resolution.local_path = replacements[resolution.git_url] + + # Replace URLs in configuration + for section_name, option_name, url in self.get_platform_urls(): + if url in replacements: + self.replace_url(section_name, option_name, url, replacements[url]) + + for section_name, option_name, url in self.get_framework_urls(): + if url in replacements: + self.replace_url(section_name, option_name, url, replacements[url]) + + logger.info( + f"Enhanced caching: Replaced {len(replacements)} URLs with cached local paths" + ) diff --git a/v1.5/lib/FastLED/ci/compiler/test_compiler.py b/v1.5/lib/FastLED/ci/compiler/test_compiler.py new file mode 100644 index 0000000..ff292ce --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/test_compiler.py @@ -0,0 +1,983 @@ +#!/usr/bin/env python3 +""" +FastLED Test Build System - Python Compiler API Integration + +This module provides a high-performance test compilation system built on the proven +ci.util.ci.util.clang_compiler API that delivers 8x faster build times compared to CMake. + +Key Features: +- Parallel test compilation using ThreadPoolExecutor +- 90%+ cache hit rates with proven compiler settings +- Integration with build_unit.toml configuration +- Support for STUB platform testing without hardware dependencies +- GDB-compatible debug symbol generation +- Consistent performance across all platforms + +""" + +import hashlib +import os +import shutil +import subprocess +import sys +import tempfile +import textwrap +import time +import tomllib +from concurrent.futures import Future, as_completed +from dataclasses import dataclass, field +from pathlib import Path +from types import TracebackType +from typing import Any, Callable, Dict, List, Optional + +from ci.util.paths import PROJECT_ROOT + + +def optimize_python_command(cmd: list[str]) -> list[str]: + """ + Optimize command list for subprocess execution in uv environment. + + For python commands, we need to use 'uv run python' to ensure access to + installed packages like ziglang. Direct sys.executable bypasses uv environment. + + Args: + cmd: Command list that may contain 'python' as first element + + Returns: + list[str]: Optimized command with 'python' prefixed by 'uv run' + """ + if cmd and (cmd[0] == "python" or cmd[0] == "python3"): + # Use uv run python to ensure access to uv-managed packages + optimized_cmd = ["uv", "run", "python"] + cmd[1:] + return optimized_cmd + return cmd + + +from ci.compiler.clang_compiler import ( + BuildFlags, + BuildTools, + Compiler, + CompilerOptions, + LinkOptions, + Result, + create_compiler_options_from_toml, + link_program_sync, + load_build_flags_from_toml, +) + + +@dataclass +class TestExecutable: + """Represents a compiled test executable""" + + name: str + executable_path: Path + test_source_path: Path + + +@dataclass +class CompileResult: + """Result of test compilation operation""" + + success: bool + compiled_count: int + duration: float + errors: List["CompileError"] = field(default_factory=lambda: list()) + + +@dataclass +class CompileError: + """Compilation error for a specific test""" + + test_name: str + message: str + + +class FastLEDTestCompiler: + """ + Test compiler built on proven Compiler API for 8x faster builds. + + This class leverages the proven ci.util.ci.util.clang_compiler.Compiler API that has already + demonstrated 4x+ performance improvements in example compilation. By using the same + proven patterns and configurations, we inherit the optimized build flags, caching, + and parallel compilation capabilities. + """ + + # Class variable to store existing instance for get_existing_instance() + _existing_instance: Optional["FastLEDTestCompiler"] = None + + def __init__( + self, + compiler: Compiler, + build_dir: Path, + project_root: Path, + quick_build: bool = True, + strict_mode: bool = False, + no_unity: bool = False, + ): + self.compiler = compiler + self.build_dir = build_dir + self.project_root = project_root + self.quick_build = quick_build + self.strict_mode = strict_mode + self.no_unity = no_unity + self.compiled_tests: List[TestExecutable] = [] + self.linking_failures: List[str] = [] + self.build_flags = self._load_build_flags() + FastLEDTestCompiler._existing_instance = self + + def _load_build_flags(self) -> BuildFlags: + """Load build flags from TOML configuration""" + toml_path = self.project_root / "ci" / "build_unit.toml" + return BuildFlags.parse(toml_path, self.quick_build, self.strict_mode) + + @classmethod + def create_for_unit_tests( + cls, + project_root: Path, + clean_build: bool = False, + enable_static_analysis: bool = False, + specific_test: str | None = None, + quick_build: bool = True, + strict_mode: bool = False, + no_unity: bool = False, + ) -> "FastLEDTestCompiler": + """ + Create compiler configured for FastLED unit tests using TOML build flags. + + This configuration uses the new TOML-based build flag system: + - ci/build_unit.toml for centralized flag configuration + - Support for build modes (quick/debug) and strict mode + - STUB platform for hardware-free testing + - Precompiled headers for faster compilation + - Parallel compilation with ThreadPoolExecutor + """ + + # Set up build directory in .build/fled/unit for consistency + build_dir = project_root / ".build" / "fled" / "unit" + if clean_build and build_dir.exists(): + print("###########################") + print("# CLEANING UNIT TEST BUILD DIR #") + print("###########################") + import errno + import os + import shutil + import stat + import time + + def handle_remove_readonly( + func: Callable[[str], None], + path: str, + exc: tuple[type[BaseException], BaseException, TracebackType | None], + ) -> None: + """Error handler for Windows readonly files""" + if hasattr(exc[1], "errno") and exc[1].errno == errno.EACCES: # type: ignore + os.chmod(path, stat.S_IWRITE) + func(path) + else: + raise + + # Windows-compatible directory removal with retry + max_retries = 3 + for attempt in range(max_retries): + try: + # Use onerror parameter for compatibility across Python versions + shutil.rmtree(build_dir, onerror=handle_remove_readonly) + break + except (OSError, PermissionError) as e: + if attempt < max_retries - 1: + print( + f"Warning: Failed to remove build directory (attempt {attempt + 1}): {e}" + ) + time.sleep(0.01) # Brief pause before retry + continue + else: + print( + f"Warning: Could not remove build directory after {max_retries} attempts: {e}" + ) + print("Continuing with existing directory...") + break + build_dir.mkdir(parents=True, exist_ok=True) + + # Additional compiler args beyond TOML + additional_compiler_args = [ + f"-I{project_root}/src/platforms/stub", # STUB platform headers + ] + + # Load build flags using BuildFlags.parse() to get platform-specific configuration + toml_path = project_root / "ci" / "build_unit.toml" + build_flags = BuildFlags.parse(toml_path, quick_build, strict_mode) + + # Load TOML config for test-specific flags that aren't in BuildFlags + with open(toml_path, "rb") as f: + config = tomllib.load(f) + + # Get test-specific defines from TOML (not included in BuildFlags.parse) + test_defines = config.get("test", {}).get("defines", []) + + # Create compiler args from BuildFlags (includes platform-specific flags) + compiler_args: List[str] = [] + compiler_args.extend( + build_flags.compiler_flags + ) # Platform-aware compiler flags + compiler_args.extend(build_flags.include_flags) # Platform-aware include flags + + # Add strict mode flags if enabled (already handled by BuildFlags.parse) + + # Add additional compiler args + if additional_compiler_args: + compiler_args.extend(additional_compiler_args) + + # Extract defines without the "-D" prefix for CompilerOptions + defines: List[str] = [] + for define in test_defines: + if define.startswith("-D"): + defines.append(define[2:]) # Remove "-D" prefix + else: + defines.append(define) + + # Get tools configuration from BuildFlags (respects build_unit.toml) + # Use modern command-based approach + compiler_command = build_flags.tools.cpp_compiler + + print(f"Using compiler from build_unit.toml: {compiler_command}") + + # Use compiler command from TOML + flags as final compiler args + final_compiler_args = compiler_command + compiler_args + + # Create compiler options with TOML-loaded flags and tools + settings = CompilerOptions( + include_path=str(project_root / "src"), + defines=defines, + compiler_args=final_compiler_args, # Use cache-aware compiler args + # Note: archiver and compiler fields are now handled via command-based approach + ) + + compiler = Compiler(settings, build_flags) + + # Create final instance with proper compiler and flags + instance = cls( + compiler, build_dir, project_root, quick_build, strict_mode, no_unity + ) + return instance + + def discover_test_files(self, specific_test: str | None = None) -> List[Path]: + """ + Discover test_*.cpp files using proven patterns. + + Uses the same file discovery patterns as the existing CMake system + to ensure 100% compatibility with current test discovery logic. + """ + tests_dir = Path(PROJECT_ROOT) / "tests" + test_files: List[Path] = [] + + # Find all test files in tests directory and subdirectories + for test_file in tests_dir.rglob("test_*.cpp"): + # Skip the doctest_main.cpp file (not a test file) + if test_file.name == "doctest_main.cpp": + continue + + # Handle both "test_name" and "name" formats for compatibility + test_stem = test_file.stem + test_name = test_stem.replace("test_", "") + + # Check if we should do fuzzy matching (if there's a * in the name) + if specific_test: + if "*" in specific_test: + # Convert glob pattern to regex pattern + import re + + pattern = specific_test.replace("*", ".*") + if re.search(pattern, test_stem) or re.search(pattern, test_name): + test_files.append(test_file) + else: + # Exact matching - either the full name or with test_ prefix (case-insensitive) + if ( + test_stem.lower() == specific_test.lower() + or test_stem.lower() == f"test_{specific_test}".lower() + or test_name.lower() == specific_test.lower() + ): + test_files.append(test_file) + else: + test_files.append(test_file) + + return test_files + + def compile_all_tests(self, specific_test: str | None = None) -> CompileResult: + """ + Compile all tests in parallel using proven API patterns. + + This method uses the same parallel compilation patterns proven in + example compilation that deliver 4x+ performance improvements. + """ + compile_start = time.time() + + # If specific_test is not provided in the method call, use the one from discover_test_files + test_files = self.discover_test_files(specific_test) + + if not test_files: + return CompileResult(success=True, compiled_count=0, duration=0.0) + + print(f"Compiling {len(test_files)} test files using proven Python API...") + # Convert absolute path to relative for display + rel_build_dir = os.path.relpath(self.build_dir) + print(f"Build directory: {rel_build_dir}") + + # Always show build configuration in unit test mode + print("\nFastLED Library Build Configuration:") + if self.no_unity: + print(" ❌ Unity build disabled - Compiling individual source files") + else: + print(" ✅ Unity build enabled - Using glob pattern:") + print(" src/**/*.cpp") + + print("\nPrecompiled header status:") + if self.compiler.settings.use_pch: + print(" ✅ PCH enabled - Using precompiled headers for faster compilation") + print(" PCH content:") + print(textwrap.indent(self.compiler.generate_pch_header(), " ")) + + # Create PCH for faster compilation + print("\n🔧 Creating precompiled header...") + pch_success = self.compiler.create_pch_file() + if pch_success: + print(" ✅ PCH created successfully") + else: + print(" ❌ PCH creation failed - continuing with direct compilation") + else: + print(" ❌ PCH disabled - Not using precompiled headers") + + print("\nCompiler flags:") + for flag in self.compiler.get_compiler_args(): + print(f" {flag}") + print("\nLinker flags:") + for flag in self._get_platform_linker_args(self.build_dir / "libfastled.lib"): + print(f" {flag}") + print("") + + # Print list of test files being compiled + print("Test files to compile:") + for i, test_file in enumerate(test_files, 1): + print(f" {i}. {test_file.name}") + print("") + + # Submit parallel compilation jobs (proven pattern from example compilation) + future_to_test: Dict[Future[Result], Path] = {} + + # Check if we're running in parallel or sequential mode + if os.environ.get("NO_PARALLEL"): + print("Starting sequential compilation of test files...") + else: + print("Starting parallel compilation of test files...") + for test_file in test_files: + # Compile to object file first (since compile_cpp_file uses -c flag) + obj_path = self.build_dir / f"{test_file.stem}.o" + # Convert absolute path to relative for display + rel_obj_path = os.path.relpath(obj_path) + print(f"Submitting compilation job for: {test_file.name} -> {rel_obj_path}") + # Show compilation command if enabled + if os.environ.get("FASTLED_TEST_SHOW_COMPILE", "").lower() in ( + "1", + "true", + "yes", + ): + # Convert absolute path to relative for display + rel_obj_path = os.path.relpath(obj_path) + print(f"[COMPILE] {test_file.name} -> {rel_obj_path}") + + compile_future = self.compiler.compile_cpp_file( + test_file, + output_path=obj_path, + additional_flags=[ + "-I", + str(self.project_root / "src"), + "-I", + str(self.project_root / "tests"), + # NOTE: Compiler flags now come from build_unit.toml + ], + ) + future_to_test[compile_future] = test_file + + # Collect compilation results with progress reporting + compiled_objects: List[Path] = [] + errors: List[CompileError] = [] + completed = 0 + + print(f"Waiting for {len(future_to_test)} compilation jobs to complete...") + + try: + for future in as_completed(future_to_test.keys()): + test_file = future_to_test[future] + result: Result = future.result() + completed += 1 + + if result.ok: + obj_path = self.build_dir / f"{test_file.stem}.o" + compiled_objects.append(obj_path) + print(f"[{completed}/{len(test_files)}] Compiled {test_file.name}") + else: + errors.append( + CompileError( + test_name=test_file.stem, + message=result.stderr + or result.stdout + or "Compilation failed", + ) + ) + print( + f"[{completed}/{len(test_files)}] FAILED {test_file.name}: {result.stderr}" + ) + except KeyboardInterrupt: + print("\nKeyboard interrupt detected during compilation") + # Clean up any in-progress compilations + for future in future_to_test.keys(): + future.cancel() + import _thread + + _thread.interrupt_main() + raise + + if errors: + duration = time.time() - compile_start + print(f"Compilation failed with {len(errors)} errors in {duration:.2f}s") + return CompileResult( + success=False, compiled_count=0, duration=duration, errors=errors + ) + + print(f"All {len(compiled_objects)} object files compiled successfully") + + # Link each test to executable using proven linking API + self.compiled_tests = self._link_tests(compiled_objects) + + # Check for linking failures and add them to errors + if hasattr(self, "linking_failures") and self.linking_failures: + for failure in self.linking_failures: + # Parse the failure string to extract test name and error + if ":" in failure: + test_name, error_msg = failure.split(":", 1) + errors.append( + CompileError( + test_name=test_name.strip(), + message=f"Linking failed: {error_msg.strip()}", + ) + ) + + duration = time.time() - compile_start + + # Success requires both compilation AND linking to succeed + success = len(errors) == 0 and len(self.compiled_tests) > 0 + + if success: + print( + f"SUCCESS: Compiled and linked {len(self.compiled_tests)} tests in {duration:.2f}s" + ) + # List all the successful test executables + print("Test executables created:") + for i, test in enumerate(self.compiled_tests, 1): + # Convert absolute path to relative for display + rel_exe_path = os.path.relpath(test.executable_path) + print(f" {i}. {test.name} -> {rel_exe_path}") + else: + print(f"FAILED: {len(errors)} total failures (compilation + linking)") + + return CompileResult( + success=success, + compiled_count=len(self.compiled_tests), + duration=duration, + errors=errors, + ) + + def _link_tests(self, compiled_objects: List[Path]) -> List[TestExecutable]: + """Link each test to executable using proven linking API""" + print(f"Linking {len(compiled_objects)} test executables...") + + # First, build a complete FastLED library similar to CMake approach + fastled_lib_path = self._build_fastled_library() + + # Compile doctest_main.cpp once for all tests (provides main function and doctest implementation) + doctest_main_path = self.project_root / "tests" / "doctest_main.cpp" + doctest_obj_path = self.build_dir / "doctest_main.o" + + if not doctest_obj_path.exists(): + print("Compiling doctest_main.cpp...") + doctest_future = self.compiler.compile_cpp_file( + doctest_main_path, + output_path=doctest_obj_path, + additional_flags=["-c"], # Compile only + ) + doctest_result: Result = doctest_future.result() + if not doctest_result.ok: + print( + f"ERROR: Failed to compile doctest_main.cpp: {doctest_result.stderr}" + ) + return [] + + print(f"Compiled FastLED library and doctest_main") + + compiled_tests: List[TestExecutable] = [] + linking_failures: List[str] = [] + cache_hits = 0 + cache_misses = 0 + + # Link each test with the FastLED library + for obj_path in compiled_objects: + # Derive test name from object file + test_name = obj_path.stem.replace("test_", "") + exe_path = self.build_dir / f"test_{test_name}.exe" + + # Define clean execution path in tests/bin/ (this is where tests will actually run from) + tests_bin_dir = self.project_root / "tests" / "bin" + tests_bin_dir.mkdir(parents=True, exist_ok=True) + clean_exe_path = tests_bin_dir / f"test_{test_name}.exe" + + # Check if this test has its own main() function + test_source = self.project_root / "tests" / f"test_{test_name}.cpp" + has_own_main = self._test_has_own_main(test_source) + + # Get linker args (needed for cache key calculation) + linker_args = self._get_platform_linker_args(fastled_lib_path) + + # Calculate comprehensive cache key based on all linking inputs + if has_own_main: + # For standalone tests, only include test object file + cache_key = self._calculate_link_cache_key( + obj_path, fastled_lib_path, linker_args + ) + else: + # For doctest tests, include both test object and doctest_main object + # Calculate hashes for both object files and combine them + test_obj_hash = self._calculate_file_hash(obj_path) + doctest_obj_hash = self._calculate_file_hash(doctest_obj_path) + combined_obj_hash = hashlib.sha256( + f"{test_obj_hash}+{doctest_obj_hash}".encode("utf-8") + ).hexdigest() + + # Create a temporary cache key with combined object hash + fastled_hash = self._calculate_file_hash(fastled_lib_path) + linker_hash = self._calculate_linker_args_hash(linker_args) + combined = f"fastled:{fastled_hash}|test:{combined_obj_hash}|flags:{linker_hash}" + cache_key = hashlib.sha256(combined.encode("utf-8")).hexdigest()[:16] + + # Check for cached executable based on comprehensive cache key + cached_exe = self._get_cached_executable(test_name, cache_key) + if cached_exe: + print(f" {test_name}: Using cached executable (link cache hit)") + try: + shutil.copy2(cached_exe, exe_path) + # Also copy to clean execution location + shutil.copy2(cached_exe, clean_exe_path) + cache_hits += 1 + + test_exe = TestExecutable( + name=test_name, + executable_path=clean_exe_path, # Point to clean location for execution + test_source_path=test_source, + ) + compiled_tests.append(test_exe) + continue # Skip linking, we have cached result + except Exception as e: + print( + f" {test_name}: Warning - failed to copy cached executable, will relink: {e}" + ) + # Fall through to linking logic + + if has_own_main: + # Link standalone test without doctest_main.o + link_options = LinkOptions( + object_files=[obj_path], # Only the test object file + output_executable=exe_path, + linker=f'"{sys.executable}" -m ziglang c++', # Use optimized Python executable for linking + linker_args=linker_args, # Use pre-calculated args for consistency + ) + else: + # Link with the FastLED library instead of individual objects + link_options = LinkOptions( + object_files=[ + obj_path, + doctest_obj_path, + ], # Test object + doctest main + output_executable=exe_path, + linker=f'"{sys.executable}" -m ziglang c++', # Use optimized Python executable for linking + linker_args=linker_args, # Use pre-calculated args for consistency + ) + + # Show linking command if enabled + if os.environ.get("FASTLED_TEST_SHOW_LINK", "").lower() in ( + "1", + "true", + "yes", + ): + # Convert absolute path to relative for display + rel_exe_path = os.path.relpath(exe_path) + print(f"[LINK] {test_name} -> {rel_exe_path}") + + print(f"Linking test: {test_name}") + # STRICT: Provide explicit BuildFlags - NO defaults allowed + link_result: Result = link_program_sync( + link_options, self.compiler.build_flags + ) + + if not link_result.ok: + # Create more detailed error message showing link command context + error_msg = f" {test_name}: Linking failed - Linker: {link_options.linker}, Output: {link_options.output_executable}, Objects: {len(link_options.object_files)} files" + print(error_msg) + print(f" Error details: {link_result.stderr}") + linking_failures.append(f"{test_name}: {link_result.stderr}") + continue + + print(f" {test_name}: Linking successful") + cache_misses += 1 + + # Cache the newly linked executable for future use + self._cache_executable(test_name, cache_key, exe_path) + + # Copy to clean execution location + shutil.copy2(exe_path, clean_exe_path) + + test_exe = TestExecutable( + name=test_name, + executable_path=clean_exe_path, + test_source_path=test_source, # Point to clean location for execution + ) + compiled_tests.append(test_exe) + + if linking_failures: + print(f"ERROR: {len(linking_failures)} linking failures:") + for failure in linking_failures: + print(f" {failure}") + # Store linking failures for later reporting + self.linking_failures = linking_failures + else: + self.linking_failures = [] + + # Report link cache statistics + total_tests = cache_hits + cache_misses + if total_tests > 0: + hit_rate = (cache_hits / total_tests) * 100 + print( + f"Link cache: {cache_hits} hits, {cache_misses} misses ({hit_rate:.1f}% hit rate) - caching by lib+test+flags" + ) + + print(f"Successfully linked {len(compiled_tests)} test executables") + return compiled_tests + + def _test_has_own_main(self, test_source_path: Path) -> bool: + """Check if a test file defines its own main() function""" + try: + if not test_source_path.exists(): + return False + + content = test_source_path.read_text(encoding="utf-8") + # Look for main function definition (simple pattern matching) + import re + + # Match "int main(" allowing for whitespace and various formats + main_pattern = r"\bint\s+main\s*\(" + return bool(re.search(main_pattern, content)) + except Exception: + # If we can't read the file, assume it doesn't have main + return False + + def _build_fastled_library(self) -> Path: + """Build a complete FastLED static library like CMake does""" + print("Building FastLED static library...") + + # Define library path + fastled_lib_path = self.build_dir / "libfastled.lib" + + # If library already exists, return it (for faster rebuilds) + if fastled_lib_path.exists(): + # Convert absolute path to relative for display + rel_lib_path = os.path.relpath(fastled_lib_path) + print(f"Using existing FastLED library: {rel_lib_path}") + return fastled_lib_path + + # Compile essential FastLED source files for STUB platform + # User directive: Just glob ALL .cpp files in src/** - no exclusions needed + import glob + + fastled_src_dir = self.project_root / "src" + # Include ALL .cpp files recursively - no platform exclusions + all_cpp_files: List[str] = glob.glob( + str(fastled_src_dir / "**" / "*.cpp"), recursive=True + ) + + fastled_sources: List[Path] = [] + for cpp_file in all_cpp_files: + # Include ALL files - no filtering needed + fastled_sources.append(Path(cpp_file)) + + if self.no_unity: + print( + f"Individual compilation: Found {len(fastled_sources)} FastLED .cpp files for compilation" + ) + else: + print( + f"Unity build mode: Found {len(fastled_sources)} FastLED .cpp files for compilation" + ) + + # Compile all FastLED source files to object files + fastled_objects: List[Path] = [] + for src_file in fastled_sources: + src_path = self.project_root / src_file + # Create unique object file name including directory path to avoid collisions + # Convert path separators to underscores to create valid filename + relative_path = src_path.relative_to(self.project_root / "src") + safe_name = ( + str(relative_path.with_suffix("")).replace("/", "_").replace("\\", "_") + ) + obj_path = self.build_dir / f"{safe_name}_fastled.o" + + if not obj_path.exists(): + future = self.compiler.compile_cpp_file( + src_path, + output_path=obj_path, + additional_flags=[ + "-c", # Compile only + "-DFASTLED_STUB_IMPL", # Essential for STUB platform + # NOTE: All defines and compiler flags now come from build_unit.toml + # Remove hardcoded defines - they should be in [test] section + ], + ) + result: Result = future.result() + if not result.ok: + print(f"ERROR: Failed to compile {src_file}: {result.stderr}") + # Continue with other files rather than failing completely + continue + + fastled_objects.append(obj_path) + + print(f"Creating static library from {len(fastled_objects)} object files...") + + # Create static library using ar (archiver) + # First try llvm-ar since it's more likely to be available + ar_cmd: List[str] = [ + "llvm-ar", # LLVM archiver tool (works with Clang) + "rcs", # Create archive with symbol table + str(fastled_lib_path), + ] + [str(obj) for obj in fastled_objects] + + # Create static library using ziglang ar (archiver) + print("Creating static library using ziglang ar...") + lib_cmd: List[str] = [ + "python", + "-m", + "ziglang", + "ar", + "rcs", # Create archive with symbol table + str(fastled_lib_path), + ] + [str(obj) for obj in fastled_objects] + + try: + # Optimize command to use sys.executable instead of shell 'python' resolution + python_exe = optimize_python_command(lib_cmd) + + # Use streaming to prevent buffer overflow + process = subprocess.Popen( + python_exe, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + encoding="utf-8", + errors="replace", + cwd=self.build_dir, + ) + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] + + while True: + stdout_line = process.stdout.readline() if process.stdout else "" + stderr_line = process.stderr.readline() if process.stderr else "" + + if stdout_line: + stdout_lines.append(stdout_line.rstrip()) + if stderr_line: + stderr_lines.append(stderr_line.rstrip()) + + if process.poll() is not None: + remaining_stdout = process.stdout.read() if process.stdout else "" + remaining_stderr = process.stderr.read() if process.stderr else "" + + if remaining_stdout: + for line in remaining_stdout.splitlines(): + stdout_lines.append(line.rstrip()) + if remaining_stderr: + for line in remaining_stderr.splitlines(): + stderr_lines.append(line.rstrip()) + break + + if process.returncode != 0: + print(f"ERROR: Failed to create static library: {stderr_lines}") + return fastled_lib_path # Return path even if creation failed + + # Convert absolute path to relative for display + rel_lib_path = os.path.relpath(fastled_lib_path) + print(f"Successfully created FastLED library: {rel_lib_path}") + return fastled_lib_path + + except Exception as e: + print(f"ERROR: Exception during library creation: {e}") + raise + + def get_test_executables( + self, specific_test: str | None = None + ) -> List[TestExecutable]: + """Get compiled test executables with name filtering""" + if specific_test: + # Check if we should do fuzzy matching (if there's a * in the name) + if "*" in specific_test: + # Convert glob pattern to regex pattern + import re + + pattern = specific_test.replace("*", ".*") + return [ + t + for t in self.compiled_tests + if re.search(pattern, t.name) + or re.search(pattern, f"test_{t.name}") + ] + else: + # Exact matching - either the full name or with test_ prefix (case-insensitive) + return [ + t + for t in self.compiled_tests + if ( + t.name.lower() == specific_test.lower() + or t.name.lower() == f"test_{specific_test}".lower() + or t.name.lower() == specific_test.replace("test_", "").lower() + ) + ] + return self.compiled_tests + + @property + def link_cache_dir(self) -> Path: + """Get the link cache directory""" + cache_dir = self.build_dir / "link_cache" + cache_dir.mkdir(exist_ok=True) + return cache_dir + + def _calculate_file_hash(self, file_path: Path) -> str: + """Calculate SHA256 hash of a file""" + if not file_path.exists(): + return "no_file" + + hash_sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + # Read in chunks to handle large files efficiently + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + return hash_sha256.hexdigest() + + def _calculate_linker_args_hash(self, linker_args: List[str]) -> str: + """Calculate SHA256 hash of linker arguments""" + # Convert linker args to a stable string representation + args_str = "|".join(sorted(linker_args)) + hash_sha256 = hashlib.sha256() + hash_sha256.update(args_str.encode("utf-8")) + return hash_sha256.hexdigest() + + def _calculate_link_cache_key( + self, test_obj_path: Path, fastled_lib_path: Path, linker_args: List[str] + ) -> str: + """Calculate comprehensive cache key for linking""" + # Hash all the factors that affect linking output + fastled_hash = self._calculate_file_hash(fastled_lib_path) + test_obj_hash = self._calculate_file_hash(test_obj_path) + linker_hash = self._calculate_linker_args_hash(linker_args) + + # Combine all hashes into a single cache key + combined = f"fastled:{fastled_hash}|test:{test_obj_hash}|flags:{linker_hash}" + final_hash = hashlib.sha256(combined.encode("utf-8")).hexdigest() + + return final_hash[:16] # Use first 16 chars for readability + + def _get_cached_executable(self, test_name: str, cache_key: str) -> Optional[Path]: + """Check if a cached executable exists for this test and cache key""" + cached_exe = self.link_cache_dir / f"{test_name}_{cache_key}.exe" + return cached_exe if cached_exe.exists() else None + + def _cache_executable(self, test_name: str, cache_key: str, exe_path: Path) -> None: + """Cache an executable with the given cache key""" + if not exe_path.exists(): + return + + cached_exe = self.link_cache_dir / f"{test_name}_{cache_key}.exe" + try: + shutil.copy2(exe_path, cached_exe) + print(f" {test_name}: Cached executable for future use") + except Exception as e: + print(f" {test_name}: Warning - failed to cache executable: {e}") + + def _get_platform_linker_args(self, fastled_lib_path: Path) -> List[str]: + """Get platform-specific linker arguments using BuildFlags""" + # Use the build_flags that includes platform-specific configuration + args = self.build_flags.link_flags.copy() + + # Load TOML config for test-specific and build mode flags + toml_path = self.project_root / "ci" / "build_unit.toml" + with open(toml_path, "rb") as f: + config = tomllib.load(f) + + # Add test-specific linking flags if available + test_link_flags = config.get("linking", {}).get("test", {}).get("flags", []) + args.extend(test_link_flags) + + # Add build mode specific flags (use test-specific debug mode) + mode = "quick" if self.quick_build else "test_debug" + mode_flags = config.get("build_modes", {}).get(mode, {}).get("link_flags", []) + args.extend(mode_flags) + + # Add FastLED library + args.append(str(fastled_lib_path)) # Link against the FastLED static library + + return args + + @classmethod + def get_existing_instance(cls) -> Optional["FastLEDTestCompiler"]: + """Get existing compiler instance for reuse""" + return cls._existing_instance + + +def check_iwyu_available() -> bool: + """Check if include-what-you-use is available (preserving existing logic)""" + import shutil + + return ( + shutil.which("include-what-you-use") is not None + or shutil.which("iwyu") is not None + ) + + +# For testing this module directly +if __name__ == "__main__": + print("Testing FastLEDTestCompiler...") + + # Create test compiler + test_compiler = FastLEDTestCompiler.create_for_unit_tests( + project_root=Path(PROJECT_ROOT), clean_build=True + ) + + # Discover test files + test_files = test_compiler.discover_test_files() + print(f"Discovered {len(test_files)} test files") + + # Test compilation of a few files + if test_files: + limited_files = test_files[:3] # Test first 3 files + print(f"Testing compilation of {len(limited_files)} test files...") + + compile_result = test_compiler.compile_all_tests() + + if compile_result.success: + print( + f"SUCCESS: Compiled {compile_result.compiled_count} tests in {compile_result.duration:.2f}s" + ) + executables = test_compiler.get_test_executables() + for exe in executables: + print(f" Executable: {exe.name} -> {exe.executable_path}") + else: + print(f"FAILED: {len(compile_result.errors)} errors") + for error in compile_result.errors: + print(f" {error.test_name}: {error.message}") diff --git a/v1.5/lib/FastLED/ci/compiler/test_example_compilation.py b/v1.5/lib/FastLED/ci/compiler/test_example_compilation.py new file mode 100644 index 0000000..ba9a77c --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/test_example_compilation.py @@ -0,0 +1,2595 @@ +#!/usr/bin/env python3 +""" +FastLED Example Compilation Testing Script +Ultra-fast compilation testing of Arduino .ino examples + +ENHANCED with Simple Build System Integration: +- Uses proven Compiler class infrastructure for actual compilation +- Preserves all existing functionality (system info, timing, error handling) +- Adds informative stubs for complex features +- Maintains compatibility with existing command-line interface +""" + +import argparse +import concurrent.futures +import hashlib +import os +import platform +import shutil +import stat +import subprocess +import sys +import tempfile +import time +import tomllib +from concurrent.futures import Future, as_completed +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Union + +import psutil +import toml # type: ignore + + +_IS_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" + +_TIMEOUT = 600 if _IS_GITHUB_ACTIONS else 120 + +# Add the parent directory to Python path for imports +# Import the proven Compiler infrastructure +from ci.compiler.clang_compiler import ( + BuildFlags, + Compiler, + CompilerOptions, + LinkOptions, + Result, +) +from ci.util.running_process import EndOfStream, RunningProcess + + +# Abort threshold to prevent flooding logs with repeated failures +MAX_FAILURES_BEFORE_ABORT = 3 + + +# Color output functions using ANSI escape codes +def red_text(text: str) -> str: + """Return text in red color.""" + return f"\033[31m{text}\033[0m" + + +def green_text(text: str) -> str: + """Return text in green color.""" + return f"\033[32m{text}\033[0m" + + +def orange_text(text: str) -> str: + """Return text in orange color.""" + return f"\033[33m{text}\033[0m" + + +@dataclass +class CompilationResult: + """Results from compiling a set of examples.""" + + successful_count: int + failed_count: int + compile_time: float + failed_examples: List[Dict[str, Any]] + object_file_map: Optional[Dict[Path, List[Path]]] + + +@dataclass +class LinkingResult: + """Results from linking examples into executable programs.""" + + linked_count: int + failed_count: int + cache_hits: int = 0 + cache_misses: int = 0 + + @property + def total_count(self) -> int: + """Total number of examples processed (linked + failed)""" + return self.linked_count + self.failed_count + + @property + def cache_hit_rate(self) -> float: + """Cache hit rate as percentage""" + total_processed = self.cache_hits + self.cache_misses + return (self.cache_hits / total_processed * 100) if total_processed > 0 else 0.0 + + +@dataclass +class ExecutionFailure: + """Represents a failed example execution with identifying info and reason.""" + + name: str + reason: str + stdout: str + + +def load_build_flags_toml(toml_path: str) -> Dict[str, Any]: + """Load and parse build_example.toml file.""" + try: + with open(toml_path, "rb") as f: + config = tomllib.load(f) + if not config: + raise RuntimeError( + f"build_example.toml at {toml_path} is empty or invalid" + ) + return config + except FileNotFoundError: + raise RuntimeError( + f"CRITICAL: build_example.toml not found at {toml_path}. This file is required for proper compilation flags." + ) + except Exception as e: + raise RuntimeError( + f"CRITICAL: Failed to parse build_example.toml at {toml_path}: {e}" + ) + + +def extract_compiler_flags_from_toml(config: Dict[str, Any]) -> List[str]: + """Extract compiler flags from TOML config - includes universal [all] flags and [build_modes.quick] flags.""" + flags: List[str] = [] + + # First, extract universal compiler flags from [all] section + if "all" in config and isinstance(config["all"], dict): + all_section: Dict[str, Any] = config["all"] # type: ignore + if "compiler_flags" in all_section and isinstance( + all_section["compiler_flags"], list + ): + compiler_flags: List[str] = all_section["compiler_flags"] # type: ignore + flags.extend(compiler_flags) + print( + f"[CONFIG] Loaded {len(compiler_flags)} universal compiler flags from [all] section" + ) + + # Then, extract flags from [build_modes.quick] section for sketch compilation + if "build_modes" in config and isinstance(config["build_modes"], dict): + build_modes: Dict[str, Any] = config["build_modes"] # type: ignore + if "quick" in build_modes and isinstance(build_modes["quick"], dict): + quick_section: Dict[str, Any] = build_modes["quick"] # type: ignore + + if "flags" in quick_section and isinstance(quick_section["flags"], list): + quick_flags: List[str] = quick_section["flags"] # type: ignore + flags.extend(quick_flags) + print( + f"[CONFIG] Loaded {len(quick_flags)} quick mode flags from [build_modes.quick] section" + ) + + return flags + + +def extract_stub_platform_defines_from_toml(config: Dict[str, Any]) -> List[str]: + """Extract stub platform defines from TOML config - uses [stub_platform] section.""" + defines: List[str] = [] + + # Extract defines from [stub_platform] section + if "stub_platform" in config and isinstance(config["stub_platform"], dict): + stub_section: Dict[str, Any] = config["stub_platform"] # type: ignore + if "defines" in stub_section and isinstance(stub_section["defines"], list): + stub_defines: List[str] = stub_section["defines"] # type: ignore + defines.extend(stub_defines) + print( + f"[CONFIG] Loaded {len(stub_defines)} stub platform defines from [stub_platform] section" + ) + else: + raise RuntimeError( + "CRITICAL: No 'defines' list found in [stub_platform] section. " + "This is required for stub platform compilation." + ) + else: + raise RuntimeError( + "CRITICAL: No [stub_platform] section found in build_example.toml. " + "This section is MANDATORY for stub platform compilation." + ) + + # Validate that all required defines are present + required_defines = [ + "STUB_PLATFORM", + "ARDUINO=", # Partial match for version number + "FASTLED_USE_STUB_ARDUINO", + "FASTLED_STUB_IMPL", + ] + + for required in required_defines: + if not any(define.startswith(required) for define in defines): + raise RuntimeError( + f"CRITICAL: Required stub platform define '{required}' not found in configuration. " + f"Please ensure [stub_platform] section contains all required defines." + ) + + return defines + + +def extract_stub_platform_include_paths_from_toml(config: Dict[str, Any]) -> List[str]: + """Extract stub platform include paths from TOML config - uses [stub_platform] section.""" + include_paths: List[str] = [] + + # Extract include paths from [stub_platform] section + if "stub_platform" in config and isinstance(config["stub_platform"], dict): + stub_section: Dict[str, Any] = config["stub_platform"] # type: ignore + if "include_paths" in stub_section and isinstance( + stub_section["include_paths"], list + ): + stub_include_paths: List[str] = stub_section["include_paths"] # type: ignore + include_paths.extend(stub_include_paths) + print( + f"[CONFIG] Loaded {len(stub_include_paths)} stub platform include paths from [stub_platform] section" + ) + else: + raise RuntimeError( + "CRITICAL: No 'include_paths' list found in [stub_platform] section. " + "This is required for stub platform compilation." + ) + else: + raise RuntimeError( + "CRITICAL: No [stub_platform] section found in build_example.toml. " + "This section is MANDATORY for stub platform compilation." + ) + + return include_paths + + +def get_system_info() -> Dict[str, Union[str, int, float]]: + """Get basic system configuration information. + + OPTIMIZED: Removed expensive subprocess calls for compiler detection + since we know the build system uses Zig/Clang after migration. + """ + try: + # Get CPU information + cpu_count = os.cpu_count() or 1 + + # Get memory information + memory = psutil.virtual_memory() + memory_gb = memory.total / (1024**3) + + # Get OS information + os_name = platform.system() + os_version = platform.release() + + # Use known compiler info since we're post-migration to Zig/Clang + # No expensive subprocess calls needed + compiler_info = "Zig/Clang (known)" + + return { + "os": f"{os_name} {os_version}", + "compiler": compiler_info, + "cpu_cores": cpu_count, + "memory_gb": memory_gb, + } + except Exception as e: + return { + "os": f"{platform.system()} {platform.release()}", + "compiler": "Zig/Clang (fallback)", + "cpu_cores": os.cpu_count() or 1, + "memory_gb": 8.0, # Fallback estimate + } + + +def get_build_configuration() -> Dict[str, Union[bool, str]]: + """Get build configuration information.""" + config: Dict[str, Union[bool, str]] = {} + + # Check unified compilation (note: simple build system uses direct compilation) + config["unified_compilation"] = False # Simple build system uses direct compilation + + # Compiler cache disabled + config["cache_type"] = "none" + + # Build mode for simple build system + config["build_mode"] = "simple_direct" + + return config + + +def format_file_size(size_bytes: int) -> str: + """Format file size in human readable format.""" + if size_bytes == 0: + return "0B" + + units = ["B", "KB", "MB", "GB"] + size = float(size_bytes) + unit_index = 0 + + while size >= 1024 and unit_index < len(units) - 1: + size /= 1024 + unit_index += 1 + + if unit_index == 0: + return f"{int(size)}{units[unit_index]}" + else: + return f"{size:.1f}{units[unit_index]}" + + +# Stub functions for complex features (with informative messages) +def generate_pch_header(source_filename: str) -> None: + """Stub: PCH generation not needed in simple build system.""" + print( + f"[INFO] PCH header generation for {source_filename} not implemented - using direct compilation" + ) + + +def detect_build_cache() -> bool: + """Stub: Build cache detection not implemented.""" + print("[INFO] Build cache detection not implemented - performing fresh compilation") + return False + + +def check_incremental_build() -> bool: + """Stub: Incremental builds not implemented.""" + print( + "[INFO] Incremental build detection not implemented - performing full compilation" + ) + return False + + +def optimize_parallel_jobs() -> int: + """Stub: Manual parallel job optimization not needed.""" + print( + "[INFO] Parallel job optimization handled automatically by ThreadPoolExecutor" + ) + cpu_count = os.cpu_count() or 1 + return cpu_count * 2 # Return sensible default + + +def check_pch_status(build_dir: Path) -> Dict[str, Union[bool, Path, int, str]]: + """Check PCH file status - stub for simple build system.""" + # Simple build system doesn't use PCH, but we maintain the interface + print("[INFO] PCH status check - simple build system uses direct compilation") + return {"exists": False, "path": None, "size": 0, "size_formatted": "0B"} # type: ignore + + +def create_fastled_compiler(use_pch: bool, parallel: bool) -> Compiler: + """Create compiler with standard FastLED settings for simple build system.""" + import os + import tempfile + + # Get absolute paths to ensure they work from any working directory + current_dir = os.getcwd() + src_path = os.path.join(current_dir, "src") + arduino_stub_path = os.path.join(current_dir, "src", "platforms", "stub") + + # Load build_example.toml configuration directly from ci/ directory + toml_path = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "build_example.toml" + ) + build_config = load_build_flags_toml( + toml_path + ) # Will raise RuntimeError if not found + + # Extract additional compiler flags from TOML (using ci/build_example.toml directly) + toml_flags = extract_compiler_flags_from_toml( + build_config + ) # Will raise RuntimeError if critical flags missing + print(f"Loaded {len(toml_flags)} total compiler flags from build_example.toml") + + # Extract stub platform defines from TOML configuration + stub_defines = extract_stub_platform_defines_from_toml( + build_config + ) # Will raise RuntimeError if critical defines missing + print(f"Loaded {len(stub_defines)} stub platform defines from build_example.toml") + + # Extract stub platform include paths from TOML configuration + stub_include_paths = extract_stub_platform_include_paths_from_toml( + build_config + ) # Will raise RuntimeError if critical paths missing + print( + f"Loaded {len(stub_include_paths)} stub platform include paths from build_example.toml" + ) + + # Base compiler settings - convert relative paths to absolute + base_args: list[str] = [] + for include_path in stub_include_paths: + if os.path.isabs(include_path): + base_args.append(f"-I{include_path}") + else: + # Convert relative path to absolute from project root + abs_path = os.path.join(current_dir, include_path) + base_args.append(f"-I{abs_path}") + print(f"[CONFIG] Added {len(base_args)} include paths from configuration") + + # Combine base args with TOML flags + all_args: List[str] = base_args + toml_flags + + # PCH output path in build cache directory (persistent) + pch_output_path = None + if use_pch: + cache_dir = Path(".build") / "cache" + cache_dir.mkdir(parents=True, exist_ok=True) + pch_output_path = str(cache_dir / "fastled_pch.hpp.pch") + + # Determine compiler command (with or without cache) + compiler_cmd = "python -m ziglang c++" + cache_args: List[str] = [] + print("Using direct compilation with ziglang c++") + + # Combine cache args with other args (cache args go first) + final_args: List[str] = cache_args + all_args + + settings = CompilerOptions( + include_path=src_path, + defines=stub_defines, # Use configuration-based defines instead of hardcoded ones + std_version="c++14", + compiler=compiler_cmd, + compiler_args=final_args, + use_pch=use_pch, + pch_output_path=pch_output_path, + parallel=parallel, + ) + # Load build flags from TOML + current_dir_path = Path(current_dir) + build_flags_path = current_dir_path / "ci" / "build_example.toml" + build_flags = BuildFlags.parse( + build_flags_path, quick_build=False, strict_mode=False + ) + + return Compiler(settings, build_flags) + + +def compile_examples_simple( + compiler: Compiler, + ino_files: List[Path], + pch_compatible_files: set[Path], + log_timing: Callable[[str], None], + full_compilation: bool, + verbose: bool = False, +) -> CompilationResult: + """ + Compile examples using the simple build system (Compiler class). + + Args: + compiler: The Compiler instance to use + ino_files: List of .ino files to compile + pch_compatible_files: Set of files that are PCH compatible + log_timing: Logging function + full_compilation: If True, preserve object files for linking; if False, use temp files + + Returns: + CompilationResult: Results from compilation including counts and failed examples + """ + compile_start = time.time() + results: List[Dict[str, Any]] = [] + + # Create build directory structure if full compilation is enabled + build_dir: Optional[Path] = None + if full_compilation: + build_dir = Path(".build/examples") + build_dir.mkdir(parents=True, exist_ok=True) + log_timing(f"[BUILD] Created build directory: {build_dir}") + + # Submit all compilation jobs with file tracking + future_to_file: Dict[Future[Result], Path] = {} + # Track object files for linking (if full_compilation enabled) + object_file_map: Dict[Path, List[Path]] = {} # ino_file -> [obj_files] + pch_files_count = 0 + direct_files_count = 0 + total_cpp_files = 0 + + for ino_file in ino_files: + # Find additional .cpp files in the same directory + cpp_files = compiler.find_cpp_files_for_example(ino_file) + if cpp_files: + total_cpp_files += len(cpp_files) + log_timing( + f"[SIMPLE] Found {len(cpp_files)} .cpp file(s) for {ino_file.name}: {[f.name for f in cpp_files]}" + ) + + # Find include directories for this example + include_dirs = compiler.find_include_dirs_for_example(ino_file) + if len(include_dirs) > 1: # More than just the example root directory + log_timing( + f"[SIMPLE] Found {len(include_dirs)} include dir(s) for {ino_file.name}: {[Path(d).name for d in include_dirs]}" + ) + + # Build additional flags with include directories + additional_flags: list[str] = [] + for include_dir in include_dirs: + additional_flags.append(f"-I{include_dir}") + + # Determine if this file should use PCH + use_pch_for_file = ino_file in pch_compatible_files + + # Set up object file paths for full compilation + ino_output_path: Optional[Path] = None + example_obj_files: List[Path] = [] + + if full_compilation and build_dir: + # Create example-specific build directory + example_name = ino_file.parent.name + example_build_dir = build_dir / example_name + example_build_dir.mkdir(exist_ok=True) + + # Set specific output path for .ino file + ino_output_path = example_build_dir / f"{ino_file.stem}.o" + example_obj_files.append(ino_output_path) + + # Compile the .ino file + if verbose: + pch_status = "with PCH" if use_pch_for_file else "direct compilation" + print( + f"[VERBOSE] Compiling {ino_file.relative_to(Path('examples'))} ({pch_status})" + ) + log_timing( + f"[VERBOSE] Compiling {ino_file.relative_to(Path('examples'))} ({pch_status})" + ) + + future = compiler.compile_ino_file( + ino_file, + output_path=ino_output_path, + use_pch_for_this_file=use_pch_for_file, + additional_flags=additional_flags, + ) + future_to_file[future] = ino_file + + if use_pch_for_file: + pch_files_count += 1 + else: + direct_files_count += 1 + + # Compile additional .cpp files in the same directory + for cpp_file in cpp_files: + cpp_output_path: Optional[Path] = None + + if full_compilation and build_dir: + example_name = ino_file.parent.name + example_build_dir = build_dir / example_name + cpp_output_path = example_build_dir / f"{cpp_file.stem}.o" + example_obj_files.append(cpp_output_path) + + if verbose: + pch_status = "with PCH" if use_pch_for_file else "direct compilation" + print( + f"[VERBOSE] Compiling {cpp_file.relative_to(Path('examples'))} ({pch_status})" + ) + log_timing( + f"[VERBOSE] Compiling {cpp_file.relative_to(Path('examples'))} ({pch_status})" + ) + + cpp_future = compiler.compile_cpp_file( + cpp_file, + output_path=cpp_output_path, + use_pch_for_this_file=use_pch_for_file, + additional_flags=additional_flags, + ) + future_to_file[cpp_future] = cpp_file + + if use_pch_for_file: + pch_files_count += 1 + else: + direct_files_count += 1 + + # Track object files for this example (for linking) + if full_compilation and example_obj_files: + object_file_map[ino_file] = example_obj_files + + total_files = len(ino_files) + total_cpp_files + log_timing( + f"[SIMPLE] Submitted {total_files} compilation jobs to ThreadPoolExecutor ({len(ino_files)} .ino + {total_cpp_files} .cpp)" + ) + log_timing( + f"[SIMPLE] Using PCH for {pch_files_count} files, direct compilation for {direct_files_count} files" + ) + + # Collect results as they complete with timeout to prevent hanging + completed_count = 0 + failure_count = 0 + for future in as_completed( + future_to_file.keys(), timeout=_TIMEOUT + ): # 2 minute total timeout + try: + result: Result = future.result(timeout=30) # 30 second timeout per file + source_file: Path = future_to_file[future] + completed_count += 1 + except Exception as e: + source_file: Path = future_to_file[future] + completed_count += 1 + print(f"ERROR: Compilation timed out or failed for {source_file}: {e}") + result = Result( + ok=False, stdout="", stderr=f"Compilation timeout: {e}", return_code=-1 + ) + + # Show verbose completion status or only final completion + if verbose: + status = "SUCCESS" if result.ok else "FAILED" + log_timing( + f"[VERBOSE] {status}: {source_file.relative_to(Path('examples'))} ({completed_count}/{total_files})" + ) + elif completed_count == total_files: + log_timing( + f"[SIMPLE] Completed {completed_count}/{total_files} compilations" + ) + + # Show compilation errors immediately for better debugging + if not result.ok and result.stderr.strip(): + failure_count += 1 + log_timing( + f"[ERROR] Compilation failed for {source_file.relative_to(Path('examples'))}:" + ) + error_lines = result.stderr.strip().split("\n") + for line in error_lines[:20]: # Limit to first 20 lines per file + log_timing(f"[ERROR] {line}") + if len(error_lines) > 20: + log_timing(f"[ERROR] ... ({len(error_lines) - 20} more lines)") + + if failure_count >= MAX_FAILURES_BEFORE_ABORT: + log_timing( + f"[ERROR] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining compilation jobs to avoid log spam." + ) + break + + file_result: Dict[str, Any] = { + "file": str(source_file.name), + "path": str(source_file.relative_to(Path("examples"))), + "success": bool(result.ok), + "stderr": str(result.stderr), + } + results.append(file_result) + + compile_time = time.time() - compile_start + + # Analyze results + successful = [r for r in results if r["success"]] + failed = [r for r in results if not r["success"]] + + log_timing( + f"[SIMPLE] Compilation completed: {len(successful)} succeeded, {len(failed)} failed" + ) + + # Report failures summary if any (detailed errors already shown above) + if failed: + if verbose: + log_timing( + f"[SIMPLE] Failed examples summary: {[f['path'] for f in failed]}" + ) + else: + # Show full details only in non-verbose mode since we didn't show them above + if len(failed) <= 10: + log_timing("[SIMPLE] Failed examples with full error details:") + for failure in failed[:10]: + log_timing(f"[SIMPLE] === FAILED: {failure['path']} ===") + if failure["stderr"]: + # Show full error message, not just preview + error_lines = failure["stderr"].strip().split("\n") + for line in error_lines: + log_timing(f"[SIMPLE] {line}") + else: + log_timing(f"[SIMPLE] No error details available") + log_timing(f"[SIMPLE] === END: {failure['path']} ===") + elif failed: + log_timing( + f"[SIMPLE] {len(failed)} examples failed (too many to list full details)" + ) + log_timing("[SIMPLE] First 10 failure summaries:") + for failure in failed[:10]: + err = "\n" + failure["stderr"] + error_preview = err if err.strip() else "No error details" + log_timing(f"[SIMPLE] {failure['path']}: {error_preview}...") + + return CompilationResult( + successful_count=len(successful), + failed_count=len(failed), + compile_time=compile_time, + failed_examples=failed, + object_file_map=object_file_map if full_compilation else None, + ) + + +def compile_examples_unity( + compiler: Compiler, + ino_files: list[Path], + log_timing: Callable[[str], None], + unity_custom_output: Optional[str] = None, + unity_additional_flags: Optional[List[str]] = None, +) -> CompilationResult: + """ + Compile FastLED examples using UNITY build approach. + + Creates a single unity.cpp file containing all .ino examples and their + associated .cpp files, then compiles everything as one unit. + """ + log_timing("[UNITY] Starting UNITY build compilation...") + + compile_start = time.time() + + # Collect all .cpp files from examples + all_cpp_files: list[Path] = [] + + for ino_file in ino_files: + # Convert .ino to .cpp (we'll create a temporary .cpp file) + ino_cpp = ino_file.with_suffix(".cpp") + + # Create temporary .cpp file from .ino + try: + ino_content = ino_file.read_text(encoding="utf-8", errors="ignore") + cpp_content = f"""#include "FastLED.h" +{ino_content} +""" + # Create temporary .cpp file + temp_cpp = Path(tempfile.gettempdir()) / f"unity_{ino_file.stem}.cpp" + temp_cpp.write_text(cpp_content, encoding="utf-8") + all_cpp_files.append(temp_cpp) + + # Also find any additional .cpp files in the example directory + example_dir = ino_file.parent + additional_cpp_files = compiler.find_cpp_files_for_example(ino_file) + all_cpp_files.extend(additional_cpp_files) + + except Exception as e: + log_timing(f"[UNITY] ERROR: Failed to process {ino_file.name}: {e}") + return CompilationResult( + successful_count=0, + failed_count=len(ino_files), + compile_time=0.0, + failed_examples=[ + { + "file": ino_file.name, + "path": str(ino_file.relative_to(Path("examples"))), + "success": False, + "stderr": f"Failed to prepare for UNITY build: {e}", + } + ], + object_file_map=None, + ) + + if not all_cpp_files: + log_timing("[UNITY] ERROR: No .cpp files to compile") + return CompilationResult( + successful_count=0, + failed_count=len(ino_files), + compile_time=0.0, + failed_examples=[ + { + "file": "unity_build", + "path": "unity_build", + "success": False, + "stderr": "No .cpp files found for UNITY build", + } + ], + object_file_map=None, + ) + + log_timing(f"[UNITY] Collected {len(all_cpp_files)} .cpp files for UNITY build") + + # Log advanced unity options if used + if unity_custom_output: + log_timing(f"[UNITY] Using custom output path: {unity_custom_output}") + if unity_additional_flags: + log_timing( + f"[UNITY] Using additional flags: {' '.join(unity_additional_flags)}" + ) + + # Parse additional flags properly - handle both "flag1 flag2" and ["flag1", "flag2"] formats + additional_flags = ["-c"] # Compile only, don't link + if unity_additional_flags: + for flag_group in unity_additional_flags: + # Split space-separated flags into individual flags + additional_flags.extend(flag_group.split()) + + # Create CompilerOptions for unity compilation (reuse the same settings as the main compiler) + unity_options = CompilerOptions( + include_path=compiler.settings.include_path, + compiler=compiler.settings.compiler, + defines=compiler.settings.defines, + std_version=compiler.settings.std_version, + compiler_args=compiler.settings.compiler_args, + use_pch=False, # Unity builds don't typically need PCH + additional_flags=additional_flags, + ) + + try: + # Perform UNITY compilation - pass unity_output_path as parameter + unity_future = compiler.compile_unity( + unity_options, all_cpp_files, unity_custom_output + ) + unity_result = unity_future.result() + + compile_time = time.time() - compile_start + + # Clean up temporary .cpp files + for cpp_file in all_cpp_files: + if cpp_file.name.startswith("unity_") and cpp_file.parent == Path( + tempfile.gettempdir() + ): + try: + cpp_file.unlink() + except: + pass # Ignore cleanup errors + + if unity_result.ok: + log_timing( + f"[UNITY] UNITY build completed successfully in {compile_time:.2f}s" + ) + log_timing(f"[UNITY] All {len(ino_files)} examples compiled as single unit") + + return CompilationResult( + successful_count=len(ino_files), + failed_count=0, + compile_time=compile_time, + failed_examples=[], + object_file_map=None, + ) + else: + log_timing(f"[UNITY] UNITY build failed: {unity_result.stderr}") + + return CompilationResult( + successful_count=0, + failed_count=len(ino_files), + compile_time=compile_time, + failed_examples=[ + { + "file": "unity_build", + "path": "unity_build", + "success": False, + "stderr": unity_result.stderr, + } + ], + object_file_map=None, + ) + + except Exception as e: + compile_time = time.time() - compile_start + + # Clean up temporary .cpp files on error + for cpp_file in all_cpp_files: + if cpp_file.name.startswith("unity_") and cpp_file.parent == Path( + tempfile.gettempdir() + ): + try: + cpp_file.unlink() + except: + pass # Ignore cleanup errors + + log_timing(f"[UNITY] UNITY build failed with exception: {e}") + + return CompilationResult( + successful_count=0, + failed_count=len(ino_files), + compile_time=compile_time, + failed_examples=[ + { + "file": "unity_build", + "path": "unity_build", + "success": False, + "stderr": f"UNITY build exception: {e}", + } + ], + object_file_map=None, + ) + + +def get_fastled_core_sources() -> List[Path]: + """Get essential FastLED .cpp files for library creation.""" + src_dir = Path("src") + + # Core FastLED files that must be included + core_files: List[Path] = [ + src_dir / "FastLED.cpp", + src_dir / "colorutils.cpp", + src_dir / "hsv2rgb.cpp", + ] + + # Find all .cpp files in key directories + additional_sources: List[Path] = [] + for pattern in ["*.cpp", "lib8tion/*.cpp", "platforms/stub/*.cpp"]: + additional_sources.extend(list(src_dir.glob(pattern))) + + # Include essential .cpp files from nested directories + additional_sources.extend(list(src_dir.rglob("*.cpp"))) + + # Filter out duplicates and ensure files exist + all_sources: List[Path] = [] + seen_files: set[Path] = set() + + for cpp_file in core_files + additional_sources: + # Skip stub_main.cpp since we create individual main.cpp files for each example + if cpp_file.name == "stub_main.cpp": + continue + + if cpp_file.exists() and cpp_file not in seen_files: + all_sources.append(cpp_file) + seen_files.add(cpp_file) + + return all_sources + + +def create_fastled_library( + compiler: Compiler, + fastled_build_dir: Path, + log_timing: Callable[[str], None], + verbose: bool, +) -> Path: + """Create libfastled.a static library.""" + + log_timing("[LIBRARY] Creating FastLED static library...") + + # Compile all FastLED sources to object files + fastled_sources = get_fastled_core_sources() + fastled_objects: List[Path] = [] + + obj_dir = fastled_build_dir / "obj" + obj_dir.mkdir(exist_ok=True) + + log_timing(f"[LIBRARY] Compiling {len(fastled_sources)} FastLED source files...") + + # Compile each source file + futures: List[tuple[Future[Result], Path, Path]] = [] + for cpp_file in fastled_sources: + # Create unique object file name by including relative path to prevent collisions + # Convert path separators to underscores to create valid filename + src_dir = Path("src") + if cpp_file.is_relative_to(src_dir): + rel_path = cpp_file.relative_to(src_dir) + else: + rel_path = cpp_file + + # Replace path separators with underscores for unique object file names + obj_name = str(rel_path.with_suffix(".o")).replace("/", "_").replace("\\", "_") + obj_file = obj_dir / obj_name + if verbose: + log_timing(f"[LIBRARY] Compiling src/{rel_path} -> {obj_file.name}") + future = compiler.compile_cpp_file(cpp_file, obj_file) + futures.append((future, obj_file, cpp_file)) + + # Wait for compilation to complete + compiled_count = 0 + failed_count = 0 + for future, obj_file, cpp_file in futures: + try: + result: Result = future.result() + if result.ok: + fastled_objects.append(obj_file) + compiled_count += 1 + if verbose: + log_timing( + f"[LIBRARY] SUCCESS: {cpp_file.relative_to(Path('src'))}" + ) + else: + failed_count += 1 + log_timing( + f"[LIBRARY] ERROR: Failed to compile {cpp_file.relative_to(Path('src'))}: {result.stderr[:300]}..." + ) + except Exception as e: + failed_count += 1 + log_timing( + f"[LIBRARY] ERROR: Exception compiling {cpp_file.relative_to(Path('src'))}: {e}" + ) + + log_timing( + f"[LIBRARY] Successfully compiled {compiled_count}/{len(fastled_sources)} FastLED sources" + ) + + # FAIL FAST: If any source files failed to compile, abort immediately + if failed_count > 0: + raise Exception( + f"CRITICAL: {failed_count} FastLED source files failed to compile. " + f"This indicates a serious build environment issue (likely PCH invalidation). " + f"All source files must compile successfully." + ) + + if not fastled_objects: + raise Exception("No FastLED source files compiled successfully") + + # Create static library using ar + lib_file = fastled_build_dir / "libfastled.a" + log_timing(f"[LIBRARY] Creating static library: {lib_file}") + + archive_future = compiler.create_archive(fastled_objects, lib_file) + archive_result = archive_future.result() + + if not archive_result.ok: + raise Exception(f"Library creation failed: {archive_result.stderr}") + + log_timing(f"[LIBRARY] SUCCESS: FastLED library created: {lib_file}") + return lib_file + + +def get_platform_linker_args() -> List[str]: + """Get platform-specific linker arguments for FastLED executables.""" + import platform + + system = platform.system() + if system == "Windows": + # In this project, we use Zig toolchain which has GNU target + # The compiler is hardcoded to "python -m ziglang c++" in create_fastled_compiler() + # Zig toolchain uses GNU-style linking even on Windows + return [ + "-static-libgcc", # Static link GCC runtime + "-static-libstdc++", # Static link C++ runtime + "-lpthread", # Threading support + "-lm", # Math library + # Windows system libraries linked automatically + ] + elif system == "Linux": + return [ + "-pthread", # Threading support + "-lm", # Math library + "-ldl", # Dynamic loading + "-lrt", # Real-time extensions + ] + elif system == "Darwin": # macOS + return [ + "-pthread", # Threading support + "-lm", # Math library + "-framework", + "CoreFoundation", + "-framework", + "IOKit", + ] + else: + return [ + "-pthread", # Threading support + "-lm", # Math library + ] + + +def get_executable_name(example_name: str) -> str: + """Get platform-appropriate executable name.""" + import platform + + if platform.system() == "Windows": + return f"{example_name}.exe" + else: + return example_name + + +def create_main_cpp_for_example(example_build_dir: Path) -> Path: + """Create a main.cpp file with sketch runner support and stub main function.""" + main_cpp_content = """// Auto-generated main.cpp for Arduino sketch with sketch runner support +// This file provides both the main() function and sketch runner exports + +#include + +#ifdef FASTLED_STUB_IMPL +#include "platforms/stub/time_stub.h" +#include "fl/function.h" +#endif + +// Arduino sketch functions (provided by compiled .ino file) +extern void setup(); +extern void loop(); + +// Sketch runner exports for external calling +extern "C" { + void sketch_setup() { + setup(); + } + + void sketch_loop() { + loop(); + } +} + +// Main function for direct execution +int main() { + printf("RUNNER: Starting sketch execution\\n"); + +#ifdef FASTLED_STUB_IMPL + // Override delay function to return immediately for fast testing + setDelayFunction([](uint32_t ms) { + // Fast delay override - do nothing for speed + (void)ms; // Suppress unused parameter warning + }); +#endif + + // Initialize sketch + printf("RUNNER: Calling setup()\\n"); + setup(); + printf("RUNNER: Setup complete\\n"); + + // Run sketch loop limited times for testing (not infinite) + printf("RUNNER: Running loop() 5 times for testing\\n"); + for (int i = 1; i <= 5; i++) { + printf("RUNNER: Loop iteration %d\\n", i); + loop(); + } + + printf("RUNNER: Sketch execution complete\\n"); + return 0; +} +""" + + main_cpp_path = example_build_dir / "main.cpp" + main_cpp_path.write_text(main_cpp_content) + return main_cpp_path + + +def link_examples( + object_file_map: Dict[Path, List[Path]], + fastled_lib: Path, + build_dir: Path, + compiler: Compiler, + log_timing: Callable[[str], None], +) -> LinkingResult: + """Link all examples into executable programs.""" + + linked_count = 0 + failed_count = 0 + + for ino_file, obj_files in object_file_map.items(): + example_name = ino_file.parent.name + example_build_dir = build_dir / example_name + + # Create executable name + executable_name = get_executable_name(example_name) + executable_path = example_build_dir / executable_name + + # Validate that all object files exist + missing_objects = [obj for obj in obj_files if not obj.exists()] + if missing_objects: + log_timing( + f"[LINKING] FAILED: {executable_name}: Missing object files: {[str(obj) for obj in missing_objects]}" + ) + failed_count += 1 + continue + + try: + # Create and compile main.cpp for this example + main_cpp_path = create_main_cpp_for_example(example_build_dir) + main_obj_path = example_build_dir / "main.o" + + main_future = compiler.compile_cpp_file(main_cpp_path, main_obj_path) + main_result: Result = main_future.result() + + if not main_result.ok: + log_timing( + f"[LINKING] FAILED: {executable_name}: Failed to compile main.cpp: {main_result.stderr[:300]}..." + ) + failed_count += 1 + continue + + # Add main.o to object files for linking + all_obj_files = obj_files + [main_obj_path] + + # Set up linking options + link_options = LinkOptions( + output_executable=str(executable_path), + object_files=[str(obj) for obj in all_obj_files], + static_libraries=[str(fastled_lib)], + linker_args=get_platform_linker_args(), + ) + + # Perform linking + link_future = compiler.link_program(link_options) + link_result: Result = link_future.result() + + if link_result.ok: + log_timing(f"[LINKING] SUCCESS: {executable_name}") + + # Verify the executable was actually created + if executable_path.exists(): + try: + file_stat = executable_path.stat() + is_executable = bool(file_stat.st_mode & stat.S_IXUSR) + log_timing( + f"[LINKING] VERIFIED: {executable_name} created, size: {file_stat.st_size} bytes, executable: {is_executable}" + ) + # Fix permissions if needed on Unix systems + if not is_executable and not platform.system() == "Windows": + try: + executable_path.chmod( + executable_path.stat().st_mode + | stat.S_IXUSR + | stat.S_IXGRP + | stat.S_IXOTH + ) + log_timing( + f"[LINKING] FIXED: Added execute permissions to {executable_name}" + ) + except Exception as e: + log_timing( + f"[LINKING] WARNING: Could not fix permissions for {executable_name}: {e}" + ) + + # Only count as success if executable actually exists and is valid + linked_count += 1 + + except Exception as e: + log_timing( + f"[LINKING] WARNING: Error checking created executable: {e}" + ) + # Count as success anyway since file exists, just can't check it + linked_count += 1 + else: + # CRITICAL BUG FIX: If executable doesn't exist, count as failure! + failed_count += 1 + log_timing( + f"[LINKING] FAILED: {executable_name} reported success but file not found at {executable_path}" + ) + else: + failed_count += 1 + log_timing( + f"[LINKING] FAILED: {executable_name}: {link_result.stderr[:200]}..." + ) + + except Exception as e: + failed_count += 1 + log_timing(f"[LINKING] FAILED: {executable_name}: Exception: {e}") + + if failed_count >= MAX_FAILURES_BEFORE_ABORT: + log_timing( + f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting further linking to avoid repeated errors." + ) + break + + return LinkingResult(linked_count=linked_count, failed_count=failed_count) + + +def link_examples_with_cache( + object_file_map: Dict[Path, List[Path]], + fastled_lib: Path, + build_dir: Path, + compiler: Compiler, + log_timing: Callable[[str], None], +) -> LinkingResult: + """ + Link all examples into executable programs with intelligent caching. + + Leverages the existing FastLEDTestCompiler cache infrastructure to skip + linking when all input artifacts (object files, library, linker args) are unchanged. + + Uses parallel cache detection for much faster cache checking (~10x speedup). + """ + from ci.compiler.clang_compiler import link_program_sync + + linked_count = 0 + failed_count = 0 + cache_hits = 0 + cache_misses = 0 + + # Create cache directory (same as FastLEDTestCompiler) + cache_dir = Path(".build/link_cache") + cache_dir.mkdir(parents=True, exist_ok=True) + + @dataclass + class ExampleCacheInfo: + """Information about an example's cache status and linking requirements""" + + ino_file: Path + obj_files: List[Path] + example_name: str + executable_name: str + executable_path: Path + main_obj_path: Path + all_obj_files: List[Path] + cache_key: str + cached_exe: Optional[Path] + is_cache_hit: bool + main_compile_future: Optional[Any] = None + + # Check if parallel processing is disabled + no_parallel = False + try: + # Access the config through the module's current instance if available + import inspect + + from ci.compiler.test_example_compilation import CompilationTestRunner + + frame = inspect.currentframe() + while frame: + if "self" in frame.f_locals and hasattr(frame.f_locals["self"], "config"): + config = frame.f_locals["self"].config + no_parallel = getattr(config, "no_parallel", False) + break + frame = frame.f_back + except Exception: + # Fallback: check environment variable + import os + + no_parallel = os.getenv("NO_PARALLEL") == "1" + + def parallel_cache_detection( + object_file_map: Dict[Path, List[Path]], + fastled_lib: Path, + build_dir: Path, + compiler: Compiler, + log_timing: Callable[[str], None], + ) -> tuple[int, int, int, int]: + """ + Parallel cache detection and linking for much faster processing. + + Returns: (linked_count, failed_count, cache_hits, cache_misses) + """ + linked_count = 0 + failed_count = 0 + cache_hits = 0 + cache_misses = 0 + + # Phase 1: Prepare all examples and submit main.cpp compilations in parallel + example_infos: List[ExampleCacheInfo] = [] + + log_timing( + f"[LINKING] Starting parallel preparation for {len(object_file_map)} examples..." + ) + + for ino_file, obj_files in object_file_map.items(): + example_name = ino_file.parent.name + example_build_dir = build_dir / example_name + executable_name = get_executable_name(example_name) + executable_path = example_build_dir / executable_name + + # Validate that all object files exist + missing_objects = [obj for obj in obj_files if not obj.exists()] + if missing_objects: + log_timing( + f"[LINKING] FAILED: {executable_name}: Missing object files: {[str(obj) for obj in missing_objects]}" + ) + failed_count += 1 + continue + + # Create main.cpp and submit compilation (non-blocking) + main_cpp_path = create_main_cpp_for_example(example_build_dir) + main_obj_path = example_build_dir / "main.o" + main_future = compiler.compile_cpp_file(main_cpp_path, main_obj_path) + + # Create example info (cache key will be calculated after main.cpp compiles) + example_info = ExampleCacheInfo( + ino_file=ino_file, + obj_files=obj_files, + example_name=example_name, + executable_name=executable_name, + executable_path=executable_path, + main_obj_path=main_obj_path, + all_obj_files=[], # Will be filled after main.cpp compiles + cache_key="", # Will be calculated + cached_exe=None, # Will be checked + is_cache_hit=False, # Will be determined + main_compile_future=main_future, + ) + example_infos.append(example_info) + + # Phase 2: Wait for main.cpp compilations and calculate cache keys in parallel + log_timing( + f"[LINKING] Parallel cache key calculation for {len(example_infos)} examples..." + ) + + for example_info in example_infos[ + : + ]: # Use slice to allow removal during iteration + try: + main_result: Result = example_info.main_compile_future.result() + + if not main_result.ok: + log_timing( + f"[LINKING] FAILED: {example_info.executable_name}: Failed to compile main.cpp: {main_result.stderr[:100]}..." + ) + failed_count += 1 + example_infos.remove(example_info) + continue + + # Update example info with complete information + example_info.all_obj_files = example_info.obj_files + [ + example_info.main_obj_path + ] + linker_args = get_platform_linker_args() + example_info.cache_key = calculate_multiple_objects_cache_key( + example_info.all_obj_files, fastled_lib, linker_args + ) + example_info.cached_exe = get_cached_executable( + example_info.example_name, example_info.cache_key + ) + example_info.is_cache_hit = example_info.cached_exe is not None + + except Exception as e: + log_timing( + f"[LINKING] FAILED: {example_info.executable_name}: Exception during preparation: {e}" + ) + failed_count += 1 + example_infos.remove(example_info) + if failed_count >= MAX_FAILURES_BEFORE_ABORT: + log_timing( + f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}) during preparation. Aborting." + ) + return linked_count, failed_count, cache_hits, cache_misses + + # Phase 3: Separate cache hits from cache misses + cache_hit_examples = [info for info in example_infos if info.is_cache_hit] + cache_miss_examples = [info for info in example_infos if not info.is_cache_hit] + + log_timing( + f"[LINKING] Cache analysis: {len(cache_hit_examples)} hits, {len(cache_miss_examples)} misses" + ) + + # Phase 4: Process cache hits in parallel (copy cached executables) + if cache_hit_examples: + + def copy_cached_executable(example_info: ExampleCacheInfo) -> bool: + try: + if example_info.cached_exe is None: + return False + shutil.copy2(example_info.cached_exe, example_info.executable_path) + return True + except Exception as e: + log_timing( + f"[LINKING] Warning: Failed to copy cached {example_info.executable_name}, will relink: {e}" + ) + return False + + with concurrent.futures.ThreadPoolExecutor( + max_workers=min(len(cache_hit_examples), 8) + ) as executor: + copy_futures = { + executor.submit(copy_cached_executable, info): info + for info in cache_hit_examples + } + + for future in concurrent.futures.as_completed(copy_futures): + example_info = copy_futures[future] + try: + success = future.result() + if success: + linked_count += 1 + cache_hits += 1 + log_timing( + f"[LINKING] {green_text('SUCCESS (CACHED)')}: {example_info.executable_name}" + ) + else: + # Failed to copy, add to cache misses for actual linking + cache_miss_examples.append(example_info) + except Exception as e: + log_timing( + f"[LINKING] Exception copying cached {example_info.executable_name}: {e}" + ) + cache_miss_examples.append(example_info) + + # Phase 5: Process cache misses in parallel (actual linking) + if cache_miss_examples: + + def link_example(example_info: ExampleCacheInfo) -> bool: + try: + linker_args = get_platform_linker_args() + link_options = LinkOptions( + output_executable=str(example_info.executable_path), + object_files=[str(obj) for obj in example_info.all_obj_files], + static_libraries=[str(fastled_lib)], + linker_args=linker_args, + ) + + link_result: Result = link_program_sync( + link_options, compiler.build_flags + ) + + if link_result.ok: + # Cache the successful executable + cache_executable( + example_info.example_name, + example_info.cache_key, + example_info.executable_path, + ) + return True + else: + log_timing( + f"[LINKING] FAILED: {example_info.executable_name}: {link_result.stderr[:200]}..." + ) + return False + + except Exception as e: + log_timing( + f"[LINKING] FAILED: {example_info.executable_name}: Exception: {e}" + ) + return False + + with concurrent.futures.ThreadPoolExecutor( + max_workers=min(len(cache_miss_examples), 4) + ) as executor: + link_futures = { + executor.submit(link_example, info): info + for info in cache_miss_examples + } + + for future in concurrent.futures.as_completed(link_futures): + example_info = link_futures[future] + try: + success = future.result() + if success: + linked_count += 1 + cache_misses += 1 + log_timing( + f"[LINKING] {green_text('SUCCESS (REBUILT)')}: {example_info.executable_name}" + ) + else: + failed_count += 1 + if failed_count >= MAX_FAILURES_BEFORE_ABORT: + log_timing( + f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining linking jobs." + ) + break + except Exception as e: + log_timing( + f"[LINKING] Exception linking {example_info.executable_name}: {e}" + ) + failed_count += 1 + if failed_count >= MAX_FAILURES_BEFORE_ABORT: + log_timing( + f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining linking jobs." + ) + break + + return linked_count, failed_count, cache_hits, cache_misses + + # Helper functions using the same algorithms as FastLEDTestCompiler + def calculate_file_hash(file_path: Path) -> str: + """Calculate SHA256 hash of a file (same as FastLEDTestCompiler._calculate_file_hash)""" + if not file_path.exists(): + return "no_file" + + hash_sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + return hash_sha256.hexdigest() + + def calculate_linker_args_hash(linker_args: List[str]) -> str: + """Calculate SHA256 hash of linker arguments (same as FastLEDTestCompiler._calculate_linker_args_hash)""" + args_str = "|".join(sorted(linker_args)) + hash_sha256 = hashlib.sha256() + hash_sha256.update(args_str.encode("utf-8")) + return hash_sha256.hexdigest() + + def calculate_multiple_objects_cache_key( + obj_files: List[Path], fastled_lib: Path, linker_args: List[str] + ) -> str: + """ + Calculate cache key for multiple object files linking. + Extends FastLEDTestCompiler logic to handle multiple object files. + """ + # Calculate hash for each object file + obj_hashes: List[str] = [] + for obj_file in obj_files: + obj_hashes.append(calculate_file_hash(obj_file)) + + # Combine object file hashes in a stable way (sorted by path for consistency) + sorted_obj_paths = sorted(str(obj) for obj in obj_files) + sorted_obj_hashes: List[str] = [] + for obj_path in sorted_obj_paths: + obj_file = Path(obj_path) + sorted_obj_hashes.append(calculate_file_hash(obj_file)) + + combined_obj_hash = hashlib.sha256( + "|".join(sorted_obj_hashes).encode("utf-8") + ).hexdigest() + + # Calculate other hashes + fastled_hash = calculate_file_hash(fastled_lib) + linker_hash = calculate_linker_args_hash(linker_args) + # Include header dependency fingerprint to invalidate link cache when headers change + try: + header_hash = "" + if hasattr(compiler, "_get_pch_dependencies"): + dep_paths = compiler._get_pch_dependencies() # type: ignore[attr-defined] + dep_hashes: List[str] = [] + for p in dep_paths: + dep_hashes.append(calculate_file_hash(p)) + header_hash = hashlib.sha256( + "|".join(dep_hashes).encode("utf-8") + ).hexdigest() + else: + header_hash = "no_header_info" + except KeyboardInterrupt: + import _thread + + _thread.interrupt_main() + raise + except Exception: + header_hash = "header_scan_error" + + # Combine all components (same format as FastLEDTestCompiler) + combined = f"fastled:{fastled_hash}|objects:{combined_obj_hash}|flags:{linker_hash}|hdr:{header_hash}" + final_hash = hashlib.sha256(combined.encode("utf-8")).hexdigest() + + return final_hash[ + :16 + ] # Use first 16 chars for readability (same as FastLEDTestCompiler) + + def get_cached_executable(example_name: str, cache_key: str) -> Optional[Path]: + """Check if cached executable exists (same as FastLEDTestCompiler._get_cached_executable)""" + cached_exe = cache_dir / f"{example_name}_{cache_key}.exe" + return cached_exe if cached_exe.exists() else None + + def cache_executable(example_name: str, cache_key: str, exe_path: Path) -> None: + """Cache an executable (same as FastLEDTestCompiler._cache_executable)""" + if not exe_path.exists(): + return + + cached_exe = cache_dir / f"{example_name}_{cache_key}.exe" + try: + shutil.copy2(exe_path, cached_exe) + except Exception as e: + log_timing(f"[LINKING] Warning: Failed to cache {example_name}: {e}") + + # Choose between parallel and serial processing based on --no-parallel flag + if no_parallel: + log_timing("[LINKING] Serial cache detection (--no-parallel specified)") + + # Original serial processing + for ino_file, obj_files in object_file_map.items(): + example_name = ino_file.parent.name + example_build_dir = build_dir / example_name + + # Create executable name and path + executable_name = get_executable_name(example_name) + executable_path = example_build_dir / executable_name + + # Validate that all object files exist + missing_objects = [obj for obj in obj_files if not obj.exists()] + if missing_objects: + log_timing( + f"[LINKING] FAILED: {executable_name}: Missing object files: {[str(obj) for obj in missing_objects]}" + ) + failed_count += 1 + continue + + try: + # Create and compile main.cpp for this example + main_cpp_path = create_main_cpp_for_example(example_build_dir) + main_obj_path = example_build_dir / "main.o" + + main_future = compiler.compile_cpp_file(main_cpp_path, main_obj_path) + main_result: Result = main_future.result() + + if not main_result.ok: + log_timing( + f"[LINKING] FAILED: {executable_name}: Failed to compile main.cpp: {main_result.stderr[:100]}..." + ) + failed_count += 1 + continue + + # Combine all object files for linking + all_obj_files = obj_files + [main_obj_path] + + # Get platform-specific linker arguments + linker_args = get_platform_linker_args() + + # Calculate cache key using the same algorithm as FastLEDTestCompiler + cache_key = calculate_multiple_objects_cache_key( + all_obj_files, fastled_lib, linker_args + ) + + # Check for cached executable + cached_exe = get_cached_executable(example_name, cache_key) + if cached_exe: + try: + # Copy cached executable to target location + shutil.copy2(cached_exe, executable_path) + linked_count += 1 + cache_hits += 1 + log_timing( + f"[LINKING] {green_text('SUCCESS (CACHED)')}: {executable_name}" + ) + continue # Skip actual linking + except Exception as e: + log_timing( + f"[LINKING] Warning: Failed to copy cached {executable_name}, will relink: {e}" + ) + # Fall through to actual linking + + # No cache hit - perform actual linking using existing link_program_sync + link_options = LinkOptions( + output_executable=str(executable_path), + object_files=[str(obj) for obj in all_obj_files], + static_libraries=[str(fastled_lib)], + linker_args=linker_args, + ) + + # Use link_program_sync for synchronous linking + link_result: Result = link_program_sync( + link_options, compiler.build_flags + ) + + if link_result.ok: + linked_count += 1 + cache_misses += 1 + log_timing( + f"[LINKING] {green_text('SUCCESS (REBUILT)')}: {executable_name}" + ) + + # Cache the newly linked executable for future use + cache_executable(example_name, cache_key, executable_path) + else: + failed_count += 1 + log_timing( + f"[LINKING] FAILED: {executable_name}: {link_result.stderr[:200]}..." + ) + + except Exception as e: + failed_count += 1 + log_timing(f"[LINKING] FAILED: {executable_name}: Exception: {e}") + + else: + # Use parallel cache detection for much faster processing + log_timing("[LINKING] Parallel cache detection enabled") + linked_count, failed_count, cache_hits, cache_misses = parallel_cache_detection( + object_file_map, fastled_lib, build_dir, compiler, log_timing + ) + + return LinkingResult( + linked_count=linked_count, + failed_count=failed_count, + cache_hits=cache_hits, + cache_misses=cache_misses, + ) + + +@dataclass +class CompilationTestConfig: + """Configuration for the compilation test.""" + + specific_examples: Optional[List[str]] + clean_build: bool + disable_pch: bool + unity_build: bool + unity_custom_output: Optional[str] + unity_additional_flags: Optional[List[str]] + full_compilation: bool + no_parallel: bool + verbose: bool + + +@dataclass +class CompilationTestResults: + """Results from compilation test.""" + + successful_count: int + failed_count: int + failed_examples: List[Dict[str, str]] + compile_time: float + linking_time: float + linked_count: int + linking_failed_count: int + object_file_map: Optional[Dict[Path, List[Path]]] + execution_failures: List[ExecutionFailure] + # Sketch runner execution results + executed_count: int = 0 + execution_failed_count: int = 0 + execution_time: float = 0.0 + + +class CompilationTestRunner: + """Handles the orchestration of FastLED example compilation tests.""" + + def __init__(self, config: CompilationTestConfig): + self.config = config + self.global_start_time = time.time() + + def log_timing(self, message: str) -> None: + """Log a message with timestamp relative to start.""" + elapsed = time.time() - self.global_start_time + print(f"[{elapsed:6.2f}s] {message}") + + def initialize_system( + self, + ) -> tuple[ + Compiler, Dict[str, Union[str, int, float]], Dict[str, Union[bool, str]] + ]: + """Initialize the compiler and get system information.""" + self.log_timing("==> FastLED Example Compilation Test (SIMPLE BUILD SYSTEM)") + self.log_timing("=" * 70) + + # Get system information + self.log_timing("Getting system information...") + system_info = get_system_info() + build_config = get_build_configuration() + + self.log_timing( + f"[SYSTEM] OS: {system_info['os']}, Compiler: {system_info['compiler']}, CPU: {system_info['cpu_cores']} cores" + ) + self.log_timing(f"[SYSTEM] Memory: {system_info['memory_gb']:.1f}GB available") + + # Initialize compiler + self.log_timing("Initializing simple build system...") + try: + compiler = create_fastled_compiler( + use_pch=not self.config.disable_pch, + parallel=not self.config.no_parallel, + ) + + # Verify compiler accessibility + + version_result = compiler.check_clang_version() + + if not version_result.success: + raise RuntimeError( + f"Compiler accessibility check failed: {version_result.error}" + ) + + return compiler, system_info, build_config + + except Exception as e: + raise RuntimeError(f"Failed to initialize simple build system: {e}") + + def discover_examples(self, compiler: Compiler) -> List[Path]: + """Discover and validate .ino examples to compile.""" + self.log_timing("Discovering .ino examples...") + if self.config.verbose: + print( + f"[VERBOSE] Discovering examples with filter: {self.config.specific_examples}" + ) + + try: + filter_names = ( + self.config.specific_examples if self.config.specific_examples else None + ) + ino_files = compiler.find_ino_files("examples", filter_names=filter_names) + + if not ino_files: + if self.config.specific_examples: + # Show detailed error with suggestions + all_ino_files = list(Path("examples").rglob("*.ino")) + if all_ino_files: + available_names = sorted([f.stem for f in all_ino_files]) + suggestion = f"\nAvailable examples include: {', '.join(available_names[:10])}..." + if len(available_names) > 10: + suggestion += ( + f"\n ... and {len(available_names) - 10} more examples" + ) + else: + suggestion = "" + + raise ValueError( + f"No .ino files found matching: {self.config.specific_examples}{suggestion}" + ) + else: + raise ValueError("No .ino files found in examples directory") + + # Report discovery results + if self.config.specific_examples: + self.log_timing( + f"[DISCOVER] Found {len(ino_files)} specific examples: {', '.join([f.stem for f in ino_files])}" + ) + if self.config.verbose: + print(f"[VERBOSE] Specific examples found:") + for ino_file in ino_files: + print(f"[VERBOSE] - {ino_file}") + else: + self.log_timing( + f"[DISCOVER] Found {len(ino_files)} total .ino examples in examples/" + ) + if self.config.verbose: + print(f"[VERBOSE] All examples found:") + for ino_file in ino_files: + print(f"[VERBOSE] - {ino_file.stem}") + + return ino_files + + except Exception as e: + raise RuntimeError(f"Failed to discover examples: {e}") + + def analyze_pch_compatibility( + self, compiler: Compiler, ino_files: List[Path] + ) -> tuple[set[Path], List[str], List[str]]: + """Analyze files for PCH compatibility and return configuration.""" + config_parts = ["Simple Build System: enabled"] + + if self.config.unity_build: + config_parts.append("UNITY build: enabled") + else: + config_parts.append("Direct .ino compilation: enabled") + + # Caching is disabled + config_parts.append("cache: disabled") + + pch_compatible_files: set[Path] = set() + pch_incompatible_files: List[str] = [] + + if self.config.unity_build: + self.log_timing("[UNITY] PCH disabled for UNITY builds (not needed)") + config_parts.append("PCH: disabled (UNITY mode)") + elif compiler.settings.use_pch: + self.log_timing("[PCH] Analyzing examples for PCH compatibility...") + + for ino_file in ino_files: + if compiler.analyze_ino_for_pch_compatibility(ino_file): + pch_compatible_files.add(ino_file) + else: + pch_incompatible_files.append(ino_file.name) + + if pch_incompatible_files: + self.log_timing( + f"[PCH] Found {len(pch_incompatible_files)} incompatible files (will use direct compilation):" + ) + for filename in pch_incompatible_files[:5]: # Show first 5 + self.log_timing(f"[PCH] - {filename} (has code before FastLED.h)") + if len(pch_incompatible_files) > 5: + self.log_timing( + f"[PCH] - ... and {len(pch_incompatible_files) - 5} more" + ) + + config_parts.append( + f"PCH: selective ({len(pch_compatible_files)} files)" + ) + else: + self.log_timing(f"[PCH] All {len(ino_files)} files are PCH compatible") + config_parts.append("PCH: enabled (all files)") + else: + self.log_timing("[PCH] PCH disabled globally") + config_parts.append("PCH: disabled") + + return pch_compatible_files, pch_incompatible_files, config_parts + + def setup_pch(self, compiler: Compiler, pch_compatible_files: set[Path]) -> bool: + """Setup precompiled headers if applicable.""" + if ( + not self.config.unity_build + and compiler.settings.use_pch + and len(pch_compatible_files) > 0 + ): + pch_start = time.time() + pch_success = compiler.create_pch_file() + pch_time = time.time() - pch_start + + if pch_success: + self.log_timing(f"[PCH] Precompiled header created in {pch_time:.2f}s") + return True + else: + self.log_timing( + "[PCH] Precompiled header creation failed, using direct compilation for all files" + ) + pch_compatible_files.clear() + return False + elif compiler.settings.use_pch and len(pch_compatible_files) == 0: + self.log_timing( + "[PCH] No PCH-compatible files found, skipping PCH creation" + ) + + return True + + def compile_examples( + self, + compiler: Compiler, + ino_files: List[Path], + pch_compatible_files: set[Path], + enable_fingerprint_cache: bool = True, + cache_file: str = ".build/fingerprint_cache.json", + cache_verbose: bool = False, + ) -> CompilationTestResults: + """Execute the compilation process.""" + parallel_status = "disabled" if self.config.no_parallel else "enabled" + self.log_timing( + f"[PERF] Parallel compilation: {parallel_status} (managed by compiler)" + ) + self.log_timing("[PERF] Direct compilation enabled (no CMake overhead)") + + if self.config.verbose: + print(f"[VERBOSE] Starting compilation with configuration:") + print(f"[VERBOSE] - Examples: {len(ino_files)}") + print(f"[VERBOSE] - PCH compatible: {len(pch_compatible_files)}") + print(f"[VERBOSE] - PCH disabled: {self.config.disable_pch}") + print(f"[VERBOSE] - Parallel: {not self.config.no_parallel}") + print(f"[VERBOSE] - Full compilation: {self.config.full_compilation}") + print(f"[VERBOSE] - Unity build: {self.config.unity_build}") + + self.log_timing(f"\n[BUILD] Starting example compilation...") + self.log_timing(f"[BUILD] Target examples: {len(ino_files)}") + + start_time = time.time() + + try: + if self.config.unity_build: + result = compile_examples_unity( + compiler, + ino_files, + self.log_timing, + unity_custom_output=self.config.unity_custom_output, + unity_additional_flags=self.config.unity_additional_flags, + ) + else: + if enable_fingerprint_cache: + # Use cache-aware compilation + from ci.compiler.cache_enhanced_compilation import ( + create_cache_compiler, + ) + + cache_compiler = create_cache_compiler( + compiler, Path(cache_file), verbose=cache_verbose + ) + + cache_result = cache_compiler.compile_with_cache( + ino_files, + pch_compatible_files, + self.log_timing, + self.config.full_compilation, + self.config.verbose, + ) + + result = cache_result["compilation_result"] + else: + # Standard compilation without cache + result = compile_examples_simple( + compiler, + ino_files, + pch_compatible_files, + self.log_timing, + self.config.full_compilation, + self.config.verbose, + ) + + compile_time = time.time() - start_time + + return CompilationTestResults( + successful_count=result.successful_count, + failed_count=result.failed_count, + failed_examples=result.failed_examples, + compile_time=compile_time, + linking_time=0.0, + linked_count=0, + linking_failed_count=0, + object_file_map=getattr(result, "object_file_map", None), + execution_failures=[], + ) + + except Exception as e: + raise RuntimeError(f"Compilation failed: {e}") + + def handle_linking( + self, compiler: Compiler, results: CompilationTestResults + ) -> CompilationTestResults: + """Handle linking phase if full compilation is requested.""" + if ( + not self.config.full_compilation + or results.failed_count > 0 + or not results.object_file_map + ): + if self.config.full_compilation and results.failed_count > 0: + self.log_timing( + f"[LINKING] Skipping linking due to {results.failed_count} compilation failures" + ) + return results + + self.log_timing("\n[LINKING] Starting real program linking...") + linking_start = time.time() + + try: + # Create FastLED static library + self.log_timing("[LINKING] Creating FastLED static library...") + fastled_build_dir = Path(".build/fastled") + fastled_build_dir.mkdir(parents=True, exist_ok=True) + + fastled_lib = create_fastled_library( + compiler, + fastled_build_dir, + self.log_timing, + verbose=self.config.verbose, + ) + + # Link examples + self.log_timing("[LINKING] Linking example programs...") + build_dir = Path(".build/examples") + linking_result = link_examples_with_cache( + results.object_file_map, + fastled_lib, + build_dir, + compiler, + self.log_timing, + ) + + results.linked_count = linking_result.linked_count + results.linking_failed_count = linking_result.failed_count + results.linking_time = time.time() - linking_start + + if results.linking_failed_count == 0: + self.log_timing( + f"[LINKING] SUCCESS: Successfully linked {results.linked_count} executable programs" + ) + else: + self.log_timing( + f"[LINKING] WARNING: Linked {results.linked_count} programs, {results.linking_failed_count} failed" + ) + + # Report cache statistics + if hasattr(linking_result, "cache_hits") and hasattr( + linking_result, "cache_misses" + ): + total_processed = ( + linking_result.cache_hits + linking_result.cache_misses + ) + if total_processed > 0: + hit_rate = linking_result.cache_hit_rate + self.log_timing( + f"[LINKING] Cache: {linking_result.cache_hits} hits, {linking_result.cache_misses} misses ({hit_rate:.1f}% hit rate)" + ) + + self.log_timing( + f"[LINKING] Real linking completed in {results.linking_time:.2f}s" + ) + + except Exception as e: + results.linking_time = time.time() - linking_start + self.log_timing(f"[LINKING] ERROR: Linking failed: {e}") + results.linking_failed_count = ( + results.successful_count + ) # Mark all as failed + results.linked_count = 0 + + return results + + def handle_execution( + self, results: CompilationTestResults + ) -> CompilationTestResults: + """Execute linked programs when full compilation is requested.""" + if ( + not self.config.full_compilation + or results.linking_failed_count > 0 + or results.linked_count == 0 + ): + if self.config.full_compilation and results.linking_failed_count > 0: + self.log_timing( + f"[EXECUTION] Skipping execution due to {results.linking_failed_count} linking failures" + ) + return results + + self.log_timing("\n[EXECUTION] Starting sketch runner execution...") + execution_start = time.time() + + executed_count = 0 + execution_failed_count = 0 + execution_failures: List[ExecutionFailure] = [] + build_dir = Path(".build/examples") + + # Only execute examples that were compiled and linked in this run + if not results.object_file_map: + self.log_timing( + "[EXECUTION] No object file map available - no examples to execute" + ) + return results + + # Execute only the examples from this compilation run + failure_threshold_reached = False + for ino_file in results.object_file_map.keys(): + example_name = ino_file.parent.name + example_dir = build_dir / example_name + + if not example_dir.is_dir(): + continue + + executable_name = get_executable_name(example_name) + executable_path = example_dir / executable_name + + if not executable_path.exists(): + # Enhanced logging to debug why executable is missing + self.log_timing( + f"[EXECUTION] SKIPPED: {executable_name}: Executable not found at {executable_path}" + ) + # Show what files actually exist in the directory + try: + existing_files = list(example_dir.iterdir()) + self.log_timing( + f"[EXECUTION] DEBUG: Directory {example_dir} contains: {[f.name for f in existing_files]}" + ) + except Exception as e: + self.log_timing(f"[EXECUTION] DEBUG: Error listing directory: {e}") + continue + + # Check executable permissions + import stat + + try: + file_stat = executable_path.stat() + is_executable = bool(file_stat.st_mode & stat.S_IXUSR) + self.log_timing( + f"[EXECUTION] DEBUG: {executable_name} exists, size: {file_stat.st_size} bytes, executable: {is_executable}" + ) + except Exception as e: + self.log_timing( + f"[EXECUTION] DEBUG: Error checking file permissions: {e}" + ) + + rp = RunningProcess( + command=[str(executable_path.absolute())], + cwd=example_dir.absolute(), + check=False, + auto_run=True, + enable_stack_trace=True, + on_complete=None, + output_formatter=None, + ) + + try: + self.log_timing(f"[EXECUTION] Running: {executable_name}") + + # Use RunningProcess to execute and stream output + + with rp.line_iter(timeout=60) as it: + for line in it: + if self.config.verbose: + self.log_timing(f"[EXECUTION] {line}") + + rc = rp.wait() + + if rc == 0: + executed_count += 1 + self.log_timing(f"[EXECUTION] SUCCESS: {executable_name}") + else: + execution_failed_count += 1 + self.log_timing( + f"[EXECUTION] FAILED: {executable_name}: Exit code {rc}" + ) + execution_failures.append( + ExecutionFailure( + name=example_name, + reason=f"exit code {rc}", + stdout=rp.stdout, + ) + ) + if self.config.verbose: + self.log_timing(f"[EXECUTION] Failed output:") + # If nothing meaningful captured, note it + stdout = rp.stdout + if not stdout.strip(): + self.log_timing( + "[EXECUTION] No output captured from failed execution" + ) + + except TimeoutError: + execution_failed_count += 1 + self.log_timing( + f"[EXECUTION] FAILED: {executable_name}: Execution timeout (30s)" + ) + timeout_stdout: str = "\n".join(rp.stdout) + execution_failures.append( + ExecutionFailure( + name=example_name, + reason="timeout (30s)", + stdout=timeout_stdout, + ) + ) + except Exception as e: + execution_failed_count += 1 + self.log_timing( + f"[EXECUTION] FAILED: {executable_name}: Exception: {e}" + ) + + exc_stdout: str = "\n".join(rp.stdout) + execution_failures.append( + ExecutionFailure( + name=example_name, + reason=f"exception: {e}", + stdout=exc_stdout, + ) + ) + + if execution_failed_count >= MAX_FAILURES_BEFORE_ABORT: + self.log_timing( + f"[EXECUTION] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting further execution to avoid repeated errors." + ) + failure_threshold_reached = True + break + + execution_time = time.time() - execution_start + + if execution_failed_count == 0: + self.log_timing( + f"[EXECUTION] SUCCESS: Successfully executed {executed_count} sketch programs" + ) + else: + self.log_timing( + f"[EXECUTION] WARNING: Executed {executed_count} programs, {execution_failed_count} failed" + ) + + self.log_timing( + f"[EXECUTION] Sketch execution completed in {execution_time:.2f}s" + ) + + # Store execution results in the results object + results.executed_count = executed_count + results.execution_failed_count = execution_failed_count + results.execution_time = execution_time + results.execution_failures = execution_failures + + return results + + def report_results( + self, + ino_files: List[Path], + results: CompilationTestResults, + config_parts: List[str], + ) -> int: + """Generate the final report and return exit code.""" + total_time = ( + results.compile_time + results.linking_time + results.execution_time + ) + parallel_status = "disabled" if self.config.no_parallel else "enabled" + + self.log_timing(f"[CONFIG] Mode: {', '.join(config_parts)}") + self.log_timing( + f"\n[BUILD] Parallel compilation: {parallel_status} (managed by compiler)" + ) + + # Enhanced timing breakdown + self.log_timing(f"\n[TIMING] Compilation: {results.compile_time:.2f}s") + linking_mode = ( + "(with program generation)" + if self.config.full_compilation and results.failed_count == 0 + else "(compile-only mode)" + ) + self.log_timing(f"[TIMING] Linking: {results.linking_time:.2f}s {linking_mode}") + if self.config.full_compilation and results.execution_time > 0: + self.log_timing( + f"[TIMING] Execution: {results.execution_time:.2f}s (sketch runner)" + ) + self.log_timing(f"[TIMING] Total: {total_time:.2f}s") + + # Performance summary + self.log_timing(f"\n[SUMMARY] FastLED Example Compilation Performance:") + self.log_timing(f"[SUMMARY] Examples processed: {len(ino_files)}") + + if self.config.verbose: + print(f"\n[VERBOSE] Detailed compilation results:") + print(f"[VERBOSE] - Total examples: {len(ino_files)}") + print(f"[VERBOSE] - Successful: {results.successful_count}") + print(f"[VERBOSE] - Failed: {results.failed_count}") + print(f"[VERBOSE] - Compilation time: {results.compile_time:.2f}s") + print(f"[VERBOSE] - Linking time: {results.linking_time:.2f}s") + print(f"[VERBOSE] - Execution time: {results.execution_time:.2f}s") + print(f"[VERBOSE] - Total time: {total_time:.2f}s") + self.log_timing(f"[SUMMARY] Successful: {results.successful_count}") + self.log_timing(f"[SUMMARY] Failed: {results.failed_count}") + if self.config.full_compilation: + self.log_timing(f"[SUMMARY] Linked: {results.linked_count}") + self.log_timing( + f"[SUMMARY] Linking failed: {results.linking_failed_count}" + ) + self.log_timing(f"[SUMMARY] Executed: {results.executed_count}") + self.log_timing( + f"[SUMMARY] Execution failed: {results.execution_failed_count}" + ) + self.log_timing(f"[SUMMARY] Parallel compilation: {parallel_status}") + self.log_timing(f"[SUMMARY] Build time: {results.compile_time:.2f}s") + + # Show both compilation speed and total throughput for complete picture + if results.compile_time > 0: + self.log_timing( + f"[SUMMARY] Compilation speed: {len(ino_files) / results.compile_time:.1f} examples/second" + ) + if total_time > 0: + self.log_timing( + f"[SUMMARY] Total throughput: {len(ino_files) / total_time:.1f} examples/second" + ) + + # Detailed execution failure reporting (rich output without timestamps) + if self.config.full_compilation and results.execution_failed_count > 0: + print() + print("########################################################") + print("# ERROR: TEST EXECUTION FAILED (see detailed output below) #") + print("########################################################") + print() + for failure in results.execution_failures: + print(f"{failure.name} failed with:\n") + preview: str = failure.stdout or failure.reason + preview = preview.strip() + if len(preview) > 300: + preview = preview[:300] + if preview: + print(preview) + else: + print("(no output captured)") + print("\n------------\n") + print("Failing tests (see detailed output above)") + for failure in results.execution_failures: + print(f" * {failure.name}") + + # Determine success + overall_success = results.failed_count == 0 + if self.config.full_compilation: + overall_success = ( + overall_success + and results.linking_failed_count == 0 + and results.execution_failed_count == 0 + ) + + if overall_success: + if self.config.full_compilation: + self.log_timing( + "\n[SUCCESS] EXAMPLE COMPILATION + LINKING + EXECUTION TEST: SUCCESS" + ) + self.log_timing( + f"[SUCCESS] {len(ino_files)} examples compiled ({results.successful_count} compilation jobs), " + f"{results.linked_count} linked, and {results.executed_count} executed successfully" + ) + else: + self.log_timing("\n[SUCCESS] EXAMPLE COMPILATION TEST: SUCCESS") + self.log_timing( + f"[SUCCESS] {len(ino_files)} examples compiled successfully ({results.successful_count} compilation jobs)" + ) + + print(green_text("### SUCCESS ###")) + return 0 + else: + # Show failed examples + print(f"\n{red_text('### ERROR ###')}") + + if results.failed_count > 0: + self.log_timing(f"[ERROR] {results.failed_count} compilation failures:") + for failure in results.failed_examples: + path = failure["path"].replace("\\", "/") + print(f" {orange_text(f'examples/{path}')}") + + if self.config.full_compilation and results.linking_failed_count > 0: + self.log_timing( + f"[ERROR] {results.linking_failed_count} linking failures detected" + ) + self.log_timing( + "[ERROR] Test failed due to linker errors - see linking output above" + ) + + return 1 + + +def run_example_compilation_test( + specific_examples: Optional[List[str]], + clean_build: bool, + disable_pch: bool, + unity_build: bool, + unity_custom_output: Optional[str], + unity_additional_flags: Optional[List[str]], + full_compilation: bool, + no_parallel: bool, + verbose: bool = False, + enable_fingerprint_cache: bool = True, + cache_file: str = ".build/fingerprint_cache.json", + cache_verbose: bool = False, +) -> int: + """Run the example compilation test using enhanced simple build system.""" + try: + # Create configuration + config = CompilationTestConfig( + specific_examples=specific_examples, + clean_build=clean_build, + disable_pch=disable_pch, + unity_build=unity_build, + unity_custom_output=unity_custom_output, + unity_additional_flags=unity_additional_flags, + full_compilation=full_compilation, + no_parallel=no_parallel, + verbose=verbose, + ) + + # Create test runner + + runner = CompilationTestRunner(config) + + # Initialize system and compiler + + compiler, system_info, build_config = runner.initialize_system() + + # Discover examples to compile + ino_files = runner.discover_examples(compiler) + + # Analyze PCH compatibility and configuration + pch_compatible_files, pch_incompatible_files, config_parts = ( + runner.analyze_pch_compatibility(compiler, ino_files) + ) + + # Setup PCH if needed + runner.setup_pch(compiler, pch_compatible_files) + + # Dump compiler information + + compiler_args = compiler.get_compiler_args() + + if config.verbose: + print(f"\n[VERBOSE] Compiler configuration:") + print(f"[VERBOSE] Base compiler command: {' '.join(compiler_args[:3])}") + print(f"[VERBOSE] Compiler flags ({len(compiler_args) - 3} total):") + for i, arg in enumerate(compiler_args[3:], 1): + print(f"[VERBOSE] {i:2d}. {arg}") + print() + + # Compile examples + results = runner.compile_examples( + compiler, + ino_files, + pch_compatible_files, + enable_fingerprint_cache=enable_fingerprint_cache, + cache_file=cache_file, + cache_verbose=cache_verbose, + ) + + # Handle linking if requested + results = runner.handle_linking(compiler, results) + + # Handle execution if requested (after linking) + results = runner.handle_execution(results) + + # Generate report and return exit code + return runner.report_results(ino_files, results, config_parts) + + except Exception as e: + print(f"\n{red_text('### ERROR ###')}") + print(f"Unexpected error: {e}") + return 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="FastLED Example Compilation Testing Script (Enhanced with Simple Build System)" + ) + parser.add_argument( + "examples", + nargs="*", + help="Specific examples to compile (if none specified, compile all examples)", + ) + parser.add_argument( + "--clean", + action="store_true", + help="Force a clean build (note: simple build system always performs clean builds).", + ) + parser.add_argument( + "--no-pch", + action="store_true", + help="Disable precompiled headers (PCH) for compilation - useful for debugging or compatibility issues.", + ) + parser.add_argument( + "--no-cache", + action="store_true", + help="Disable cache for compilation (cache disabled by default).", + ) + parser.add_argument( + "--unity", + action="store_true", + help="Enable UNITY build mode - compile all source files as a single unit for improved performance and optimization.", + ) + parser.add_argument( + "--custom-output", + type=str, + help="Custom output path for unity.cpp file (only used with --unity)", + ) + parser.add_argument( + "--additional-flags", + nargs="+", + help="Additional compiler flags to pass to unity build (only used with --unity)", + ) + parser.add_argument( + "--full", + action="store_true", + help="Enable full compilation mode: compile AND link examples into executable programs", + ) + parser.add_argument( + "--no-parallel", + action="store_true", + help="Disable parallel compilation - useful for debugging or single-threaded environments", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable verbose output showing detailed compilation commands and results", + ) + parser.add_argument( + "--no-fingerprint-cache", + action="store_true", + help="Disable fingerprint cache (cache is enabled by default for faster incremental builds)", + ) + parser.add_argument( + "--cache-file", + type=str, + default=".build/fingerprint_cache.json", + help="Path to fingerprint cache file (default: .build/fingerprint_cache.json)", + ) + parser.add_argument( + "--cache-verbose", + action="store_true", + help="Enable verbose cache output showing which files are skipped or compiled", + ) + + args = parser.parse_args() + + # Pass specific examples to the test function + specific_examples = args.examples if args.examples else None + + sys.exit( + run_example_compilation_test( + specific_examples, + args.clean, + disable_pch=args.no_pch, + unity_build=args.unity, + unity_custom_output=args.custom_output, + unity_additional_flags=args.additional_flags, + full_compilation=args.full, + no_parallel=args.no_parallel, + verbose=args.verbose, + enable_fingerprint_cache=not args.no_fingerprint_cache, + cache_file=args.cache_file, + cache_verbose=args.cache_verbose, + ) + ) diff --git a/v1.5/lib/FastLED/ci/compiler/unity_archive.py b/v1.5/lib/FastLED/ci/compiler/unity_archive.py new file mode 100644 index 0000000..3ce1f0e --- /dev/null +++ b/v1.5/lib/FastLED/ci/compiler/unity_archive.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 + +import argparse +import os +import sys +from concurrent.futures import Future +from pathlib import Path +from typing import List, Sequence + +from ci.compiler.clang_compiler import Compiler, CompilerOptions, Result +from ci.compiler.test_example_compilation import ( + create_fastled_compiler, + get_fastled_core_sources, +) + + +def _sorted_src_files(files: Sequence[Path]) -> List[Path]: + project_root = Path.cwd() + + def key_fn(p: Path) -> str: + try: + rel = p.relative_to(project_root) + except Exception: + rel = p + return rel.as_posix() + + return sorted(files, key=key_fn) + + +def _partition(files: List[Path], chunks: int) -> List[List[Path]]: + total = len(files) + if chunks <= 0: + raise ValueError("chunks must be >= 1") + chunks = min(chunks, total) if total > 0 else 1 + base = total // chunks + rem = total % chunks + partitions: List[List[Path]] = [] + start = 0 + for i in range(chunks): + size = base + (1 if i < rem else 0) + end = start + size + if size > 0: + partitions.append(files[start:end]) + start = end + return partitions + + +def build_unity_chunks_and_archive( + compiler: Compiler, + chunks: int, + output_archive: Path, + unity_dir: Path, + no_parallel: bool, +) -> Path: + unity_dir.mkdir(parents=True, exist_ok=True) + # Gather and sort source files consistently with existing library logic + files = get_fastled_core_sources() + # Exclude any main-like TU + files = [p for p in files if p.name != "stub_main.cpp"] + files = _sorted_src_files(files) + if not files: + raise RuntimeError("No source files found under src/** for unity build") + + partitions = _partition(files, chunks) + + # Prepare per-chunk unity paths + unity_cpp_paths: List[Path] = [ + unity_dir / f"unity{i + 1}.cpp" for i in range(len(partitions)) + ] + unity_obj_paths: List[Path] = [p.with_suffix(".o") for p in unity_cpp_paths] + + # Compile chunks + futures: List[Future[Result]] = [] + compile_opts = CompilerOptions( + include_path=compiler.settings.include_path, + compiler=compiler.settings.compiler, + defines=compiler.settings.defines, + std_version=compiler.settings.std_version, + compiler_args=compiler.settings.compiler_args, + use_pch=False, + additional_flags=["-c"], + parallel=not no_parallel, + ) + + def submit(idx: int) -> Future[Result]: + chunk_files = partitions[idx] + unity_cpp = unity_cpp_paths[idx] + return compiler.compile_unity(compile_opts, chunk_files, unity_cpp) + + if no_parallel: + # Sequential execution + for i in range(len(partitions)): + result = submit(i).result() + if not result.ok: + raise RuntimeError(f"Unity chunk {i + 1} failed: {result.stderr}") + else: + futures = [submit(i) for i in range(len(partitions))] + for i, fut in enumerate(futures): + result = fut.result() + if not result.ok: + raise RuntimeError(f"Unity chunk {i + 1} failed: {result.stderr}") + + # Create archive from chunk objects + from ci.compiler.clang_compiler import LibarchiveOptions + + output_archive.parent.mkdir(parents=True, exist_ok=True) + archive_result = compiler.create_archive( + unity_obj_paths, output_archive, LibarchiveOptions() + ).result() + if not archive_result.ok: + raise RuntimeError(f"Archive creation failed: {archive_result.stderr}") + + return output_archive + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Chunked UNITY build and archive for FastLED src/**" + ) + parser.add_argument( + "--chunks", type=int, default=1, help="Number of unity chunks (1-4 typical)" + ) + parser.add_argument( + "--no-parallel", action="store_true", help="Compile unity chunks sequentially" + ) + parser.add_argument( + "--output", + type=str, + default=str(Path(".build/fastled/libfastled.a")), + help="Path to output archive libfastled.a", + ) + parser.add_argument( + "--unity-dir", + type=str, + default=str(Path(".build/fastled/unity")), + help="Directory to place generated unity{N}.cpp/.o files", + ) + parser.add_argument( + "--use-pch", + action="store_true", + help="Enable PCH (not recommended for unity builds)", + ) + args = parser.parse_args() + + # Anchor to project root (two levels up from this file) + here = Path(__file__).resolve() + project_root = here.parent.parent.parent + os.chdir(project_root) + + try: + compiler = create_fastled_compiler( + use_pch=args.use_pch, parallel=not args.no_parallel + ) + out = build_unity_chunks_and_archive( + compiler=compiler, + chunks=args.chunks, + output_archive=Path(args.output), + unity_dir=Path(args.unity_dir), + no_parallel=args.no_parallel, + ) + print(f"[UNITY] Created archive: {out}") + return 0 + except KeyboardInterrupt: + raise + except Exception as e: + print(f"\x1b[31m[UNITY] FAILED: {e}\x1b[0m") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/demo_symbol_analysis.py b/v1.5/lib/FastLED/ci/demo_symbol_analysis.py new file mode 100644 index 0000000..3612612 --- /dev/null +++ b/v1.5/lib/FastLED/ci/demo_symbol_analysis.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +""" +Demo script showing how to use the new generic symbol analysis functionality +""" + +import json +import subprocess +import sys +from pathlib import Path +from typing import List + + +def run_symbol_analysis(board_name: str): + """Run symbol analysis for a specific board""" + print(f"Running symbol analysis for {board_name}...") + + try: + result = subprocess.run( + [sys.executable, "ci/util/symbol_analysis.py", "--board", board_name], + cwd=".", + capture_output=True, + text=True, + ) + + if result.returncode == 0: + print(f"✅ Symbol analysis completed successfully for {board_name}") + return True + else: + print(f"❌ Symbol analysis failed for {board_name}") + print(f"Error: {result.stderr}") + return False + except Exception as e: + print(f"❌ Error running symbol analysis for {board_name}: {e}") + return False + + +def load_and_summarize_results(board_name: str): + """Load and display a summary of symbol analysis results""" + json_file = Path(f".build/{board_name}_symbol_analysis.json") + + if not json_file.exists(): + print(f"❌ Results file not found: {json_file}") + return + + try: + with open(json_file) as f: + data = json.load(f) + + summary = data["summary"] + print(f"\n📊 SUMMARY FOR {board_name.upper()}:") + print(f" Total symbols: {summary['total_symbols']}") + print( + f" Total size: {summary['total_size']} bytes ({summary['total_size'] / 1024:.1f} KB)" + ) + + print("\n🔝 TOP 5 LARGEST SYMBOLS:") + for i, symbol in enumerate(summary["largest_symbols"][:5], 1): + name = symbol.get("demangled_name", symbol["name"]) + print( + f" {i}. {symbol['size']:6d} bytes - {name[:80]}{'...' if len(name) > 80 else ''}" + ) + + print("\n📁 SYMBOL TYPES:") + for sym_type, stats in list(summary["type_breakdown"].items())[:5]: + print(f" {sym_type}: {stats['count']} symbols, {stats['size']} bytes") + + except Exception as e: + print(f"❌ Error loading results for {board_name}: {e}") + + +def main(): + """Demo the symbol analysis functionality""" + print("=" * 80) + print("FASTLED SYMBOL ANALYSIS DEMO") + print("=" * 80) + + # List of boards to analyze (you can add more) + boards_to_analyze: List[str] = [] + + # Check which boards have build info available + build_dir = Path(".build") + if build_dir.exists(): + for item in build_dir.iterdir(): + if item.is_dir() and (item / "build_info.json").exists(): + boards_to_analyze.append(item.name) + + if not boards_to_analyze: + print("❌ No boards with build_info.json found in .build directory") + print( + " Please compile some examples first using: uv run -m ci.ci-compile --examples Blink" + ) + sys.exit(1) + + print( + f"📋 Found {len(boards_to_analyze)} boards with build info: {', '.join(boards_to_analyze)}" + ) + + # Run symbol analysis for each board + successful_boards: List[str] = [] + for board in boards_to_analyze: + if run_symbol_analysis(board): + successful_boards.append(board) + + print(f"\n📈 RESULTS SUMMARY FOR {len(successful_boards)} BOARDS:") + print("=" * 80) + + # Load and summarize results + for board in successful_boards: + load_and_summarize_results(board) + print() + + print("🎉 Symbol analysis demo completed!") + print("📁 Detailed JSON files saved in .build/ directory") + print("💡 Use these files to identify optimization opportunities") + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/dockerfiles/DockerManager.py b/v1.5/lib/FastLED/ci/dockerfiles/DockerManager.py new file mode 100644 index 0000000..0bd3825 --- /dev/null +++ b/v1.5/lib/FastLED/ci/dockerfiles/DockerManager.py @@ -0,0 +1,339 @@ +import os +import subprocess +import sys +from typing import Any, Dict, List, Optional + + +class DockerManager: + def __init__(self): + # No direct client object, will use subprocess for docker CLI commands + pass + + def _run_docker_command( + self, + command: List[str], + check: bool = True, + stream_output: bool = False, + timeout: Optional[int] = None, + interrupt_pattern: Optional[str] = None, + output_file: Optional[str] = None, + ) -> subprocess.CompletedProcess[str]: + full_command = ["docker"] + command + print(f"Executing Docker command: {' '.join(full_command)}") + + # Set environment to prevent MSYS2/Git Bash path conversion on Windows + env = os.environ.copy() + # Set UTF-8 encoding environment variables for Windows + env["PYTHONIOENCODING"] = "utf-8" + env["PYTHONUTF8"] = "1" + # Only set MSYS_NO_PATHCONV if we're in a Git Bash/MSYS2 environment + if ( + "MSYSTEM" in os.environ + or os.environ.get("TERM") == "xterm" + or "bash.exe" in os.environ.get("SHELL", "") + ): + env["MSYS_NO_PATHCONV"] = "1" + + if stream_output: + # Use RunningProcess for streaming output + import re + import time + + from ci.util.running_process import RunningProcess + + proc = RunningProcess(full_command, env=env, auto_run=True) + pattern_found = False + timeout_occurred = False + start_time = time.time() + + # Open output file if specified + output_handle = None + if output_file: + try: + output_handle = open(output_file, "w", encoding="utf-8") + except Exception as e: + print(f"Warning: Could not open output file {output_file}: {e}") + + try: + with proc.line_iter(timeout=None) as it: + for line in it: + print(line) + + # Write to output file if specified + if output_handle: + output_handle.write(line + "\n") + output_handle.flush() # Ensure immediate write + + # Check for interrupt pattern (for informational purposes only) + if interrupt_pattern and re.search(interrupt_pattern, line): + print(f"Pattern detected: {line}") + pattern_found = True + + # Check timeout + if timeout and (time.time() - start_time) > timeout: + print( + f"Timeout reached ({timeout}s), terminating container..." + ) + timeout_occurred = True + break + + # Wait for process to complete + returncode = proc.wait() + + # Handle timeout case: return success (0) for timeouts as per requirement + if timeout_occurred: + print("Process terminated due to timeout - treating as success") + final_returncode = 0 + else: + # Return the actual container exit code for all other cases + final_returncode = returncode + + return subprocess.CompletedProcess( + args=full_command, returncode=final_returncode, stdout="", stderr="" + ) + + except Exception as e: + print(f"Error during streaming: {e}") + returncode = proc.wait() if hasattr(proc, "wait") else 1 + return subprocess.CompletedProcess( + args=full_command, returncode=returncode, stdout="", stderr=str(e) + ) + finally: + # Close output file if it was opened + if output_handle: + output_handle.close() + else: + # Use regular subprocess for non-streaming commands + result = subprocess.run( + full_command, + capture_output=True, + text=True, + check=check, + env=env, + encoding="utf-8", + errors="replace", + ) + if check and result.returncode != 0: + print( + f"Docker command failed with exit code {result.returncode}", + file=sys.stderr, + ) + print(f"Stdout: {result.stdout}", file=sys.stderr) + print(f"Stderr: {result.stderr}", file=sys.stderr) + return result + + def pull_image(self, image_name: str, tag: str = "latest"): + """Pulls the specified Docker image.""" + print(f"Pulling image: {image_name}:{tag}") + self._run_docker_command(["pull", f"{image_name}:{tag}"]) + print(f"Image {image_name}:{tag} pulled successfully.") + + def run_container( + self, + image_name: str, + command: List[str], + volumes: Optional[Dict[str, Dict[str, str]]] = None, + environment: Optional[Dict[str, str]] = None, + detach: bool = False, + name: Optional[str] = None, + ) -> str: # Return container ID as string + """ + Runs a Docker container with specified command, volume mounts, and environment variables. + Returns the container ID. + """ + print( + f"Running container from image: {image_name} with command: {' '.join(command)}" + ) + docker_cmd = ["run", "--rm"] # --rm to automatically remove container on exit + if detach: + docker_cmd.append("-d") + if name: + docker_cmd.extend(["--name", name]) + if volumes: + for host_path, container_path_info in volumes.items(): + mode = container_path_info.get("mode", "rw") + docker_cmd.extend( + ["-v", f"{host_path}:{container_path_info['bind']}:{mode}"] + ) + if environment: + for key, value in environment.items(): + docker_cmd.extend(["-e", f"{key}={value}"]) + + docker_cmd.append(image_name) + docker_cmd.extend(command) + + result = self._run_docker_command(docker_cmd) + container_id = result.stdout.strip() + print(f"Container {container_id} started.") + return container_id + + def run_container_streaming( + self, + image_name: str, + command: List[str], + volumes: Optional[Dict[str, Dict[str, str]]] = None, + environment: Optional[Dict[str, str]] = None, + name: Optional[str] = None, + timeout: Optional[int] = None, + interrupt_pattern: Optional[str] = None, + output_file: Optional[str] = None, + ) -> int: + """ + Runs a Docker container with streaming output. + Returns actual container exit code (0 for success, timeout is treated as success). + + Args: + image_name: Docker image to run + command: Command to execute in container + volumes: Volume mounts + environment: Environment variables + name: Container name + timeout: Timeout in seconds (if exceeded, returns 0) + interrupt_pattern: Pattern to detect in output (informational only) + output_file: Optional file path to write output to + """ + print( + f"Running container from image: {image_name} with command: {' '.join(command)}" + ) + docker_cmd = ["run", "--rm"] # --rm to automatically remove container on exit + if name: + docker_cmd.extend(["--name", name]) + if volumes: + for host_path, container_path_info in volumes.items(): + mode = container_path_info.get("mode", "rw") + docker_cmd.extend( + ["-v", f"{host_path}:{container_path_info['bind']}:{mode}"] + ) + if environment: + for key, value in environment.items(): + docker_cmd.extend(["-e", f"{key}={value}"]) + + docker_cmd.append(image_name) + docker_cmd.extend(command) + + result = self._run_docker_command( + docker_cmd, + check=False, + stream_output=True, + timeout=timeout, + interrupt_pattern=interrupt_pattern, + output_file=output_file, + ) + return result.returncode + + def get_container_logs(self, container_id_or_name: str) -> str: + """Retrieves logs from a container.""" + print(f"Getting logs for container: {container_id_or_name}") + try: + result = self._run_docker_command( + ["logs", container_id_or_name], check=False + ) + if result.returncode == 0: + logs = result.stdout + print(f"Logs retrieved for {container_id_or_name}.") + return logs + else: + # Container may have exited, try to get logs anyway + print(f"Warning: docker logs returned exit code {result.returncode}") + logs = result.stdout if result.stdout else result.stderr + return logs + except Exception as e: + print(f"Warning: Failed to get container logs: {e}") + return "" + + def stop_container(self, container_id_or_name: str): + """Stops a running container.""" + print(f"Stopping container: {container_id_or_name}") + self._run_docker_command(["stop", container_id_or_name]) + print(f"Container {container_id_or_name} stopped.") + + def remove_container(self, container_id_or_name: str): + """Removes a container.""" + print(f"Removing container: {container_id_or_name}") + self._run_docker_command(["rm", container_id_or_name]) + print(f"Container {container_id_or_name} removed.") + + def execute_command_in_container( + self, container_id_or_name: str, command: List[str] + ) -> tuple[int, str, str]: + """Executes a command inside a running container and returns exit code, stdout, and stderr.""" + print( + f"Executing command '{' '.join(command)}' in container: {container_id_or_name}" + ) + result = self._run_docker_command( + ["exec", container_id_or_name] + command, check=False + ) + stdout = result.stdout + stderr = result.stderr + print( + f"Command executed in {container_id_or_name}. Exit code: {result.returncode}" + ) + return result.returncode, stdout, stderr + + +if __name__ == "__main__": + # Example Usage: + manager = DockerManager() + image_to_use = "mluis/qemu-esp32" + container_name = "test_esp32_qemu" + + try: + print(f"--- Starting DockerManager test with image: {image_to_use} ---") + manager.pull_image(image_to_use) + + # Example: Run a simple command in the container + print("\n--- Running a simple command ---") + container_id = manager.run_container( + image_name=image_to_use, + command=["echo", "Hello from Docker!"], + detach=True, + name=container_name, + ) + + # Wait a bit for the command to execute and logs to be generated + import time + + time.sleep(2) + + logs = manager.get_container_logs(container_id) + print("Container Logs:\n", logs) + + # Check if the expected output is in the logs + if "Hello from Docker!" in logs: + print("SUCCESS: Expected output found in container logs.") + else: + print("FAILURE: Expected output NOT found in container logs.") + + # Example: Execute a command inside a running container + print("\n--- Executing command inside container ---") + exit_code, stdout, stderr = manager.execute_command_in_container( + container_id, ["sh", "-c", "echo 'Executed inside!'; exit 42"] + ) + print(f"Exec command stdout: {stdout}") + print(f"Exec command stderr: {stderr}") + print(f"Exec command exit code: {exit_code}") + if exit_code == 42 and "Executed inside!" in stdout: + print("SUCCESS: Command executed inside container as expected.") + else: + print("FAILURE: Command execution inside container failed.") + + except subprocess.CalledProcessError as e: + print(f"Error during Docker operation: {e}", file=sys.stderr) + print(f"Stdout: {e.stdout}", file=sys.stderr) + print(f"Stderr: {e.stderr}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"An unexpected error occurred: {e}", file=sys.stderr) + sys.exit(1) + finally: + # Ensure cleanup + try: + manager.stop_container(container_name) + manager.remove_container(container_name) + print(f"Container {container_name} stopped and removed.") + except subprocess.CalledProcessError: + print( + f"Container {container_name} not found or already removed during cleanup." + ) + except Exception as e: + print(f"Error during cleanup of {container_name}: {e}", file=sys.stderr) diff --git a/v1.5/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32 b/v1.5/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32 new file mode 100644 index 0000000..b681e84 --- /dev/null +++ b/v1.5/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32 @@ -0,0 +1,106 @@ +# Dockerfile for ESP32 QEMU emulation environment +# Based on ESP-IDF with QEMU support + +FROM espressif/idf:v5.1.2 + +# Install additional dependencies for QEMU +RUN apt-get update && apt-get install -y \ + qemu-system-misc \ + qemu-utils \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies for ESP tools +RUN pip3 install --no-cache-dir \ + esptool \ + pyserial + +# Create workspace directory +WORKDIR /workspace + +# Set up environment for QEMU ESP32 +ENV QEMU_ESP32_VERSION=esp-develop-8.2.0-20240122 + +# Download and install ESP32 QEMU if not using system QEMU +RUN mkdir -p /opt/qemu-esp32 && \ + cd /opt/qemu-esp32 && \ + wget -q https://github.com/espressif/qemu/releases/download/${QEMU_ESP32_VERSION}/qemu-xtensa-softmmu-esp_develop_8.2.0_20240122-x86_64-linux-gnu.tar.xz && \ + tar -xf qemu-xtensa-softmmu-esp_develop_8.2.0_20240122-x86_64-linux-gnu.tar.xz && \ + rm qemu-xtensa-softmmu-esp_develop_8.2.0_20240122-x86_64-linux-gnu.tar.xz && \ + ln -sf /opt/qemu-esp32/qemu/bin/qemu-system-xtensa /usr/local/bin/qemu-system-xtensa + +# Download ESP32 ROM files for QEMU +RUN mkdir -p /opt/esp-rom && \ + cd /opt/esp-rom && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32-v3-rom.bin && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32-v3-rom-eco3.bin && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32c3-rom.bin && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32s2-rom.bin && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32s3-rom.bin + +# Set environment variables for ROM files +ENV ESP_ROM_PATH=/opt/esp-rom +ENV ESP32_ROM=${ESP_ROM_PATH}/esp32-v3-rom.bin +ENV ESP32C3_ROM=${ESP_ROM_PATH}/esp32c3-rom.bin +ENV ESP32S2_ROM=${ESP_ROM_PATH}/esp32s2-rom.bin +ENV ESP32S3_ROM=${ESP_ROM_PATH}/esp32s3-rom.bin + +# Create helper script for running QEMU with proper settings +RUN cat > /usr/local/bin/run-qemu-esp32 << 'EOF' +#!/bin/bash +# Helper script to run QEMU with ESP32 firmware + +FIRMWARE_PATH=${1:-/workspace/firmware.bin} +MACHINE=${2:-esp32} +FLASH_SIZE=${3:-4} + +# Select ROM file based on machine type +case "$MACHINE" in + esp32) + ROM_FILE=$ESP32_ROM + ;; + esp32c3) + ROM_FILE=$ESP32C3_ROM + ;; + esp32s2) + ROM_FILE=$ESP32S2_ROM + ;; + esp32s3) + ROM_FILE=$ESP32S3_ROM + ;; + *) + echo "Unknown machine type: $MACHINE" + exit 1 + ;; +esac + +# Create flash image if needed +FLASH_IMAGE=/tmp/flash_${RANDOM}.bin +if [ -f "$FIRMWARE_PATH" ]; then + # Create empty flash image + dd if=/dev/zero of=$FLASH_IMAGE bs=1M count=$FLASH_SIZE 2>/dev/null + dd if=$FIRMWARE_PATH of=$FLASH_IMAGE bs=1 seek=$((0x10000)) conv=notrunc 2>/dev/null +else + echo "Firmware not found: $FIRMWARE_PATH" + exit 1 +fi + +# Run QEMU +exec qemu-system-xtensa \ + -nographic \ + -machine $MACHINE \ + -bios $ROM_FILE \ + -drive file=$FLASH_IMAGE,if=mtd,format=raw \ + -global driver=timer.esp32.timg,property=wdt_disable,value=true \ + "$@" +EOF + +RUN chmod +x /usr/local/bin/run-qemu-esp32 + +# Default command +CMD ["/usr/local/bin/run-qemu-esp32"] + +# Labels +LABEL maintainer="FastLED Team" +LABEL description="ESP32 QEMU emulation environment for FastLED testing" +LABEL version="1.0.0" \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32-lite b/v1.5/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32-lite new file mode 100644 index 0000000..6f1dc82 --- /dev/null +++ b/v1.5/lib/FastLED/ci/dockerfiles/Dockerfile.qemu-esp32-lite @@ -0,0 +1,92 @@ +# Lightweight Dockerfile for ESP32 QEMU emulation +# Uses pre-built QEMU binaries without full ESP-IDF + +FROM ubuntu:22.04 + +# Install minimal dependencies +RUN apt-get update && apt-get install -y \ + wget \ + xz-utils \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# Install esptool for flash image creation +RUN pip3 install --no-cache-dir esptool + +# Create workspace +WORKDIR /workspace + +# Download and install ESP32 QEMU binaries +ENV QEMU_VERSION=esp-develop-8.2.0-20240122 +RUN mkdir -p /opt/qemu && \ + cd /opt/qemu && \ + wget -q https://github.com/espressif/qemu/releases/download/${QEMU_VERSION}/qemu-xtensa-softmmu-${QEMU_VERSION}-x86_64-linux-gnu.tar.xz && \ + tar -xf qemu-xtensa-softmmu-${QEMU_VERSION}-x86_64-linux-gnu.tar.xz && \ + rm qemu-xtensa-softmmu-${QEMU_VERSION}-x86_64-linux-gnu.tar.xz && \ + mv qemu /usr/local/ + +# Add QEMU to PATH +ENV PATH=/usr/local/qemu/bin:$PATH + +# Download ESP32 ROM files +RUN mkdir -p /opt/esp-rom && \ + cd /opt/esp-rom && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32-v3-rom.bin && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32c3-rom.bin && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32s2-rom.bin && \ + wget -q https://github.com/espressif/qemu/raw/esp-develop/pc-bios/esp32s3-rom.bin + +# Set ROM path environment variable +ENV ESP_ROM_PATH=/opt/esp-rom + +# Create entrypoint script +RUN cat > /entrypoint.sh << 'EOF' +#!/bin/bash +set -e + +# Default values +FIRMWARE=${FIRMWARE:-/workspace/firmware.bin} +MACHINE=${MACHINE:-esp32} +FLASH_SIZE=${FLASH_SIZE:-4} +TIMEOUT=${TIMEOUT:-30} + +# Select ROM based on machine type +case "$MACHINE" in + esp32) ROM_FILE=/opt/esp-rom/esp32-v3-rom.bin ;; + esp32c3) ROM_FILE=/opt/esp-rom/esp32c3-rom.bin ;; + esp32s2) ROM_FILE=/opt/esp-rom/esp32s2-rom.bin ;; + esp32s3) ROM_FILE=/opt/esp-rom/esp32s3-rom.bin ;; + *) echo "Unknown machine: $MACHINE"; exit 1 ;; +esac + +# Check if firmware exists +if [ ! -f "$FIRMWARE" ]; then + echo "ERROR: Firmware not found: $FIRMWARE" + exit 1 +fi + +# Create flash image +FLASH_IMG=/tmp/flash.bin +dd if=/dev/zero of=$FLASH_IMG bs=1M count=$FLASH_SIZE 2>/dev/null +dd if=$FIRMWARE of=$FLASH_IMG bs=1 seek=$((0x10000)) conv=notrunc 2>/dev/null + +# Run QEMU with timeout +timeout $TIMEOUT qemu-system-xtensa \ + -nographic \ + -machine $MACHINE \ + -bios $ROM_FILE \ + -drive file=$FLASH_IMG,if=mtd,format=raw \ + -global driver=timer.esp32.timg,property=wdt_disable,value=true \ + "$@" +EOF + +RUN chmod +x /entrypoint.sh + +# Set entrypoint +ENTRYPOINT ["/entrypoint.sh"] + +# Labels +LABEL maintainer="FastLED Team" +LABEL description="Lightweight ESP32 QEMU Docker image" +LABEL version="1.0.0" \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/dockerfiles/README.md b/v1.5/lib/FastLED/ci/dockerfiles/README.md new file mode 100644 index 0000000..6ca608d --- /dev/null +++ b/v1.5/lib/FastLED/ci/dockerfiles/README.md @@ -0,0 +1,205 @@ +# Docker-based QEMU ESP32 Testing + +This directory contains Docker-based QEMU ESP32 emulation tools for FastLED testing. + +## Overview + +The Docker QEMU solution provides: +- Consistent testing environment across different platforms +- No need for local QEMU installation +- Isolated execution environment +- Support for multiple ESP32 variants (ESP32, ESP32-C3, ESP32-S2, ESP32-S3) + +## Components + +### Core Files + +- `DockerManager.py` - Docker container management utilities +- `qemu_esp32_docker.py` - Main Docker QEMU runner script +- `qemu_test_integration.py` - Integration with existing test framework +- `Dockerfile.qemu-esp32` - Full ESP-IDF + QEMU environment +- `Dockerfile.qemu-esp32-lite` - Lightweight QEMU-only environment +- `docker-compose.yml` - Docker Compose configuration + +## Usage + +### Quick Start + +1. **Run a firmware test with Docker:** + ```bash + uv run ci/docker/qemu_esp32_docker.py path/to/firmware.bin + ``` + +2. **Run with specific timeout and interrupt pattern:** + ```bash + uv run ci/docker/qemu_esp32_docker.py firmware.bin \ + --timeout 60 \ + --interrupt-regex "Test passed" + ``` + +3. **Use docker-compose:** + ```bash + cd ci/docker + FIRMWARE_DIR=../../.pio/build/esp32dev docker-compose up qemu-esp32 + ``` + +### Integration with Test Framework + +The Docker QEMU runner integrates with the existing test framework: + +```bash +# Check available runners +uv run ci/docker/qemu_test_integration.py check + +# Run test with automatic runner selection +uv run ci/docker/qemu_test_integration.py test --firmware path/to/firmware.bin + +# Force Docker runner +uv run ci/docker/qemu_test_integration.py test --firmware firmware.bin --docker +``` + +### Building Docker Images + +```bash +# Build lightweight image +docker build -f Dockerfile.qemu-esp32-lite -t fastled/qemu-esp32:lite . + +# Build full ESP-IDF image +docker build -f Dockerfile.qemu-esp32 -t fastled/qemu-esp32:full . + +# Or use docker-compose +docker-compose build +``` + +## Docker Images + +### Pre-built Images + +The runner can use pre-built images: +- `espressif/qemu:esp-develop-8.2.0-20240122` (Official Espressif image) +- `mluis/qemu-esp32:latest` (Community image) + +### Custom Images + +Two Dockerfiles are provided: + +1. **Dockerfile.qemu-esp32-lite** (Recommended) + - Minimal Ubuntu base + - QEMU binaries only + - ~200MB image size + - Fast startup + +2. **Dockerfile.qemu-esp32** + - Full ESP-IDF environment + - Development tools included + - ~2GB image size + - Complete toolchain + +## Features + +### Automatic Fallback + +The integration module automatically selects the best available runner: +1. Native QEMU (if installed) +2. Docker QEMU (if Docker available) +3. Error if neither available + +### Platform Support + +Works on: +- Linux (native Docker) +- macOS (Docker Desktop) +- Windows (Docker Desktop, WSL2) + +### Firmware Formats + +Supports: +- Direct `firmware.bin` files +- PlatformIO build directories +- ESP-IDF build directories + +## Environment Variables + +- `FIRMWARE_DIR` - Directory containing firmware files +- `FIRMWARE_FILE` - Specific firmware file to run +- `MACHINE` - ESP32 variant (esp32, esp32c3, esp32s2, esp32s3) +- `FLASH_SIZE` - Flash size in MB (default: 4) +- `TIMEOUT` - Execution timeout in seconds (default: 30) + +## Troubleshooting + +### Docker Not Found + +Install Docker: +- **Windows/Mac**: Install Docker Desktop +- **Linux**: `sudo apt-get install docker.io` or equivalent + +### Permission Denied + +Add user to docker group: +```bash +sudo usermod -aG docker $USER +# Log out and back in +``` + +### Image Pull Failed + +Check internet connection and Docker Hub access. The runner will automatically try alternative images if the primary fails. + +### QEMU Timeout + +Increase timeout value: +```bash +uv run ci/docker/qemu_esp32_docker.py firmware.bin --timeout 120 +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +- name: Run QEMU Test in Docker + run: | + uv run ci/docker/qemu_esp32_docker.py \ + .pio/build/esp32dev/firmware.bin \ + --timeout 60 \ + --interrupt-regex "Setup complete" +``` + +### Local CI + +```bash +# Install dependencies +uv pip install -r requirements.txt + +# Run tests +uv run ci/docker/qemu_test_integration.py test \ + --firmware .pio/build/esp32dev/firmware.bin +``` + +## Development + +### Adding New ESP32 Variants + +1. Update Dockerfiles with new ROM files +2. Add machine type to runner scripts +3. Test with sample firmware + +### Debugging + +Run interactively: +```bash +uv run ci/docker/qemu_esp32_docker.py firmware.bin --interactive +``` + +Enable verbose output: +```bash +docker-compose up qemu-esp32-interactive +``` + +## Performance + +- Docker adds ~2-5 seconds overhead vs native QEMU +- First run pulls image (one-time ~100MB download) +- Subsequent runs use cached image +- Consider using lightweight image for CI/CD \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/dockerfiles/__init__.py b/v1.5/lib/FastLED/ci/dockerfiles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v1.5/lib/FastLED/ci/dockerfiles/docker-compose.yml b/v1.5/lib/FastLED/ci/dockerfiles/docker-compose.yml new file mode 100644 index 0000000..4be5423 --- /dev/null +++ b/v1.5/lib/FastLED/ci/dockerfiles/docker-compose.yml @@ -0,0 +1,61 @@ +version: '3.8' + +services: + qemu-esp32: + build: + context: . + dockerfile: Dockerfile.qemu-esp32-lite + image: fastled/qemu-esp32:latest + container_name: fastled-qemu-esp32 + volumes: + # Mount firmware directory + - ${FIRMWARE_DIR:-./firmware}:/workspace:ro + # Mount output directory for logs + - ${OUTPUT_DIR:-./output}:/output + environment: + - FIRMWARE=${FIRMWARE_FILE:-/workspace/firmware.bin} + - MACHINE=${MACHINE:-esp32} + - FLASH_SIZE=${FLASH_SIZE:-4} + - TIMEOUT=${TIMEOUT:-30} + command: [] + stdin_open: false + tty: false + networks: + - qemu-network + + qemu-esp32-interactive: + build: + context: . + dockerfile: Dockerfile.qemu-esp32-lite + image: fastled/qemu-esp32:latest + container_name: fastled-qemu-esp32-interactive + volumes: + - ${FIRMWARE_DIR:-./firmware}:/workspace:ro + environment: + - FIRMWARE=${FIRMWARE_FILE:-/workspace/firmware.bin} + - MACHINE=${MACHINE:-esp32} + - FLASH_SIZE=${FLASH_SIZE:-4} + - TIMEOUT=${TIMEOUT:-300} + stdin_open: true + tty: true + networks: + - qemu-network + + # Service for building the Docker image + builder: + build: + context: . + dockerfile: Dockerfile.qemu-esp32-lite + image: fastled/qemu-esp32:latest + command: echo "Image built successfully" + +networks: + qemu-network: + driver: bridge + +# Volume definitions (optional, for persistent storage) +volumes: + firmware: + driver: local + output: + driver: local \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/dockerfiles/qemu_esp32_docker.py b/v1.5/lib/FastLED/ci/dockerfiles/qemu_esp32_docker.py new file mode 100644 index 0000000..113ee45 --- /dev/null +++ b/v1.5/lib/FastLED/ci/dockerfiles/qemu_esp32_docker.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 +""" +Docker-based QEMU ESP32 Runner + +Runs ESP32 firmware in QEMU using Docker containers for better isolation +and consistency across different environments. +""" + +import argparse +import os +import platform +import re +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +from ci.dockerfiles.DockerManager import DockerManager +from ci.util.running_process import RunningProcess + + +def get_docker_env() -> Dict[str, str]: + """Get environment for Docker commands, handling Git Bash/MSYS2 path conversion.""" + env = os.environ.copy() + # Set UTF-8 encoding environment variables for Windows + env["PYTHONIOENCODING"] = "utf-8" + env["PYTHONUTF8"] = "1" + # Only set MSYS_NO_PATHCONV if we're in a Git Bash/MSYS2 environment + if ( + "MSYSTEM" in os.environ + or os.environ.get("TERM") == "xterm" + or "bash.exe" in os.environ.get("SHELL", "") + ): + env["MSYS_NO_PATHCONV"] = "1" + return env + + +def run_subprocess_safe( + cmd: List[str], **kwargs: Any +) -> subprocess.CompletedProcess[Any]: + """Run subprocess with safe UTF-8 handling and error replacement.""" + kwargs.setdefault("encoding", "utf-8") + kwargs.setdefault("errors", "replace") + kwargs.setdefault("env", get_docker_env()) + return subprocess.run(cmd, **kwargs) # type: ignore[misc] + + +def run_docker_command_streaming(cmd: List[str]) -> int: + """Run docker command with streaming output using RunningProcess.""" + print(f"Executing: {' '.join(cmd)}") + proc = RunningProcess(cmd, env=get_docker_env(), auto_run=True) + + # Stream all output to stdout + with proc.line_iter(timeout=None) as it: + for line in it: + print(line) + + returncode = proc.wait() + if returncode != 0: + raise subprocess.CalledProcessError(returncode, cmd) + return returncode + + +def run_docker_command_no_fail(cmd: List[str]) -> int: + """Run docker command with streaming output, but don't raise exceptions on failure.""" + print(f"Executing: {' '.join(cmd)}") + proc = RunningProcess(cmd, env=get_docker_env(), auto_run=True) + + # Stream all output to stdout + with proc.line_iter(timeout=None) as it: + for line in it: + print(line) + + return proc.wait() + + +class DockerQEMURunner: + """Runner for ESP32 QEMU emulation using Docker containers.""" + + DEFAULT_IMAGE = "mluis/qemu-esp32:latest" + ALTERNATIVE_IMAGE = "espressif/idf:latest" + FALLBACK_IMAGE = "espressif/idf:release-v5.2" + + def __init__(self, docker_image: Optional[str] = None): + """Initialize Docker QEMU runner. + + Args: + docker_image: Docker image to use, defaults to espressif/qemu + """ + self.docker_manager = DockerManager() + self.docker_image = docker_image or self.DEFAULT_IMAGE + self.container_name = None + # Use Linux-style paths for all containers since we're using Ubuntu/Alpine + self.firmware_mount_path = "/workspace/firmware" + + def check_docker_available(self) -> bool: + """Check if Docker is available and running.""" + try: + proc = RunningProcess( + ["docker", "version"], env=get_docker_env(), auto_run=True + ) + returncode = proc.wait(timeout=5) + return returncode == 0 + except (Exception, FileNotFoundError): + return False + + def pull_image(self): + """Pull the Docker image if not already available.""" + print(f"Ensuring Docker image {self.docker_image} is available...") + try: + # Check if image exists locally + proc = RunningProcess( + ["docker", "images", "-q", self.docker_image], + env=get_docker_env(), + auto_run=True, + ) + stdout_lines: List[str] = [] + with proc.line_iter(timeout=None) as it: + for line in it: + stdout_lines.append(line) + result_stdout = "\n".join(stdout_lines) + if not result_stdout.strip(): + # Image doesn't exist, pull it directly using docker command + print(f"Pulling Docker image: {self.docker_image}") + run_docker_command_streaming(["docker", "pull", self.docker_image]) + print(f"Successfully pulled {self.docker_image}") + else: + print(f"Image {self.docker_image} already available locally") + except subprocess.CalledProcessError as e: + print(f"Warning: Failed to pull image {self.docker_image}: {e}") + if self.docker_image == self.DEFAULT_IMAGE: + print(f"Trying alternative image: {self.ALTERNATIVE_IMAGE}") + self.docker_image = self.ALTERNATIVE_IMAGE + try: + print(f"Pulling alternative Docker image: {self.docker_image}") + run_docker_command_streaming(["docker", "pull", self.docker_image]) + print(f"Successfully pulled {self.docker_image}") + except subprocess.CalledProcessError as e2: + print(f"Failed to pull alternative image: {e2}") + print(f"Trying fallback image: {self.FALLBACK_IMAGE}") + self.docker_image = self.FALLBACK_IMAGE + try: + print(f"Pulling fallback Docker image: {self.docker_image}") + run_docker_command_streaming( + ["docker", "pull", self.docker_image] + ) + print(f"Successfully pulled {self.docker_image}") + except subprocess.CalledProcessError as e3: + print(f"Failed to pull fallback image: {e3}") + raise e3 + else: + raise e + + def prepare_firmware(self, firmware_path: Path) -> Path: + """Prepare firmware files for mounting into Docker container. + + Args: + firmware_path: Path to firmware.bin or build directory + + Returns: + Path to the prepared firmware directory + """ + # Create temporary directory for firmware files + temp_dir = Path(tempfile.mkdtemp(prefix="qemu_firmware_")) + + try: + if firmware_path.is_file() and firmware_path.suffix == ".bin": + # Copy single firmware file + shutil.copy2(firmware_path, temp_dir / "firmware.bin") + # Create proper 4MB flash image for QEMU + self._create_flash_image(firmware_path, temp_dir / "flash.bin") + else: + # Copy entire build directory + if firmware_path.is_dir(): + # Copy relevant files from build directory + patterns = ["*.bin", "*.elf", "partitions.csv", "flash_args"] + for pattern in patterns: + for file in firmware_path.glob(pattern): + shutil.copy2(file, temp_dir) + + # Also check for PlatformIO build structure + pio_build = firmware_path / ".pio" / "build" / "esp32dev" + if pio_build.exists(): + for pattern in patterns: + for file in pio_build.glob(pattern): + shutil.copy2(file, temp_dir) + + # Create proper flash.bin file from firmware.bin + firmware_bin = temp_dir / "firmware.bin" + if firmware_bin.exists(): + # Create proper 4MB flash image for QEMU + self._create_flash_image(firmware_bin, temp_dir / "flash.bin") + else: + # Create a minimal 4MB flash.bin for testing + flash_bin = temp_dir / "flash.bin" + flash_bin.write_bytes( + b"\xff" * (4 * 1024 * 1024) + ) # 4MB of 0xFF + else: + raise ValueError(f"Invalid firmware path: {firmware_path}") + + return temp_dir + except Exception as e: + # Clean up on error + shutil.rmtree(temp_dir, ignore_errors=True) + raise e + + def _create_flash_image( + self, firmware_path: Path, output_path: Path, flash_size_mb: int = 4 + ): + """Create a proper flash image for QEMU ESP32. + + Args: + firmware_path: Path to the firmware.bin file + output_path: Path where to write the flash.bin + flash_size_mb: Flash size in MB (must be 2, 4, 8, or 16) + """ + if flash_size_mb not in [2, 4, 8, 16]: + raise ValueError( + f"Flash size must be 2, 4, 8, or 16 MB, got {flash_size_mb}" + ) + + flash_size = flash_size_mb * 1024 * 1024 + + # Read firmware content + firmware_data = firmware_path.read_bytes() + + # Create flash image: firmware at beginning, rest filled with 0xFF + flash_data = firmware_data + b"\xff" * (flash_size - len(firmware_data)) + + # Ensure we have exactly the right size + if len(flash_data) > flash_size: + raise ValueError( + f"Firmware size ({len(firmware_data)} bytes) exceeds flash size ({flash_size} bytes)" + ) + + flash_data = flash_data[:flash_size] # Truncate to exact size + + output_path.write_bytes(flash_data) + + def build_qemu_command( + self, + firmware_name: str = "firmware.bin", + flash_size: int = 4, + machine: str = "esp32", + ) -> List[str]: + """Build QEMU command to run inside Docker container. + + Args: + firmware_name: Name of firmware file in mounted directory + flash_size: Flash size in MB + machine: QEMU machine type (esp32, esp32c3, esp32s3, etc.) + + Returns: + List of command arguments for QEMU + """ + # For Linux containers, use a workaround to run QEMU through Docker Desktop's Windows integration + firmware_path = f"{self.firmware_mount_path}/flash.bin" + + # Determine QEMU system and machine based on target + if machine == "esp32c3": + qemu_system = "/usr/local/bin/qemu-system-riscv32" + qemu_machine = "esp32c3" + echo_target = "ESP32C3" + elif machine == "esp32s3": + qemu_system = "/usr/local/bin/qemu-system-xtensa" + qemu_machine = "esp32s3" + echo_target = "ESP32S3" + else: + # Default to ESP32 (Xtensa) + qemu_system = "/usr/local/bin/qemu-system-xtensa" + qemu_machine = "esp32" + echo_target = "ESP32" + + # Use container's QEMU with proper configuration + wrapper_script = f'''#!/bin/bash +set -e +echo "Starting {echo_target} QEMU emulation..." +echo "Firmware: {firmware_path}" +echo "Machine: {qemu_machine}" +echo "QEMU system: {qemu_system}" +echo "Container: $(cat /etc/os-release | head -1)" + +# Check if firmware file exists +if [ ! -f "{firmware_path}" ]; then + echo "ERROR: Firmware file not found: {firmware_path}" + exit 1 +fi + +# Check firmware size +FIRMWARE_SIZE=$(stat -c%s "{firmware_path}") +echo "Firmware size: $FIRMWARE_SIZE bytes" + +# Copy firmware to writable location since QEMU needs write access +cp "{firmware_path}" /tmp/flash.bin +echo "Copied firmware to writable location: /tmp/flash.bin" + +# Try different QEMU configurations depending on machine type +if [ "{qemu_machine}" = "esp32c3" ]; then + # ESP32C3 uses RISC-V architecture + echo "Running {qemu_system} for {qemu_machine}" + {qemu_system} \\ + -nographic \\ + -machine {qemu_machine} \\ + -drive file="/tmp/flash.bin",if=mtd,format=raw \\ + -monitor none \\ + -serial mon:stdio +else + # ESP32 uses Xtensa architecture + echo "Running {qemu_system} for {qemu_machine}" + {qemu_system} \\ + -nographic \\ + -machine {qemu_machine} \\ + -drive file="/tmp/flash.bin",if=mtd,format=raw \\ + -global driver=timer.esp32.timg,property=wdt_disable,value=true \\ + -monitor none \\ + -serial mon:stdio +fi + +echo "QEMU execution completed" +exit 0 +''' + + cmd = ["bash", "-c", wrapper_script] + + return cmd + + def run( + self, + firmware_path: Path, + timeout: int = 30, + flash_size: int = 4, + interrupt_regex: Optional[str] = None, + interactive: bool = False, + output_file: Optional[str] = None, + machine: str = "esp32", + ) -> int: + """Run ESP32/ESP32C3/ESP32S3 firmware in QEMU using Docker. + + Args: + firmware_path: Path to firmware.bin or build directory + timeout: Timeout in seconds (timeout is treated as success) + flash_size: Flash size in MB + interrupt_regex: Regex pattern to detect in output (informational only) + interactive: Run in interactive mode (attach to container) + output_file: Optional file path to write QEMU output to + machine: QEMU machine type (esp32, esp32c3, esp32s3, etc.) + + Returns: + Exit code: actual QEMU/container exit code, except timeout returns 0 + """ + if not self.check_docker_available(): + print("ERROR: Docker is not available or not running", file=sys.stderr) + print("Please install Docker and ensure it's running", file=sys.stderr) + return 1 + + # Pull image if needed + self.pull_image() + + # Prepare firmware files + print(f"Preparing firmware from: {firmware_path}") + temp_firmware_dir = None + + try: + temp_firmware_dir = self.prepare_firmware(firmware_path) + + # Generate unique container name + self.container_name = f"qemu_esp32_{int(time.time())}" + + # Convert Windows paths to Docker-compatible paths + def windows_to_docker_path(path_str: Union[str, Path]) -> str: + """Convert Windows path to Docker volume mount format.""" + import os + + # Check if we're in Git Bash/MSYS2 environment + is_git_bash = ( + "MSYSTEM" in os.environ + or os.environ.get("TERM") == "xterm" + or "bash.exe" in os.environ.get("SHELL", "") + ) + + if os.name == "nt" and is_git_bash: + # Convert C:\path\to\dir to /c/path/to/dir for Git Bash + path_str = str(path_str).replace("\\", "/") + if len(path_str) > 2 and path_str[1:3] == ":/": # Drive letter + path_str = "/" + path_str[0].lower() + path_str[2:] + else: + # For cmd.exe on Windows, keep native Windows paths + path_str = str(path_str) + + return path_str + + docker_firmware_path = windows_to_docker_path(temp_firmware_dir) + + # Only mount the firmware directory - QEMU is built into the container + volumes = { + docker_firmware_path: { + "bind": self.firmware_mount_path, + "mode": "ro", # Read-only mount + }, + } + + # Build QEMU command + qemu_cmd = self.build_qemu_command( + firmware_name="firmware.bin", flash_size=flash_size, machine=machine + ) + + print(f"Running QEMU in Docker container: {self.container_name}") + print(f"QEMU command: {' '.join(qemu_cmd)}") + + if interactive: + # Run interactively with streaming + return self.docker_manager.run_container_streaming( + image_name=self.docker_image, + command=qemu_cmd, + volumes=volumes, + name=self.container_name, + timeout=timeout, + interrupt_pattern=interrupt_regex, + output_file=output_file, + ) + else: + # Run with streaming output and return actual exit code + return self.docker_manager.run_container_streaming( + image_name=self.docker_image, + command=qemu_cmd, + volumes=volumes, + name=self.container_name, + timeout=timeout, + interrupt_pattern=interrupt_regex, + output_file=output_file, + ) + + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + return 1 + finally: + # Cleanup + if temp_firmware_dir and temp_firmware_dir.exists(): + shutil.rmtree(temp_firmware_dir, ignore_errors=True) + + # Ensure container is stopped and removed + if self.container_name: + try: + # Stop and remove container + run_docker_command_no_fail(["docker", "stop", self.container_name]) + run_docker_command_no_fail(["docker", "rm", self.container_name]) + except: + pass + + +def main(): + parser = argparse.ArgumentParser( + description="Run ESP32 firmware in QEMU using Docker" + ) + parser.add_argument( + "firmware_path", type=Path, help="Path to firmware.bin file or build directory" + ) + parser.add_argument( + "--docker-image", + type=str, + help=f"Docker image to use (default: {DockerQEMURunner.DEFAULT_IMAGE})", + ) + parser.add_argument( + "--flash-size", type=int, default=4, help="Flash size in MB (default: 4)" + ) + parser.add_argument( + "--timeout", type=int, default=30, help="Timeout in seconds (default: 30)" + ) + parser.add_argument( + "--interrupt-regex", type=str, help="Regex pattern to interrupt execution" + ) + parser.add_argument( + "--interactive", action="store_true", help="Run in interactive mode" + ) + parser.add_argument("--output-file", type=str, help="File to write QEMU output to") + + args = parser.parse_args() + + if not args.firmware_path.exists(): + print( + f"ERROR: Firmware path does not exist: {args.firmware_path}", + file=sys.stderr, + ) + return 1 + + runner = DockerQEMURunner(docker_image=args.docker_image) + return runner.run( + firmware_path=args.firmware_path, + timeout=args.timeout, + flash_size=args.flash_size, + interrupt_regex=args.interrupt_regex, + interactive=args.interactive, + output_file=args.output_file, + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/dockerfiles/qemu_test_integration.py b/v1.5/lib/FastLED/ci/dockerfiles/qemu_test_integration.py new file mode 100644 index 0000000..f052c60 --- /dev/null +++ b/v1.5/lib/FastLED/ci/dockerfiles/qemu_test_integration.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +Integration module for Docker-based QEMU ESP32 testing. + +This module provides a bridge between the existing test infrastructure +and the Docker-based QEMU runner. +""" + +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Optional, Union + +from ci.dockerfiles.qemu_esp32_docker import DockerQEMURunner + + +class QEMUTestIntegration: + """Integration class for QEMU testing with Docker fallback.""" + + def __init__(self, prefer_docker: bool = False): + """Initialize QEMU test integration. + + Args: + prefer_docker: If True, prefer Docker even if native QEMU is available + """ + self.prefer_docker = prefer_docker + self.docker_available = self._check_docker() + self.native_qemu_available = self._check_native_qemu() + + def _check_docker(self) -> bool: + """Check if Docker is available.""" + try: + result = subprocess.run( + ["docker", "version"], capture_output=True, timeout=5 + ) + return result.returncode == 0 + except (subprocess.SubprocessError, FileNotFoundError): + return False + + def _check_native_qemu(self) -> bool: + """Check if native QEMU ESP32 is available.""" + try: + # Import the native QEMU module to check + from ci.qemu_esp32 import find_qemu_binary # type: ignore[import-untyped] + + return find_qemu_binary() is not None + except ImportError: + return False + + def select_runner(self) -> str: + """Select the best available runner. + + Returns: + 'docker' or 'native' based on availability and preference + """ + if self.prefer_docker and self.docker_available: + return "docker" + elif self.native_qemu_available: + return "native" + elif self.docker_available: + return "docker" + else: + return "none" + + def run_qemu_test( + self, + firmware_path: Union[str, Path], + timeout: int = 30, + interrupt_regex: Optional[str] = None, + flash_size: int = 4, + force_runner: Optional[str] = None, + ) -> int: + """Run QEMU test with automatic runner selection. + + Args: + firmware_path: Path to firmware or build directory + timeout: Test timeout in seconds + interrupt_regex: Pattern to interrupt on success + flash_size: Flash size in MB + force_runner: Force specific runner ('docker' or 'native') + + Returns: + Exit code (0 for success) + """ + firmware_path = Path(firmware_path) + + # Determine which runner to use + if force_runner: + runner_type = force_runner + else: + runner_type = self.select_runner() + + print(f"Selected QEMU runner: {runner_type}") + + if runner_type == "docker": + return self._run_docker_qemu( + firmware_path, timeout, interrupt_regex, flash_size + ) + elif runner_type == "native": + return self._run_native_qemu( + firmware_path, timeout, interrupt_regex, flash_size + ) + else: + print("ERROR: No QEMU runner available!", file=sys.stderr) + print("Install Docker or native QEMU ESP32", file=sys.stderr) + return 1 + + def _run_docker_qemu( + self, + firmware_path: Path, + timeout: int, + interrupt_regex: Optional[str], + flash_size: int, + ) -> int: + """Run QEMU test using Docker.""" + print("Running QEMU test in Docker container...") + + runner = DockerQEMURunner() + return runner.run( + firmware_path=firmware_path, + timeout=timeout, + interrupt_regex=interrupt_regex, + flash_size=flash_size, + ) + + def _run_native_qemu( + self, + firmware_path: Path, + timeout: int, + interrupt_regex: Optional[str], + flash_size: int, + ) -> int: + """Run QEMU test using native installation.""" + print("Running QEMU test with native installation...") + + # Import and use the native runner + from ci.qemu_esp32 import QEMURunner # type: ignore[import-untyped] + + runner = QEMURunner() # type: ignore[no-untyped-call] + return runner.run( # type: ignore[no-untyped-call] + firmware_path=firmware_path, + timeout=timeout, + interrupt_regex=interrupt_regex, + flash_size=flash_size, + ) + + def install_qemu(self, use_docker: bool = False) -> bool: + """Install QEMU (native or pull Docker image). + + Args: + use_docker: If True, pull Docker image instead of native install + + Returns: + True if installation successful + """ + if use_docker: + print("Pulling Docker QEMU image...") + try: + runner = DockerQEMURunner() + runner.pull_image() + return True + except Exception as e: + print(f"Failed to pull Docker image: {e}", file=sys.stderr) + return False + else: + print("Installing native QEMU...") + try: + # Run the native install script + result = subprocess.run( + [sys.executable, "ci/install-qemu.py"], + cwd=Path(__file__).parent.parent.parent, + ) + return result.returncode == 0 + except Exception as e: + print(f"Failed to install native QEMU: {e}", file=sys.stderr) + return False + + +def integrate_with_test_framework(): + """Integrate Docker QEMU with the existing test framework. + + This function modifies the test.py to add Docker support. + """ + test_file = Path(__file__).parent.parent.parent / "test.py" + + if not test_file.exists(): + print(f"ERROR: test.py not found at {test_file}", file=sys.stderr) + return False + + print(f"Integrating Docker QEMU support into {test_file}") + + # Read the test.py file + content = test_file.read_text() + + # Check if already integrated + if "docker.qemu_test_integration" in content: + print("Docker QEMU support already integrated") + return True + + # Find the QEMU test section and add Docker support + integration_code = """ +# Docker QEMU integration +try: + from ci.dockerfiles.qemu_test_integration import QEMUTestIntegration + DOCKER_QEMU_AVAILABLE = True +except ImportError: + DOCKER_QEMU_AVAILABLE = False +""" + + # Add the import at the top of the file after other imports + import_marker = "from ci.qemu_esp32 import" + if import_marker in content: + # Add after the existing QEMU import + content = content.replace( + import_marker, integration_code + "\n" + import_marker + ) + + # Save the modified file + test_file.write_text(content) + print("Successfully integrated Docker QEMU support") + return True + else: + print("WARNING: Could not find appropriate location to add integration") + return False + + +def main(): + """Main function for testing the integration.""" + import argparse + + parser = argparse.ArgumentParser( + description="QEMU Test Integration with Docker support" + ) + parser.add_argument( + "command", + choices=["test", "install", "integrate", "check"], + help="Command to execute", + ) + parser.add_argument("--firmware", type=Path, help="Firmware path for test command") + parser.add_argument( + "--docker", action="store_true", help="Prefer Docker over native QEMU" + ) + parser.add_argument( + "--timeout", type=int, default=30, help="Test timeout in seconds" + ) + + args = parser.parse_args() + + integration = QEMUTestIntegration(prefer_docker=args.docker) + + if args.command == "check": + print(f"Docker available: {integration.docker_available}") + print(f"Native QEMU available: {integration.native_qemu_available}") + print(f"Selected runner: {integration.select_runner()}") + return 0 + + elif args.command == "install": + success = integration.install_qemu(use_docker=args.docker) + return 0 if success else 1 + + elif args.command == "integrate": + success = integrate_with_test_framework() + return 0 if success else 1 + + elif args.command == "test": + if not args.firmware: + print("ERROR: --firmware required for test command", file=sys.stderr) + return 1 + + return integration.run_qemu_test( + firmware_path=args.firmware, + timeout=args.timeout, + force_runner="docker" if args.docker else None, + ) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/docs.py b/v1.5/lib/FastLED/ci/docs.py new file mode 100644 index 0000000..bc4fbbd --- /dev/null +++ b/v1.5/lib/FastLED/ci/docs.py @@ -0,0 +1,186 @@ +""" +Work in progress to generate doxygen via a script instead of a GitHub action. +""" + +import os +import platform +import shutil +import subprocess +import warnings +from pathlib import Path +from typing import Optional, Tuple + +from download import download # type: ignore + + +# Configs +DOXYGEN_VERSION = ( + "1.13.2" # DOXYGEN_AWESOME styler is has certain restrictions with doxygen version +) +DOXYGEN_AWESOME_VERSION = "2.3.4" # deprecating +DOXYFILE_PATH = Path("docs/Doxyfile") +HTML_OUTPUT_DIR = Path("docs/html") +DOXYGEN_CSS_REPO = "https://github.com/jothepro/doxygen-awesome-css" # deprecating + + +HERE = Path(__file__).parent.resolve() +PROJECT_ROOT = HERE.parent + +DOCS_ROOT = PROJECT_ROOT / "docs" +DOCS_TOOL_PATH = PROJECT_ROOT / ".tools_cache" +DOCS_OUTPUT_PATH = DOCS_ROOT / "html" + + +def run( + cmd: str, + cwd: Optional[str] = None, + shell: bool = True, + check: bool = True, + capture: bool = True, +) -> str: + print(f"Running: {cmd}") + result = subprocess.run( + cmd, shell=shell, cwd=cwd, check=False, capture_output=capture, text=False + ) + if capture: + stdout = result.stdout.decode("utf-8") if result.stdout else "" + stderr = result.stderr.decode("utf-8") if result.stderr else "" + else: + stdout = "" + stderr = "" + if result.returncode != 0: + msg = f"Command failed with exit code {result.returncode}:\nstdout:\n{stdout}\n\nstderr:\n{stderr}" + warnings.warn(msg) + if check: + raise subprocess.CalledProcessError( + result.returncode, cmd, output=result.stdout + ) + return stdout.strip() + + +def get_git_info() -> Tuple[str, str]: + release_tag = os.environ.get("RELEASE_TAG", "") + + try: + latest_tag = run("git tag | grep -E '^[0-9]' | sort -V | tail -1") + latest_tag = latest_tag if latest_tag else "" + except subprocess.CalledProcessError: + latest_tag = "" + + git_sha_short = run("git rev-parse --short HEAD") + full_sha = run("git rev-parse HEAD") + project_number = release_tag or latest_tag or git_sha_short + commit_message = ( + f"{project_number} ({full_sha})" + if project_number != git_sha_short + else project_number + ) + + print(f"Project number: {project_number}") + print(f"Commit message: {commit_message}") + return project_number, commit_message + + +def install_doxygen_windows() -> Path: + print("Installing Doxygen...") + doxygen_url = ( + f"https://www.doxygen.nl/files/doxygen-{DOXYGEN_VERSION}.windows.x64.bin.zip" + ) + zip_path = DOCS_TOOL_PATH / f"doxygen-{DOXYGEN_VERSION}.zip" + extract_dir = DOCS_TOOL_PATH / f"doxygen-{DOXYGEN_VERSION}" + + # Create tool path if it doesn't exist + DOCS_TOOL_PATH.mkdir(exist_ok=True, parents=True) + + download(doxygen_url, zip_path) + shutil.unpack_archive(str(zip_path), extract_dir) + bin_path = next(extract_dir.glob("**/doxygen.exe"), None) + if not bin_path: + raise FileNotFoundError("Doxygen executable not found after extraction.") + print(f"Doxygen installed at: {bin_path}") + return bin_path + + +def install_doxygen_unix() -> Path: + print("Installing Doxygen...") + archive = f"doxygen-{DOXYGEN_VERSION}.linux.bin.tar.gz" + url = f"https://www.doxygen.nl/files/{archive}" + + # Create tool path if it doesn't exist + DOCS_TOOL_PATH.mkdir(exist_ok=True, parents=True) + + # Change to tool directory for download and extraction + original_dir = os.getcwd() + os.chdir(str(DOCS_TOOL_PATH)) + + try: + run(f"wget -q {url}") + run(f"tar -xf {archive}") + bin_dir = DOCS_TOOL_PATH / f"doxygen-{DOXYGEN_VERSION}" + return bin_dir / "bin" / "doxygen" + finally: + os.chdir(original_dir) + + +def install_theme() -> Path: + print("Installing Doxygen Awesome Theme...") + theme_path = DOCS_ROOT / "doxygen-awesome-css" + if theme_path.exists(): + return theme_path + run( + f"git clone --depth 1 -b v{DOXYGEN_AWESOME_VERSION} {DOXYGEN_CSS_REPO}", + cwd=str(DOCS_ROOT), + ) + return theme_path + + +def generate_docs(doxygen_bin: Path) -> None: + print("Generating documentation...") + cmd_str = f'"{doxygen_bin}" {DOXYFILE_PATH.name}' + run(cmd_str, cwd=str(DOCS_ROOT), capture=False) + + +# def install_graphviz() -> None: +# url: str = get_latest_release_for_platform() +# print(url) + + +def main() -> None: + is_windows = platform.system() == "Windows" + # is_macos = platform.system() == "Darwin" + _, commit_msg = get_git_info() + + if is_windows: + doxygen_bin = install_doxygen_windows() + # add to path C:\Program Files\Graphviz\bin\ + os.environ["PATH"] += os.pathsep + r"C:\Program Files\Graphviz\bin" + else: + doxygen_bin = install_doxygen_unix() + + # install_theme() + + # install_graphviz() # Work in progress + + # Verify Graphviz installation + try: + dot_version = run("dot -V", check=False) + print(f"Graphviz detected: {dot_version}") + except Exception: + warnings.warn( + "Graphviz (dot) not found in PATH. Diagrams may not be generated." + ) + + # Check it graphviz is installed + # if linux + if not is_windows: + run("dot -Tsvg -Kneato -Grankdir=LR", check=True) + + generate_docs(doxygen_bin=doxygen_bin) + + print(f"\n✅ Docs generated in: {HTML_OUTPUT_DIR}") + print(f"📄 Commit message: {commit_msg}") + print("✨ You can now manually deploy to GitHub Pages or automate this step.") + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/esp32_symbol_analysis_runner.py b/v1.5/lib/FastLED/ci/esp32_symbol_analysis_runner.py new file mode 100644 index 0000000..65713ce --- /dev/null +++ b/v1.5/lib/FastLED/ci/esp32_symbol_analysis_runner.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +ESP32 Symbol Analysis Runner for GitHub Actions +Wrapper script to run ESP32 symbol analysis with proper error handling +""" + +import argparse +import os +import sys +from pathlib import Path + +from ci.util.esp32_symbol_analysis import main as esp32_symbol_analysis + + +def is_esp32_board(board_name: str) -> bool: + """Check if the board is ESP32-based""" + esp32_boards = [ + "esp32dev", + "esp32", + "esp32s2", + "esp32s3", + "esp32c3", + "esp32c6", + "esp32h2", + "esp32p4", + "esp32c2", + ] + return any(esp32_board in board_name.lower() for esp32_board in esp32_boards) + + +def main(): + parser = argparse.ArgumentParser( + description="Run ESP32 symbol analysis for GitHub Actions" + ) + parser.add_argument("--board", required=True, help="Board name being analyzed") + parser.add_argument("--example", default="Blink", help="Example that was compiled") + parser.add_argument( + "--skip-on-failure", + action="store_true", + help="Don't fail the build if symbol analysis fails", + ) + args = parser.parse_args() + + print( + f"ESP32 Symbol Analysis Runner - Board: {args.board}, Example: {args.example}" + ) + + # Only run for ESP32 boards + if not is_esp32_board(args.board): + print(f"Skipping symbol analysis - {args.board} is not an ESP32-based board") + return 0 + + # Check if build_info.json exists + build_info_path = Path(".build") / args.board / "build_info.json" + if not build_info_path.exists(): + message = ( + f"Build info not found at {build_info_path}. Skipping symbol analysis." + ) + if args.skip_on_failure: + print(f"Warning: {message}") + return 0 + else: + print(f"Error: {message}") + return 1 + + try: + # TODO - Remove this. + # Change to the ci/util directory since the script expects to be run from there + original_cwd = os.getcwd() + ci_ci_dir = Path(__file__).parent / "ci" + os.chdir(ci_ci_dir) + + # Import and run the ESP32 symbol analysis + try: + print("Running ESP32 symbol analysis...") + esp32_symbol_analysis() + print("ESP32 symbol analysis completed successfully!") + return 0 + except ImportError as e: + print(f"Failed to import esp32_symbol_analysis module: {e}") + if args.skip_on_failure: + return 0 + return 1 + except Exception as e: + print(f"ESP32 symbol analysis failed: {e}") + if args.skip_on_failure: + return 0 + return 1 + finally: + os.chdir(original_cwd) + + except Exception as e: + print(f"Unexpected error in ESP32 symbol analysis runner: {e}") + if args.skip_on_failure: + return 0 + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/examples/fingerprint_cache_demo.py b/v1.5/lib/FastLED/ci/examples/fingerprint_cache_demo.py new file mode 100644 index 0000000..10fa1b5 --- /dev/null +++ b/v1.5/lib/FastLED/ci/examples/fingerprint_cache_demo.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +""" +Demonstration of the Fingerprint Cache Feature + +This script shows how to use the fingerprint cache for efficient file change detection +in build systems and development workflows. +""" + +import os +import tempfile +import time +from pathlib import Path +from typing import List + +from ci.ci.fingerprint_cache import FingerprintCache, has_changed + + +def demo_basic_usage() -> None: + """Demonstrate basic fingerprint cache usage.""" + print("=== Basic Fingerprint Cache Demo ===") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + cache_file = temp_path / "demo_cache.json" + cache = FingerprintCache(cache_file) + + # Create a source file + source_file = temp_path / "example.cpp" + with open(source_file, "w") as f: + f.write("#include \nint main() { return 0; }") + + baseline_time = time.time() - 3600 # 1 hour ago + current_modtime = os.path.getmtime(source_file) + + print(f"Source file: {source_file}") + print(f"Baseline time: {baseline_time}") + print(f"Current modtime: {current_modtime}") + + # First check - file is newer than baseline + changed = cache.has_changed(source_file, baseline_time) + print(f"First check (vs baseline): {changed} (expected: True)") + + # Second check - same modtime + changed = cache.has_changed(source_file, current_modtime) + print(f"Second check (same modtime): {changed} (expected: False)") + + # Touch file (change modtime but not content) + time.sleep(0.01) + source_file.touch() + + # Third check - modtime changed but content same + changed = cache.has_changed(source_file, current_modtime) + print( + f"Third check (touched file): {changed} (expected: False - content unchanged)" + ) + + # Actually modify content + time.sleep(0.01) + with open(source_file, "w") as f: + f.write( + '#include \nint main() { std::cout << "Hello"; return 0; }' + ) + + # Fourth check - content actually changed + changed = cache.has_changed(source_file, current_modtime) + print(f"Fourth check (content changed): {changed} (expected: True)") + + +def demo_build_system_workflow() -> None: + """Demonstrate build system integration workflow.""" + print("\n=== Build System Workflow Demo ===") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + cache_file = temp_path / "build_cache.json" + cache = FingerprintCache(cache_file) + + # Simulate multiple source files + source_files = [ + temp_path / "main.cpp", + temp_path / "utils.cpp", + temp_path / "config.h", + ] + + # Create source files + contents = [ + '#include "config.h"\n#include "utils.h"\nint main() { return 0; }', + '#include "utils.h"\nvoid utility_function() {}', + '#pragma once\n#define VERSION "1.0"', + ] + + for src_file, content in zip(source_files, contents): + with open(src_file, "w") as f: + f.write(content) + + print(f"Created {len(source_files)} source files") + + # Simulate first build + print("\n--- First Build (all files new) ---") + last_build_time = time.time() - 3600 # 1 hour ago + changed_files: List[str] = [] + + for src_file in source_files: + if cache.has_changed(src_file, last_build_time): + changed_files.append(src_file.name) + + print( + f"Files to rebuild: {changed_files} ({len(changed_files)}/{len(source_files)})" + ) + + # Update last build time + current_modtimes = [os.path.getmtime(f) for f in source_files] + + # Simulate second build (no changes) + print("\n--- Second Build (no changes) ---") + changed_files: List[str] = [] + + for src_file, modtime in zip(source_files, current_modtimes): + if cache.has_changed(src_file, modtime): + changed_files.append(src_file.name) + + print( + f"Files to rebuild: {changed_files} ({len(changed_files)}/{len(source_files)})" + ) + + # Modify one file + print("\n--- Third Build (one file modified) ---") + time.sleep(0.01) + with open(source_files[0], "a") as f: + f.write("\n// Added comment") + + changed_files: List[str] = [] + for src_file, modtime in zip(source_files, current_modtimes): + if cache.has_changed(src_file, modtime): + changed_files.append(src_file.name) + + print( + f"Files to rebuild: {changed_files} ({len(changed_files)}/{len(source_files)})" + ) + + +def demo_performance() -> None: + """Demonstrate cache performance characteristics.""" + print("\n=== Performance Demo ===") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + cache_file = temp_path / "perf_cache.json" + cache = FingerprintCache(cache_file) + + # Create multiple test files + num_files = 20 + test_files: List[Path] = [] + + for i in range(num_files): + test_file = temp_path / f"file_{i:03d}.cpp" + with open(test_file, "w") as f: + f.write( + f"// File {i}\n#include \nint func_{i}() {{ return {i}; }}" + ) + test_files.append(test_file) + + print(f"Created {num_files} test files") + + # Measure cache miss performance (first check) + start_time = time.time() + baseline_time = time.time() - 3600 + + for test_file in test_files: + cache.has_changed(test_file, baseline_time) + + miss_time = time.time() - start_time + print( + f"Cache miss time: {miss_time * 1000:.1f}ms ({miss_time * 1000 / num_files:.2f}ms per file)" + ) + + # Measure cache hit performance (second check with same modtime) + modtimes = [os.path.getmtime(f) for f in test_files] + + start_time = time.time() + for test_file, modtime in zip(test_files, modtimes): + cache.has_changed(test_file, modtime) + + hit_time = time.time() - start_time + print( + f"Cache hit time: {hit_time * 1000:.1f}ms ({hit_time * 1000 / num_files:.2f}ms per file)" + ) + print(f"Speedup: {miss_time / hit_time:.1f}x faster") + + # Show cache statistics + stats = cache.get_cache_stats() + print(f"Cache entries: {stats['total_entries']}") + print(f"Cache file size: {stats['cache_file_size_bytes']} bytes") + + +def demo_convenience_function() -> None: + """Demonstrate the convenience has_changed function.""" + print("\n=== Convenience Function Demo ===") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + cache_file = temp_path / "convenience_cache.json" + + # Create test file + test_file = temp_path / "test.h" + with open(test_file, "w") as f: + f.write("#pragma once\n#define API_VERSION 1") + + baseline_time = time.time() - 1800 # 30 minutes ago + + # Use convenience function + changed = has_changed(test_file, baseline_time, cache_file) + print(f"Convenience function result: {changed}") + print(f"Cache file created: {cache_file.exists()}") + + +if __name__ == "__main__": + print("Fingerprint Cache Feature Demonstration") + print("=" * 50) + + demo_basic_usage() + demo_build_system_workflow() + demo_performance() + demo_convenience_function() + + print("\n" + "=" * 50) + print("Demo completed successfully!") + print("\nKey Benefits:") + print("- Fast modification time checks avoid expensive MD5 computation") + print("- Content-based verification prevents false positives") + print("- Persistent cache survives across tool invocations") + print("- Significant speedup for incremental builds and testing") diff --git a/v1.5/lib/FastLED/ci/inspect_binary.py b/v1.5/lib/FastLED/ci/inspect_binary.py new file mode 100644 index 0000000..de309af --- /dev/null +++ b/v1.5/lib/FastLED/ci/inspect_binary.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any, Dict + +from ci.util.bin_2_elf import bin_to_elf +from ci.util.elf import dump_symbol_sizes +from ci.util.map_dump import map_dump + + +def cpp_filt(cpp_filt_path: Path, input_text: str) -> str: + """ + Demangle C++ symbols using c++filt. + + Args: + cpp_filt_path (Path): Path to c++filt executable. + input_text (str): Text to demangle. + + Returns: + str: Demangled text. + """ + if not cpp_filt_path.exists(): + raise FileNotFoundError(f"cppfilt not found at '{cpp_filt_path}'") + command = [str(cpp_filt_path), "-t", "-n"] + process = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + stdout, stderr = process.communicate(input=input_text) + if process.returncode != 0: + raise RuntimeError(f"Error running c++filt: {stderr}") + return stdout + + +def demangle_gnu_linkonce_symbols(cpp_filt_path: Path, map_text: str) -> str: + """ + Demangle .gnu.linkonce.t symbols in the map file. + + Args: + cpp_filt_path (Path): Path to c++filt executable. + map_text (str): Content of the map file. + + Returns: + str: Map file content with demangled symbols. + """ + # Extract all .gnu.linkonce.t symbols + pattern = r"\.gnu\\.linkonce\\.t\\.(.+?)\\s" + matches = re.findall(pattern, map_text) + + if not matches: + return map_text + + # Create a block of text with the extracted symbols + symbols_block = "\n".join(matches) + + # Demangle the symbols + demangled_block = cpp_filt(cpp_filt_path, symbols_block) + + # Create a dictionary of mangled to demangled symbols + demangled_dict = dict(zip(matches, demangled_block.strip().split("\n"))) + + # Replace the mangled symbols with demangled ones in the original text + for mangled, demangled in demangled_dict.items(): + map_text = map_text.replace( + f".gnu.linkonce.t.{mangled}", f".gnu.linkonce.t.{demangled}" + ) + + return map_text + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Convert a binary file to ELF using map file." + ) + parser.add_argument("--first", action="store_true", help="Inspect the first board") + parser.add_argument("--cwd", type=Path, help="Custom working directory") + + return parser.parse_args() + + +def load_build_info(build_info_path: Path) -> Dict[str, Any]: + """ + Load build information from a JSON file. + + Args: + build_info_path (Path): Path to the build_info.json file. + + Returns: + dict: Parsed JSON data. + """ + if not build_info_path.exists(): + raise FileNotFoundError(f"Build info JSON not found at '{build_info_path}'") + return json.loads(build_info_path.read_text()) + + +def main() -> int: + args = parse_args() + if args.cwd: + root_build_dir = args.cwd / ".build" + else: + root_build_dir = Path(".build") + + # Support nested PlatformIO structure: .build/pio/ + nested_pio_dir = root_build_dir / "pio" + if nested_pio_dir.is_dir(): + root_build_dir = nested_pio_dir + + board_dirs = [d for d in root_build_dir.iterdir() if d.is_dir()] + if not board_dirs: + print(f"No board directories found in {root_build_dir.absolute()}") + return 1 + + print("Available boards:") + for i, board_dir in enumerate(board_dirs): + print(f"[{i}]: {board_dir.name}") + + which = ( + 0 + if args.first + else int(input("Enter the number of the board you want to inspect: ")) + ) + board_dir = board_dirs[which] + build_info_json = board_dir / "build_info.json" + + build_info = load_build_info(build_info_json) + board = board_dir.name + board_info = build_info.get(board) or build_info[next(iter(build_info))] + + # Validate paths from build_info.json + elf_path = Path(board_info.get("prog_path", "")) + if not elf_path.exists(): + print( + f"Error: ELF path '{elf_path}' does not exist. Check the 'prog_path' in build_info.json." + ) + return 1 + + bin_file = elf_path.with_suffix(".bin") + if not bin_file.exists(): + # use .hex or .uf2 if .bin doesn't exist + bin_file = elf_path.with_suffix(".hex") + if not bin_file.exists(): + bin_file = elf_path.with_suffix(".uf2") + if not bin_file.exists(): + print(f"Error: Binary file not found for '{elf_path}'") + return 1 + cpp_filt_path = Path(board_info["aliases"]["c++filt"]) + ld_path = Path(board_info["aliases"]["ld"]) + as_path = Path(board_info["aliases"]["as"]) + nm_path = Path(board_info["aliases"]["nm"]) + objcopy_path = Path(board_info["aliases"]["objcopy"]) + nm_path = Path(board_info["aliases"]["nm"]) + map_file = board_dir / "firmware.map" + if not map_file.exists(): + # Search for the map file + map_file = bin_file.with_suffix(".map") + if not map_file.exists(): + possible_map_files = list(board_dir.glob("**/firmware.map")) + if possible_map_files: + map_file = possible_map_files[0] + else: + print("Error: firmware.map file not found") + return 1 + + try: + with TemporaryDirectory() as temp_dir: + temp_dir_path = Path(temp_dir) + output_elf = bin_to_elf( + bin_file, + map_file, + as_path, + ld_path, + objcopy_path, + temp_dir_path / "output.elf", + ) + out = dump_symbol_sizes(nm_path, cpp_filt_path, output_elf) + print(out) + except Exception as e: + print( + f"Error while converting binary to ELF, binary analysis will not work on this build: {e}" + ) + + map_dump(map_file) + + # Demangle .gnu.linkonce.t symbols and print map file + print("\n##################################################") + print("# Map file dump:") + print("##################################################\n") + map_text = map_file.read_text() + demangled_map_text = demangle_gnu_linkonce_symbols(cpp_filt_path, map_text) + print(demangled_map_text) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/inspect_elf.py b/v1.5/lib/FastLED/ci/inspect_elf.py new file mode 100644 index 0000000..062efa9 --- /dev/null +++ b/v1.5/lib/FastLED/ci/inspect_elf.py @@ -0,0 +1,78 @@ +import argparse +import json +from pathlib import Path + +from ci.util.elf import dump_symbol_sizes + + +HERE = Path(__file__).resolve().parent +PROJECT_ROOT = HERE.parent + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Inspect a compiled binary") + parser.add_argument("--first", action="store_true", help="Inspect the first board") + parser.add_argument("--cwd", type=str, help="Custom working directory") + parser.add_argument("--elf", type=str, help="Path to the ELF file to inspect") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if args.elf: + firmware_path = Path(args.elf) + if not firmware_path.exists(): + print(f"ELF file not found: {firmware_path}") + return 1 + + if args.cwd: + # os.chdir(args.cwd) + root_build_dir = Path(args.cwd) / ".build" + else: + root_build_dir = Path(".build") + + # Support nested PlatformIO structure: .build/pio/ + nested_pio_dir = root_build_dir / "pio" + if nested_pio_dir.is_dir(): + root_build_dir = nested_pio_dir + + # Find the first board directory + board_dirs = [d for d in root_build_dir.iterdir() if d.is_dir()] + if not board_dirs: + # print("No board directories found in .build") + print(f"No board directories found in {root_build_dir.absolute()}") + return 1 + + # display all the boards to the user and ask them to select which one they want by number + print("Available boards:") + for i, board_dir in enumerate(board_dirs): + print(f"[{i}]: {board_dir.name}") + + if args.first: + which = 0 + else: + which = int(input("Enter the number of the board you want to inspect: ")) + + board_dir = board_dirs[which] + board = board_dir.name + + build_info_json = board_dir / "build_info.json" + build_info = json.loads(build_info_json.read_text()) + board_info = build_info.get(board) or build_info[next(iter(build_info))] + + firmware_path = Path(board_info["prog_path"]) + cpp_filt_path = Path(board_info["aliases"]["c++filt"]) + + print(f"Dumping symbol sizes for {board} firmware: {firmware_path}") + try: + nm_path = Path(board_info["aliases"]["nm"]) + symbol_sizes = dump_symbol_sizes(nm_path, cpp_filt_path, firmware_path) + print(symbol_sizes) + except Exception as e: + print(f"Error while dumping symbol sizes: {e}") + + return 0 + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/inspect_obj.py b/v1.5/lib/FastLED/ci/inspect_obj.py new file mode 100644 index 0000000..e5cc4ae --- /dev/null +++ b/v1.5/lib/FastLED/ci/inspect_obj.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +import os +import subprocess +import sys +from pathlib import Path +from typing import List + +from ci.util.paths import BUILD +from ci.util.tools import load_tools + + +def _list_builds() -> List[Path]: + str_paths = os.listdir(BUILD) + paths = [BUILD / p for p in str_paths] + dirs = [p for p in paths if p.is_dir()] + return dirs + + +def _check_build(build: Path) -> bool: + # 1. should contain a build_info.json file + # 2. should contain a .pio/build directory + has_build_info = (build / "build_info.json").exists() + has_pio_build = (build / ".pio" / "build").exists() + return has_build_info and has_pio_build + + +def _prompt_build() -> Path: + builds = _list_builds() + if not builds: + print("Error: No builds found", file=sys.stderr) + sys.exit(1) + print("Select a build:") + for i, build in enumerate(builds): + print(f" [{i}]: {build}") + while True: + try: + which = int(input("Enter the number of the build to use: ")) + if 0 <= which < len(builds): + valid = _check_build(BUILD / builds[which]) + if valid: + return BUILD / builds[which] + print("Error: Invalid build", file=sys.stderr) + else: + print("Error: Invalid selection", file=sys.stderr) + continue + except ValueError: + print("Error: Invalid input", file=sys.stderr) + continue + + +def _prompt_object_file(build: Path) -> Path: + # Look for object files in .pio/build directory + build_dir = build / ".pio" / "build" + object_files: List[Path] = list(build_dir.rglob("*.o")) + + if not object_files: + print("Error: No object files found", file=sys.stderr) + sys.exit(1) + + print("\nSelect an object file:") + for i, obj_file in enumerate(object_files): + print(f" [{i}]: {obj_file.relative_to(build_dir)}") + + while True: + try: + which = int(input("Enter the number of the object file to use: ")) + if 0 <= which < len(object_files): + return object_files[which] + print("Error: Invalid selection", file=sys.stderr) + except ValueError: + print("Error: Invalid input", file=sys.stderr) + continue + + +def cli() -> None: + import argparse + + parser = argparse.ArgumentParser( + description="Dump object file information using build tools" + ) + + parser.add_argument( + "build_path", + type=Path, + nargs="?", + help="Path to build directory containing build info JSON file", + ) + + args = parser.parse_args() + build_path = args.build_path + + # Check if object file was provided and exists + if build_path is None: + build_path = _prompt_build() + else: + if not _check_build(build_path): + print("Error: Invalid build directory", file=sys.stderr) + sys.exit(1) + + assert build_path is not None + assert build_path + + build_info_path = build_path / "build_info.json" + assert build_info_path.exists(), f"File not found: {build_info_path}" + + tools = load_tools(build_info_path) + + object_file = _prompt_object_file(build_path) + + cmd = [str(tools.objdump_path), "--syms", str(object_file)] + if sys.platform == "win32": + cmd = ["cmd", "/c"] + cmd + cmd_str = subprocess.list2cmdline(cmd) + subprocess.run(cmd, check=True) + print("\nDone. Command used:", cmd_str) + + +if __name__ == "__main__": + try: + cli() + except KeyboardInterrupt: + print("Exiting...") + sys.exit(1) diff --git a/v1.5/lib/FastLED/ci/install-qemu.py b/v1.5/lib/FastLED/ci/install-qemu.py new file mode 100644 index 0000000..2049906 --- /dev/null +++ b/v1.5/lib/FastLED/ci/install-qemu.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +""" +Standalone QEMU Installation Script + +This is a convenience wrapper around install_qemu_esp32.py +that can be called directly to install QEMU for ESP32 emulation. + +Usage: + uv run ci/install-qemu.py +""" + +import os +import sys + +from install_qemu_esp32 import main as install_qemu_main + + +def main(): + """Run the QEMU installation script.""" + print("=== FastLED QEMU Installation ===") + print("Starting QEMU installation process...") + print(f"Python executable: {sys.executable}") + print(f"Working directory: {os.getcwd()}") + print() + + try: + # Call the main function directly + print("Calling install_qemu_esp32.main()...") + install_qemu_main() + print("install_qemu_esp32.main() completed successfully") + + except KeyboardInterrupt: + print("\nInstallation cancelled by user") + sys.exit(130) + except SystemExit as e: + print(f"SystemExit caught with code: {e.code}") + # Re-raise SystemExit to preserve exit codes + raise + except Exception as e: + print(f"ERROR: Installation failed: {e}") + import traceback + + print("Full traceback:") + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/install_qemu_esp32.py b/v1.5/lib/FastLED/ci/install_qemu_esp32.py new file mode 100644 index 0000000..28a6293 --- /dev/null +++ b/v1.5/lib/FastLED/ci/install_qemu_esp32.py @@ -0,0 +1,732 @@ +#!/usr/bin/env python3 +""" +ESP32 QEMU Installation Script + +Installs ESP32-compatible QEMU emulator using ESP-IDF tools. +Cross-platform support for Linux, macOS, and Windows. + +WARNING: Never use sys.stdout.flush() in this file! +It causes blocking issues on Windows that hang QEMU processes. +Python's default buffering behavior works correctly across platforms. +""" + +import hashlib +import os +import platform +import shutil +import subprocess +import sys +import tarfile +import tempfile +import threading +from pathlib import Path +from typing import Dict, List, Optional, Union + +from download import download # type: ignore + +from ci.util.running_process import RunningProcess + + +def get_platform_info(): + """Get platform-specific information for QEMU installation.""" + print("IMMEDIATE DEBUG: get_platform_info() called") + print("Detecting platform information...") + system = platform.system().lower() + print(f"IMMEDIATE DEBUG: platform.system() = {system}") + machine = platform.machine().lower() + print(f"IMMEDIATE DEBUG: platform.machine() = {machine}") + print(f"System: {system}, Machine: {machine}") + + # Normalize machine architecture + if machine in ["x86_64", "amd64"]: + arch = "x86_64" + elif machine in ["aarch64", "arm64"]: + arch = "arm64" + elif machine.startswith("arm"): + arch = "arm" + else: + arch = machine + + return system, arch + + +def install_system_dependencies(): + """Install system dependencies required for QEMU.""" + system, _ = get_platform_info() + + print("Installing system dependencies...") + + if system == "linux": + # Ubuntu/Debian dependencies + deps = [ + "libgcrypt20", + "libglib2.0-0", + "libpixman-1-0", + "libsdl2-2.0-0", + "libslirp0", + ] + + try: + # Check if we can install packages (requires sudo) + print("Updating package list...") + process = RunningProcess(["sudo", "apt-get", "update"]) + result = process.wait(echo=True) + if result == 0: + print("Installing system dependencies:", " ".join(deps)) + process = RunningProcess(["sudo", "apt-get", "install", "-y"] + deps) + result = process.wait(echo=True) + if result != 0: + raise subprocess.CalledProcessError( + result if result is not None else 1, + "apt-get install", + ) + print("System dependencies installed") + else: + print("WARNING: Could not install system dependencies (sudo required)") + print("Please install manually:", " ".join(deps)) + except subprocess.CalledProcessError: + print("WARNING: Failed to install system dependencies") + + elif system == "darwin": + # macOS - dependencies usually available via system + print("macOS system dependencies should be available") + + elif system == "windows": + # Windows - QEMU binaries are self-contained + print("Windows - no additional dependencies required") + + +def download_esp_idf_tools(): + """Download and setup ESP-IDF tools for QEMU installation.""" + tools_dir = Path.home() / ".espressif" + tools_dir.mkdir(exist_ok=True) + + # ESP-IDF tools script URL + tools_url = ( + "https://raw.githubusercontent.com/espressif/esp-idf/master/tools/idf_tools.py" + ) + tools_script = tools_dir / "idf_tools.py" + + if not tools_script.exists(): + print("Downloading ESP-IDF tools...") + print(f"Source: {tools_url}") + print(f"Destination: {tools_script}") + download(tools_url, tools_script) + print(f"Downloaded ESP-IDF tools to {tools_script}") + else: + print(f"ESP-IDF tools already exist at {tools_script}") + + return tools_script + + +def check_command_available(command: str) -> bool: + """Check if a command is available in PATH.""" + print(f"Checking if {command} is available...") + try: + result = subprocess.run( + [command, "--version"], capture_output=True, text=True, timeout=10 + ) + available = result.returncode == 0 + print(f"{command} {'found' if available else 'not found'}") + return available + except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): + print(f"{command} not found or timed out") + return False + + +def verify_file_hash(file_path: Path, expected_hash: str) -> bool: + """Verify SHA256 hash of a downloaded file.""" + print(f"Verifying SHA256 hash of {file_path}...") + try: + with open(file_path, "rb") as f: + file_hash = hashlib.sha256(f.read()).hexdigest() + + if file_hash == expected_hash: + print(f"SHA256 verification passed: {file_hash}") + return True + else: + print(f"SHA256 verification failed!") + print(f"Expected: {expected_hash}") + print(f"Got: {file_hash}") + return False + except Exception as e: + print(f"Error verifying hash: {e}") + return False + + +def try_espressif_binary_install(): + """Download and install QEMU using Espressif official precompiled binaries.""" + try: + print("\n--- Espressif Official Binary Installation ---") + print("Downloading QEMU from Espressif official releases...") + + # Create local QEMU directory (project-local) + qemu_dir = Path(".cache") / "qemu" + qemu_dir.mkdir(parents=True, exist_ok=True) + + # Check if already installed + qemu_xtensa_binary = qemu_dir / "qemu-system-xtensa.exe" + qemu_riscv32_binary = qemu_dir / "qemu-system-riscv32.exe" + + print(f"Checking for existing QEMU at {qemu_xtensa_binary}...") + if qemu_xtensa_binary.exists() and qemu_riscv32_binary.exists(): + print( + f"QEMU already installed at {qemu_xtensa_binary} and {qemu_riscv32_binary}" + ) + return True + else: + print("QEMU not found, proceeding with download...") + + # Espressif official QEMU binaries for Windows x64 + binaries = [ + { + "name": "QEMU-Xtensa (ESP32/ESP32-S2/ESP32-S3)", + "url": "https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/qemu-xtensa-softmmu-esp_develop_9.2.2_20250817-x86_64-w64-mingw32.tar.xz", + "sha256": "ef550b912726997f3c1ff4a4fb13c1569e2b692efdc5c9f9c3c926a8f7c540fa", + "binary_name": "qemu-system-xtensa.exe", + }, + { + "name": "QEMU-RISCV32 (ESP32-C3/ESP32-H2/ESP32-C2)", + "url": "https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/qemu-riscv32-softmmu-esp_develop_9.2.2_20250817-x86_64-w64-mingw32.tar.xz", + "sha256": "9474015f24d27acb7516955ec932e5307226bd9d6652cdc870793ed36010ab73", + "binary_name": "qemu-system-riscv32.exe", + }, + ] + + for binary_info in binaries: + print(f"\nDownloading {binary_info['name']}...") + print("This may take a few minutes depending on your internet connection.") + + archive_path = qemu_dir / f"{binary_info['binary_name']}.tar.xz" + + print(f"Downloading from: {binary_info['url']}") + print(f"Saving to: {archive_path}") + + try: + # Download the archive + download(binary_info["url"], archive_path) + print(f"Download completed: {archive_path}") + + # Verify SHA256 hash + if not verify_file_hash(archive_path, binary_info["sha256"]): + print(f"Hash verification failed for {binary_info['name']}") + archive_path.unlink() # Remove corrupted file + return False + + # Extract the archive + print(f"Extracting {binary_info['name']}...") + with tarfile.open(archive_path, "r:xz") as tar: + # List contents to understand structure + members = tar.getnames() + print(f"Archive contains {len(members)} files") + + # Extract to temporary directory first + temp_extract_dir = qemu_dir / "temp_extract" + temp_extract_dir.mkdir(exist_ok=True) + + tar.extractall(temp_extract_dir) + print(f"Extracted to temporary directory: {temp_extract_dir}") + + # Find the binary in the extracted contents + found_binary = None + for root, dirs, files in os.walk(temp_extract_dir): + for file in files: + if file == binary_info["binary_name"]: + found_binary = Path(root) / file + break + if found_binary: + break + + if found_binary: + target_binary = qemu_dir / binary_info["binary_name"] + print( + f"Copying {binary_info['binary_name']} to {target_binary}" + ) + shutil.copy2(found_binary, target_binary) + + # Make sure it's executable (though Windows doesn't use execute bit) + target_binary.chmod(0o755) + + print(f"Successfully installed {binary_info['binary_name']}") + else: + print( + f"ERROR: Could not find {binary_info['binary_name']} in extracted archive" + ) + return False + + # Clean up temporary extraction directory + shutil.rmtree(temp_extract_dir) + + # Clean up downloaded archive + archive_path.unlink() + print(f"Cleaned up {archive_path}") + + except Exception as e: + print(f"Error downloading/extracting {binary_info['name']}: {e}") + # Clean up on error + if archive_path.exists(): + archive_path.unlink() + return False + + print("Espressif binary installation completed successfully") + return True + + except Exception as e: + print(f"Espressif binary installation error: {e}") + return False + + +def try_direct_download_install(): + """Download and install QEMU directly from the official source.""" + print("\n--- Legacy Installer Method (Requires Admin) ---") + print("WARNING: This method requires administrator privileges and may fail") + print("Consider using the Espressif binary method instead") + + try: + print("Downloading QEMU directly from official source...") + + # Create local QEMU directory (project-local) + qemu_dir = Path(".cache") / "qemu" + qemu_dir.mkdir(parents=True, exist_ok=True) + + # Check if already installed + qemu_binary = qemu_dir / "qemu-system-xtensa.exe" + print(f"Checking for existing QEMU at {qemu_binary}...") + if qemu_binary.exists(): + print(f"QEMU already installed at {qemu_binary}") + return True + else: + print("QEMU not found, proceeding with download...") + + # Download QEMU from official source + print("Downloading QEMU for Windows...") + print("This may take several minutes depending on your internet connection.") + + # QEMU download URL from the official Windows builds + qemu_url = "https://qemu.weilnetz.de/w64/2024/qemu-w64-setup-20241220.exe" + installer_path = qemu_dir / "qemu-installer.exe" + + print(f"Downloading from: {qemu_url}") + print(f"Saving to: {installer_path}") + + try: + # Download the installer + download(qemu_url, installer_path) + print(f"Download completed: {installer_path}") + + # Run the installer in silent mode + print("Running QEMU installer in silent mode...") + print("Note: This may require administrator privileges") + + # Try different installation approaches + install_attempts = [ + # Attempt 1: Install to custom directory (may require admin) + [ + str(installer_path), + "/VERYSILENT", # Very silent install + "/SUPPRESSMSGBOXES", # Suppress message boxes + f"/DIR={qemu_dir.absolute()}", # Install directory + ], + # Attempt 2: Install to default location (may require admin) + [ + str(installer_path), + "/VERYSILENT", # Very silent install + "/SUPPRESSMSGBOXES", # Suppress message boxes + ], + # Attempt 3: Use basic silent install + [ + str(installer_path), + "/S", # Silent install + ], + ] + + result = None + process = None + for i, install_cmd in enumerate(install_attempts, 1): + print( + f"Installation attempt {i}/{len(install_attempts)}: {' '.join(install_cmd)}" + ) + try: + process = RunningProcess( + install_cmd, + timeout=300, # 5 minute timeout + ) + result = process.wait(echo=True) + + if result == 0: + print(f"Installation attempt {i} succeeded") + break + else: + print( + f"Installation attempt {i} failed with exit code: {result}" + ) + if i < len(install_attempts): + print("Trying next installation method...") + except Exception as e: + print(f"Installation attempt {i} failed with error: {e}") + if i < len(install_attempts): + print("Trying next installation method...") + + if result == 0: + print("QEMU installer completed successfully") + + # Look for the installed qemu-system-xtensa.exe + for qemu_path in qemu_dir.rglob("qemu-system-xtensa.exe"): + if qemu_path.is_file(): + print(f"Found QEMU binary at: {qemu_path}") + + # If it's not in the expected location, copy it there + if qemu_path != qemu_binary: + print( + f"Copying QEMU binary to standard location: {qemu_binary}" + ) + shutil.copy2(qemu_path, qemu_binary) + + # Clean up installer + if installer_path.exists(): + installer_path.unlink() + + print("Direct download installation completed successfully") + return True + + print("QEMU installer completed but qemu-system-xtensa.exe not found") + else: + print(f"QEMU installer failed with exit code: {result}") + if process: + stderr_output = process.stdout + if stderr_output: + print(f"Error output: {stderr_output[:200]}") + + except Exception as download_error: + print(f"Download/install error: {download_error}") + + return False + + except Exception as e: + print(f"Direct download installation error: {e}") + return False + + +def try_windows_installation(): + """Try installing QEMU using Espressif binaries for Windows.""" + print("Attempting automatic QEMU installation on Windows...") + + # Try Espressif official binaries first (recommended method) + print("Using Espressif official binaries...") + if try_espressif_binary_install(): + print("Espressif binary installation completed, verifying...") + # Verify installation worked + if find_qemu_binary(): + print("SUCCESS: QEMU installed via Espressif official binaries") + return True + else: + print( + "WARNING: Espressif binary installation reported success but QEMU binary not found" + ) + else: + print("Espressif binary installation failed") + + # Fall back to legacy installer method (requires admin privileges) + print("Falling back to legacy installer method...") + if try_direct_download_install(): + print("Legacy installer completed, verifying...") + # Verify installation worked + if find_qemu_binary(): + print("SUCCESS: QEMU installed via legacy installer") + return True + else: + print( + "WARNING: Legacy installer reported success but QEMU binary not found" + ) + else: + print("Legacy installer failed") + + print("Windows QEMU installation failed") + return False + + +def install_qemu_esp32(): + """Install ESP32 QEMU using ESP-IDF tools.""" + print("\n=== Starting ESP32 QEMU Installation ===") + system, arch = get_platform_info() + + print(f"\nChecking for ESP32 QEMU for {system}-{arch}...") + + # Check if already installed + print("Searching for existing QEMU installation...") + qemu_binary = find_qemu_binary() + print(f"IMMEDIATE DEBUG: find_qemu_binary returned: {qemu_binary}") + if qemu_binary: + print("ESP32 QEMU already installed") + return True + else: + print("No existing QEMU installation found") + + # Try automated installation + print("Attempting automatic installation...") + + if system == "linux": + # Check if we're in CI first for sudo operations + if os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true": + try: + # Try to install QEMU from package manager in CI + print("Installing QEMU via apt-get...") + print("Updating package list...") + process = RunningProcess(["sudo", "apt-get", "update"]) + result = process.wait(echo=True) + + if result == 0: + print("Package list updated successfully") + print("Installing QEMU packages...") + print("Installing: qemu-system-misc qemu-system-xtensa") + process = RunningProcess( + [ + "sudo", + "apt-get", + "install", + "-y", + "qemu-system-misc", + "qemu-system-xtensa", + ] + ) + result = process.wait(echo=True) + + if result == 0: + print("QEMU packages installed successfully via apt-get") + return True + else: + print("Failed to install QEMU packages via apt-get") + print("Return code:", result) + + except Exception as e: + print(f"Automatic installation failed: {e}") + else: + print( + "Local Linux environment - automatic installation requires sudo privileges" + ) + + elif system == "windows": + print("Windows platform detected, using direct download...") + # Try direct download installation (works in both CI and local environments) + if try_windows_installation(): + print("Windows QEMU installation successful") + return True + else: + print("Windows installation failed") + + elif system == "darwin": + # Try macOS installation methods + print("macOS platform detected") + print("Attempting macOS installation...") + print("Note: Homebrew installation could be added here in the future") + # Could add homebrew installation here in the future + + # If not found, provide installation instructions + print("ESP32 QEMU not found. Please install using one of these methods:") + print() + + if system == "windows": + print("Windows Installation Options:") + print() + print("Option 1: Espressif Official Binaries (Recommended)") + print( + " This is what the automatic installer uses - no administrator privileges required" + ) + print( + " Espressif QEMU Xtensa: https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/qemu-xtensa-softmmu-esp_develop_9.2.2_20250817-x86_64-w64-mingw32.tar.xz" + ) + print( + " Espressif QEMU RISC-V32: https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/qemu-riscv32-softmmu-esp_develop_9.2.2_20250817-x86_64-w64-mingw32.tar.xz" + ) + print(" Extract to .cache/qemu/ directory") + print() + print("Option 2: Official QEMU Installer (Requires Administrator)") + print( + " Download QEMU from: https://qemu.weilnetz.de/w64/2024/qemu-w64-setup-20241220.exe" + ) + print(" Run the installer and ensure qemu-system-xtensa.exe is in your PATH") + print() + print("Option 3: ESP-IDF (includes QEMU)") + print( + " Download ESP-IDF from: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/windows-setup.html" + ) + print() + print("Option 4: Manual Installation") + print(" Download QEMU from: https://www.qemu.org/download/") + print(" Make sure qemu-system-xtensa.exe is in your PATH") + + elif system == "linux": + print("Option 1: Install via ESP-IDF") + print(" git clone --recursive https://github.com/espressif/esp-idf.git") + print(" cd esp-idf && ./install.sh esp32s3") + print(" source ./export.sh") + print() + print("Option 2: Install QEMU from package manager") + print(" sudo apt-get install qemu-system-misc qemu-system-xtensa") + + elif system == "darwin": + print("Option 1: Install via ESP-IDF") + print(" git clone --recursive https://github.com/espressif/esp-idf.git") + print(" cd esp-idf && ./install.sh esp32s3") + print(" source ./export.sh") + print() + print("Option 2: Install via Homebrew") + print(" brew install qemu") + + print() + print("For CI/GitHub Actions, QEMU will be cached automatically once installed.") + return False + + +def find_qemu_binary() -> Optional[Path]: + """Find the installed QEMU binary.""" + print("IMMEDIATE DEBUG: find_qemu_binary() called") + system, _ = get_platform_info() + print(f"IMMEDIATE DEBUG: got system: {system}") + + # Look for both Xtensa and RISC-V32 binaries + binary_names = [] + if system == "windows": + binary_names = ["qemu-system-xtensa.exe", "qemu-system-riscv32.exe"] + else: + binary_names = ["qemu-system-xtensa", "qemu-system-riscv32"] + + print(f"IMMEDIATE DEBUG: looking for binaries: {binary_names}") + + # ESP-IDF installation paths and portable installation + esp_paths = [ + Path(".cache") / "qemu", # Project-local portable installation directory + Path.home() + / ".cache" + / "qemu", # Legacy user cache location (for backward compatibility) + Path.home() / ".espressif" / "tools" / "qemu-xtensa", + Path.home() / ".espressif" / "python_env", + Path("/opt/espressif/tools/qemu-xtensa"), + ] + + # Search in ESP-IDF paths first + for base_path in esp_paths: + print(f"Searching in: {base_path}") + try: + if base_path.exists(): + print(f" Directory exists, searching for QEMU binaries...") + for binary_name in binary_names: + print( + f" Starting recursive search for {binary_name} in {base_path}..." + ) + for qemu_path in base_path.rglob(binary_name): + print(f" Checking: {qemu_path}") + if qemu_path.is_file(): + print(f"Found QEMU binary at: {qemu_path}") + return qemu_path + print(f" No QEMU binaries found in {base_path}") + else: + print(f" Directory does not exist") + except Exception as e: + print(f" Error searching {base_path}: {e}") + + # Check system PATH for each binary + print("Checking system PATH...") + for binary_name in binary_names: + try: + cmd = ["where" if system == "windows" else "which", binary_name] + print(f"Running: {' '.join(cmd)}") + print("About to call subprocess.run...") + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + print("subprocess.run completed") + if result.returncode == 0: + # Take first line in case of multiple results (Windows) + first_line = result.stdout.strip().split("\n")[0] + qemu_path = Path(first_line) + print(f"Found QEMU binary in PATH: {qemu_path}") + return qemu_path + else: + print(f"Command returned code {result.returncode}") + except subprocess.TimeoutExpired: + print(f"PATH check for {binary_name} timed out after 10 seconds") + except (subprocess.SubprocessError, FileNotFoundError) as e: + print(f"Error checking PATH for {binary_name}: {e}") + + print("ERROR: Could not find any QEMU binary") + return None + + +def create_qemu_wrapper(): + """Create a wrapper script for easy QEMU access.""" + # DISABLED: This function was overwriting the actual qemu-esp32.py runner script + # The qemu-esp32.py script already exists and handles all the necessary + # functionality for running ESP32 firmware in QEMU with proper argument parsing + print("Skipping wrapper creation - using existing qemu-esp32.py runner script") + return True + + +def main(): + """Main installation routine.""" + print("IMMEDIATE DEBUG: main() function called") + print("=== ESP32 QEMU Installation ===") + print("Step 1/5: Starting installation process...") + + # Show platform info + print("Step 2/5: Detecting platform...") + system, arch = get_platform_info() + print(f"IMMEDIATE DEBUG: get_platform_info completed, got: {system}, {arch}") + print(f"Platform: {system}-{arch}") + + # Install system dependencies + print("Step 3/5: Installing system dependencies...") + install_system_dependencies() + print("IMMEDIATE DEBUG: install_system_dependencies completed") + print("System dependencies step completed") + + # Install ESP32 QEMU + print("Step 4/5: Installing ESP32 QEMU...") + qemu_installed = install_qemu_esp32() + print( + f"IMMEDIATE DEBUG: install_qemu_esp32 completed with result: {qemu_installed}" + ) + print(f"ESP32 QEMU installation step completed (success: {qemu_installed})") + + if qemu_installed: + # Create wrapper script + print("Step 5/5: Creating wrapper scripts...") + if not create_qemu_wrapper(): + print("WARNING: Could not create wrapper script, but QEMU is available") + else: + print("QEMU installation required for ESP32 emulation") + sys.exit(1) + + print("=== Installation Complete ===") + print("ESP32 QEMU is ready to use!") + + # Test installation + print("\n=== Verifying Installation ===") + print("Searching for QEMU binary...") + qemu_binary = find_qemu_binary() + if qemu_binary: + print(f"Testing QEMU binary: {qemu_binary}") + try: + print("Running version check...") + result = subprocess.run( + [str(qemu_binary), "--version"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode == 0: + print(f"QEMU version: {result.stdout.strip()}") + print("Installation verification successful!") + sys.exit(0) # Explicit success exit + else: + print("WARNING: QEMU installed but version check failed") + print(f"Version check return code: {result.returncode}") + sys.exit(1) + except Exception as e: + print(f"WARNING: Could not verify QEMU installation: {e}") + sys.exit(1) + else: + print("ERROR: QEMU installation verification failed") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/iwyu/fastled.imp b/v1.5/lib/FastLED/ci/iwyu/fastled.imp new file mode 100644 index 0000000..4dc3cb0 --- /dev/null +++ b/v1.5/lib/FastLED/ci/iwyu/fastled.imp @@ -0,0 +1,57 @@ +# FastLED Include What You Use Mapping File +# This file tells IWYU about FastLED's specific include patterns and structure + +[ + # FastLED main headers - these are the public API + { include: ['"FastLED.h"', 'public', '"FastLED.h"', 'public'] }, + { include: ['"fastled.h"', 'public', '"FastLED.h"', 'public'] }, + + # Core FastLED headers that should be included directly + { include: ['"lib8tion.h"', 'public', '"lib8tion.h"', 'public'] }, + { include: ['"pixeltypes.h"', 'public', '"pixeltypes.h"', 'public'] }, + { include: ['"hsv2rgb.h"', 'public', '"hsv2rgb.h"', 'public'] }, + { include: ['"colorutils.h"', 'public', '"colorutils.h"', 'public'] }, + { include: ['"colorpalettes.h"', 'public', '"colorpalettes.h"', 'public'] }, + + # Platform-specific headers should not be suggested directly + { include: ['"platforms/*/fastled_*.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"platforms/*/clockless_*.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"platforms/*/*"', 'private', '"FastLED.h"', 'public'] }, + + # Internal headers should redirect to public API + { include: ['"controller.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"led_sysdefs.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"fastled_config.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"fastled_delay.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"fastled_progmem.h"', 'private', '"FastLED.h"', 'public'] }, + + # fl/ namespace headers - the modern FastLED API + { include: ['"fl/namespace.h"', 'public', '"fl/namespace.h"', 'public'] }, + { include: ['"fl/ptr.h"', 'public', '"fl/ptr.h"', 'public'] }, + { include: ['"fl/vector.h"', 'public', '"fl/vector.h"', 'public'] }, + { include: ['"fl/allocator.h"', 'public', '"fl/allocator.h"', 'public'] }, + { include: ['"fl/math.h"', 'public', '"fl/math.h"', 'public'] }, + { include: ['"fl/force_inline.h"', 'public', '"fl/force_inline.h"', 'public'] }, + + # FX headers - effects system + { include: ['"fx/fx.h"', 'public', '"fx/fx.h"', 'public'] }, + { include: ['"fx/fx_engine.h"', 'public', '"fx/fx_engine.h"', 'public'] }, + { include: ['"fx/frame.h"', 'public', '"fx/frame.h"', 'public'] }, + { include: ['"fx/detail/*"', 'private', '"fx/fx.h"', 'public'] }, + + # Third-party headers should not be suggested for user code + { include: ['"third_party/*/*"', 'private', '"FastLED.h"', 'public'] }, + + # Arduino-specific headers + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + + # Chipset-specific headers redirect to main header + { include: ['"chipsets.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"ws2812_controller.h"', 'private', '"FastLED.h"', 'public'] }, + { include: ['"apa102_controller.h"', 'private', '"FastLED.h"', 'public'] }, + + # Testing headers - keep as-is for test files + { include: ['"test_*.h"', 'public', '"test_*.h"', 'public'] }, + { include: ['"doctest.h"', 'public', '"doctest.h"', 'public'] }, +] \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/iwyu/stdlib.imp b/v1.5/lib/FastLED/ci/iwyu/stdlib.imp new file mode 100644 index 0000000..27ae08d --- /dev/null +++ b/v1.5/lib/FastLED/ci/iwyu/stdlib.imp @@ -0,0 +1,67 @@ +# Standard Library Include What You Use Mapping File +# This file provides standard library header mappings for better IWYU suggestions + +[ + # C++ standard library headers + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + + # C++ containers and algorithms + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + + # I/O headers + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + + # Common type mappings + { symbol: ['std::uint8_t', 'public', '', 'public'] }, + { symbol: ['std::uint16_t', 'public', '', 'public'] }, + { symbol: ['std::uint32_t', 'public', '', 'public'] }, + { symbol: ['std::uint64_t', 'public', '', 'public'] }, + { symbol: ['std::int8_t', 'public', '', 'public'] }, + { symbol: ['std::int16_t', 'public', '', 'public'] }, + { symbol: ['std::int32_t', 'public', '', 'public'] }, + { symbol: ['std::int64_t', 'public', '', 'public'] }, + { symbol: ['std::size_t', 'public', '', 'public'] }, + { symbol: ['std::ptrdiff_t', 'public', '', 'public'] }, + + # Common functions + { symbol: ['std::memcpy', 'public', '', 'public'] }, + { symbol: ['std::memset', 'public', '', 'public'] }, + { symbol: ['std::strlen', 'public', '', 'public'] }, + { symbol: ['std::malloc', 'public', '', 'public'] }, + { symbol: ['std::free', 'public', '', 'public'] }, + { symbol: ['std::abs', 'public', '', 'public'] }, + { symbol: ['std::sin', 'public', '', 'public'] }, + { symbol: ['std::cos', 'public', '', 'public'] }, + { symbol: ['std::sqrt', 'public', '', 'public'] }, + + # C standard library compatibility (prefer C++ versions) + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, + + # Platform-specific but common headers + { include: ['', 'public', '', 'public'] }, + { include: ['', 'public', '', 'public'] }, +] \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/js/README.md b/v1.5/lib/FastLED/ci/js/README.md new file mode 100644 index 0000000..f5cda3d --- /dev/null +++ b/v1.5/lib/FastLED/ci/js/README.md @@ -0,0 +1,56 @@ +# FastLED JavaScript Linting Tools + +This directory contains JavaScript linting tools for the FastLED project, using Node.js + ESLint for **fast** JavaScript validation. + + +### Via Main Lint Script (Recommended) + +```bash +# JavaScript linting is included by default (fast only!) +bash lint +``` + +## Setup + +The fast JavaScript linter is automatically installed during project setup: + +```bash +# Installs everything including fast JS linting +./install +``` + +Or manually install just the JavaScript linter: + +```bash +uv run ci/setup-js-linting-fast.py +``` + +This will: +- Download Node.js binary to `.js-tools/node/` +- Install ESLint to `.js-tools/node_modules/` +- Create ESLint configuration in `.js-tools/.eslintrc.js` +- Create fast linting script `.cache/js-tools/lint-js-fast` + +## Philosophy: Fast Only + +**JavaScript linting follows a "fast only" policy:** +- ✅ **Fast linting available**: Uses Node.js + ESLint (~0.9 seconds) +- ⚠️ **Fast linting unavailable**: Skips JavaScript linting entirely +- ❌ **No slow fallback**: Fast linting or no linting at all + +## Configuration + +The fast linter only checks for **critical runtime issues**: +- `no-debugger` - Prevents debugger statements in production +- `no-eval` - Prevents eval() security vulnerabilities + +**No style enforcement** - focuses on critical issues only for maximum speed. + +## Integration + +JavaScript linting is included by default in `bash lint` when fast linting is available. + +## Performance + +- **Fast linting**: ~0.9 seconds (53x faster than previous solution) +- **Total lint time**: ~1.6 seconds (Python + C++ + JavaScript) diff --git a/v1.5/lib/FastLED/ci/kitchensink/kitchensink.ino.cpp b/v1.5/lib/FastLED/ci/kitchensink/kitchensink.ino.cpp new file mode 100644 index 0000000..17df6ac --- /dev/null +++ b/v1.5/lib/FastLED/ci/kitchensink/kitchensink.ino.cpp @@ -0,0 +1,16 @@ +#include "FastLED.h" + +// Let's include a bunch of stuff and see if it breaks the build. +#include +#include +#include +#include +#include + +void setup() { + +} + +void loop() { + +} \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/kitchensink/platformio.ini b/v1.5/lib/FastLED/ci/kitchensink/platformio.ini new file mode 100644 index 0000000..45eedb1 --- /dev/null +++ b/v1.5/lib/FastLED/ci/kitchensink/platformio.ini @@ -0,0 +1,17 @@ +[platformio] +src_dir = symlink://../../../src + +[env:dev] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip +board = esp32-s3-devkitc-1 +framework = arduino +lib_deps = + https://github.com/dvarrel/AsyncTCP + https://github.com/mathieucarbou/ESPAsyncWebServer + ArduinoJson + FS + ArduinoOTA + ESPmDNS + +lib_ldf_mode=deep +build_flags=-DFASTLED_STUB_MAIN_INCLUDE_INO="../ci/kitchensink/kitchensink.ino.cpp" \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/lint-js-fast b/v1.5/lib/FastLED/ci/lint-js-fast new file mode 100644 index 0000000..c24bf24 --- /dev/null +++ b/v1.5/lib/FastLED/ci/lint-js-fast @@ -0,0 +1,98 @@ +#!/bin/bash +# FastLED JavaScript Linting Script (Node.js + ESLint + JSDoc Type Checking - FAST!) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}FAST FastLED JavaScript Linting (Node.js + ESLint + JSDoc Type Checking - FAST!)${NC}" + +# Check if ESLint is installed +if [ ! -f ".cache/js-tools/node_modules/.bin/eslint.cmd" ]; then + echo -e "${RED}ERROR: ESLint not found. Run: uv run ci/setup-js-linting-fast.py${NC}" + exit 1 +fi + +# Find JavaScript files in WASM platform +JS_FILES=$(find src/platforms/wasm -name "*.js" -type f 2>/dev/null) + +if [ -z "$JS_FILES" ]; then + echo -e "${YELLOW}WARNING: No JavaScript files found in src/platforms/wasm/${NC}" + exit 0 +fi + +echo -e "${BLUE}Found JavaScript files:${NC}" +echo "$JS_FILES" | sed 's/^/ /' + +# Run ESLint using config from ci/ directory +echo -e "${BLUE}Running ESLint...${NC}" +cd .cache/js-tools +if "./node_modules/.bin/eslint.cmd" --no-eslintrc --no-inline-config -c ../../ci/.eslintrc.js ../../src/platforms/wasm/compiler/*.js ../../src/platforms/wasm/compiler/modules/*.js; then + echo -e "${GREEN}✅ ESLint completed successfully${NC}" +else + echo -e "${RED}ERROR: ESLint failed${NC}" + exit 1 +fi + +# Run JSDoc Type Checking using TypeScript +echo -e "${BLUE}Running JSDoc Type Checking...${NC}" +cd ../../src/platforms/wasm/compiler + +# Check if TypeScript is available (local installation first, then global) +if [ -f "../../../../.cache/js-tools/node_modules/.bin/tsc.cmd" ]; then + echo -e "${BLUE}Using local TypeScript for JSDoc type checking...${NC}" + # Run TypeScript and filter out Node.js events library errors + TSC_OUTPUT=$(../../../../.cache/js-tools/node_modules/.bin/tsc.cmd --noEmit --project jsconfig.json 2>&1) + TSC_EXIT_CODE=$? + # Filter out the specific Node.js events error + FILTERED_OUTPUT=$(echo "$TSC_OUTPUT" | grep -v "node_modules/events/events.js.*Property 'context' does not exist on type 'Error'" || true) + + if [ -n "$FILTERED_OUTPUT" ]; then + echo "$FILTERED_OUTPUT" + if [ $TSC_EXIT_CODE -ne 0 ]; then + echo -e "${RED}❌ JSDoc type checking failed with errors${NC}" + exit 1 + fi + fi + echo -e "${GREEN}✅ JSDoc type checking completed successfully${NC}" +elif [ -f "../../../../.cache/js-tools/node_modules/.bin/tsc" ]; then + echo -e "${BLUE}Using local TypeScript for JSDoc type checking...${NC}" + # Run TypeScript and filter out Node.js events library errors + TSC_OUTPUT=$(../../../../.cache/js-tools/node_modules/.bin/tsc --noEmit --project jsconfig.json 2>&1) + TSC_EXIT_CODE=$? + # Filter out the specific Node.js events error + FILTERED_OUTPUT=$(echo "$TSC_OUTPUT" | grep -v "node_modules/events/events.js.*Property 'context' does not exist on type 'Error'" || true) + + if [ -n "$FILTERED_OUTPUT" ]; then + echo "$FILTERED_OUTPUT" + if [ $TSC_EXIT_CODE -ne 0 ]; then + echo -e "${RED}❌ JSDoc type checking failed with errors${NC}" + exit 1 + fi + fi + echo -e "${GREEN}✅ JSDoc type checking completed successfully${NC}" +elif command -v npx >/dev/null 2>&1 && npx tsc --version >/dev/null 2>&1; then + echo -e "${BLUE}Using global TypeScript for JSDoc type checking...${NC}" + # Run TypeScript and filter out Node.js events library errors + TSC_OUTPUT=$(npx tsc --noEmit --project jsconfig.json 2>&1) + TSC_EXIT_CODE=$? + # Filter out the specific Node.js events error + FILTERED_OUTPUT=$(echo "$TSC_OUTPUT" | grep -v "node_modules/events/events.js.*Property 'context' does not exist on type 'Error'" || true) + + if [ -n "$FILTERED_OUTPUT" ]; then + echo "$FILTERED_OUTPUT" + if [ $TSC_EXIT_CODE -ne 0 ]; then + echo -e "${RED}❌ JSDoc type checking failed with errors${NC}" + exit 1 + fi + fi + echo -e "${GREEN}✅ JSDoc type checking completed successfully${NC}" +else + echo -e "${YELLOW}⚠️ TypeScript not found, skipping JSDoc type checking${NC}" + echo -e "${BLUE}💡 To enable JSDoc checking: Run the ./install script or npm install -g typescript${NC}" +fi + +echo -e "${GREEN}SUCCESS: JavaScript linting and type checking completed successfully${NC}" \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/lint-js-fast.backup b/v1.5/lib/FastLED/ci/lint-js-fast.backup new file mode 100644 index 0000000..a7d697a --- /dev/null +++ b/v1.5/lib/FastLED/ci/lint-js-fast.backup @@ -0,0 +1,38 @@ +#!/bin/bash +# FastLED JavaScript Linting Script (Node.js + ESLint - FAST!) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}FAST FastLED JavaScript Linting (Node.js + ESLint - FAST!)${NC}" + +# Check if ESLint is installed +if [ ! -f ".cache/js-tools/node_modules/.bin/eslint.cmd" ]; then + echo -e "${RED}ERROR: ESLint not found. Run: uv run ci/setup-js-linting-fast.py${NC}" + exit 1 +fi + +# Find JavaScript files in WASM platform +JS_FILES=$(find src/platforms/wasm -name "*.js" -type f 2>/dev/null) + +if [ -z "$JS_FILES" ]; then + echo -e "${YELLOW}WARNING: No JavaScript files found in src/platforms/wasm/${NC}" + exit 0 +fi + +echo -e "${BLUE}Found JavaScript files:${NC}" +echo "$JS_FILES" | sed 's/^/ /' + +# Run ESLint using config from ci/ directory +echo -e "${BLUE}Running ESLint...${NC}" +cd .cache/js-tools +if "./node_modules/.bin/eslint.cmd" --no-eslintrc --no-inline-config -c ../../ci/.eslintrc.js ../../src/platforms/wasm/compiler/*.js ../../src/platforms/wasm/compiler/modules/*.js; then + echo -e "${GREEN}SUCCESS: JavaScript linting completed successfully${NC}" +else + echo -e "${RED}ERROR: JavaScript linting failed${NC}" + exit 1 +fi \ No newline at end of file diff --git a/v1.5/lib/FastLED/ci/migration_example.py b/v1.5/lib/FastLED/ci/migration_example.py new file mode 100644 index 0000000..e2d0bea --- /dev/null +++ b/v1.5/lib/FastLED/ci/migration_example.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +FastLED Build System Migration Example + +This shows how to migrate from the old scattered build logic +to the new unified BuildAPI. +""" + +from pathlib import Path +from typing import List + +from ci.build_api import create_example_builder, create_unit_test_builder + + +def old_way_unit_test_building(): + """ + OLD WAY: Scattered logic with manual PCH and library building + + This is how unit test building worked before the refactor: + - Manual PCH building in multiple places + - Manual library building with duplicated logic + - Mixed build flags between unit tests and examples + - Manual directory management + """ + + # OLD CODE (before refactor) - DO NOT USE + """ + # Manual PCH building (scattered in multiple files) + from ci.compiler.test_example_compilation import create_fastled_compiler + from ci.compiler.cpp_test_compile import create_unit_test_fastled_library + + compiler = create_fastled_compiler(use_pch=True, use_sccache=False, parallel=True) + + # Manual library building (duplicated logic) + lib_file = create_unit_test_fastled_library(clean=False, use_pch=True) + + # Manual target compilation loop + for test_file in test_files: + success = compiler.compile_cpp_file(test_file, output_obj) + if success: + link_executable(output_obj, lib_file, output_exe) + """ + + +def new_way_unit_test_building(): + """ + NEW WAY: Unified BuildAPI with automatic PCH and library building + + This is how unit test building works after the refactor: + - Automatic PCH building (once, reused by all targets) + - Automatic library building (once, reused by all targets) + - Clean separation of unit test build flags + - Automatic directory organization + """ + + # NEW CODE (after refactor) - USE THIS + unit_builder = create_unit_test_builder( + build_dir=".build/unit", use_pch=True, parallel=True + ) + + test_files = [ + Path("tests/test_json.cpp"), + Path("tests/test_color.cpp"), + Path("tests/test_math.cpp"), + ] + + # Single call handles everything: + # - PCH building (automatically, once) + # - Library building (automatically, once) + # - Individual target compilation and linking + results = unit_builder.build_targets(test_files) + + # Clean result handling + for result in results: + if result.success: + print(f"✓ {result.target.name} built successfully") + else: + print(f"✗ {result.target.name} failed: {result.message}") + + +def old_way_example_building(): + """ + OLD WAY: Mixed build configuration and manual management + + Examples used the same build_flags.toml as unit tests, causing: + - Wrong defines (FASTLED_FORCE_NAMESPACE=1 for examples) + - Mixed configuration between unit tests and examples + - Manual PCH and library building + """ + + # OLD CODE (before refactor) - DO NOT USE + """ + # Used same build_flags.toml as unit tests (wrong!) + compiler = create_fastled_compiler(use_pch=True, use_sccache=False, parallel=True) + + # Manual example compilation with wrong defines + for example_file in example_files: + success = compiler.compile_ino_file(example_file, output_obj) + # ... manual linking logic ... + """ + + +def new_way_example_building(): + """ + NEW WAY: Clean separation with dedicated build configuration + + Examples now have their own build_example.toml with: + - Correct defines (no FASTLED_FORCE_NAMESPACE=1) + - Arduino compatibility defines + - Automatic PCH and library building + """ + + # NEW CODE (after refactor) - USE THIS + example_builder = create_example_builder( + build_dir=".build/examples", use_pch=True, parallel=True + ) + + example_files = [ + Path("examples/Blink/Blink.ino"), + Path("examples/DemoReel100/DemoReel100.ino"), + ] + + # Single call handles everything with correct configuration + results = example_builder.build_targets(example_files) + + for result in results: + if result.success: + print(f"✓ {result.target.name} built successfully") + else: + print(f"✗ {result.target.name} failed: {result.message}") + + +def migration_benefits(): + """ + Benefits of the new unified BuildAPI: + """ + + print("MIGRATION BENEFITS:") + print("=" * 50) + + print("1. UNIFIED API:") + print( + " Before: create_fastled_compiler() + create_unit_test_fastled_library() + manual loops" + ) + print(" After: create_unit_test_builder().build_targets(files)") + + print("\n2. CLEAN SEPARATION:") + print(" Before: build_flags.toml used by both unit tests and examples") + print(" After: build_unit.toml vs build_example.toml") + + print("\n3. AUTOMATIC ARTIFACT BUILDING:") + print(" Before: Manual PCH and library building in callers") + print(" After: Automatic PCH and library building in BuildAPI") + + print("\n4. ORGANIZED DIRECTORIES:") + print(" Before: Mixed artifacts in .build/") + print(" After: Organized .build/unit/ and .build/examples/ with subdirectories") + + print("\n5. MULTIPLE TARGET EFFICIENCY:") + print(" Before: PCH and library built for each target") + print(" After: PCH and library built once, reused by all targets") + + print("\n6. SIMPLE ERROR HANDLING:") + print(" Before: Manual error handling in compilation loops") + print(" After: Structured SketchResult objects with clear success/failure") + + +def migration_checklist(): + """ + Checklist for migrating existing code to the new BuildAPI: + """ + + print("\nMIGRATION CHECKLIST:") + print("=" * 50) + + print("✓ COMPLETED:") + print(" - Split ci/build_flags.toml into build_unit.toml and build_example.toml") + print(" - Created unified BuildAPI class") + print(" - Implemented automatic PCH and library building") + print(" - Implemented organized build directory structure") + print( + " - Created convenience functions create_unit_test_builder() and create_example_builder()" + ) + + print("\n⚠ TODO:") + print( + " - Update existing unit test compilation code to use create_unit_test_builder()" + ) + print( + " - Update existing example compilation code to use create_example_builder()" + ) + print(" - Remove old scattered PCH and library building logic") + print(" - Update test scripts to use new API") + print(" - Deprecate old build_flags.toml (marked as legacy)") + + +def show_api_simplification(): + """ + Show how the API is dramatically simplified: + """ + + print("\nAPI SIMPLIFICATION:") + print("=" * 50) + + print("BEFORE (scattered across multiple files):") + print(""" + # In ci/compiler/test_example_compilation.py + compiler = create_fastled_compiler(use_pch=True, use_sccache=False, parallel=True) + + # In ci/compiler/cpp_test_compile.py + lib_file = create_unit_test_fastled_library(clean=False, use_pch=True) + + # In test runner + for test_file in test_files: + obj_file = build_dir / f"{test_file.stem}.o" + success = compiler.compile_cpp_file(test_file, obj_file) + if success: + exe_file = build_dir / f"{test_file.stem}.exe" + link_success = link_executable(obj_file, lib_file, exe_file) + """) + + print("AFTER (unified API):") + print(""" + # Single unified API + builder = create_unit_test_builder() + results = builder.build_targets(test_files) + + # That's it! Everything else is automatic: + # - PCH building + # - Library building + # - Individual compilation + # - Linking + # - Directory organization + # - Error handling + """) + + +if __name__ == "__main__": + print("FastLED Build System Migration Example") + print("=" * 60) + + migration_benefits() + migration_checklist() + show_api_simplification() + + print("\n" + "=" * 60) + print("NEXT STEPS:") + print("1. Update existing unit test compilation to use create_unit_test_builder()") + print("2. Update existing example compilation to use create_example_builder()") + print("3. Remove old scattered build logic") + print("4. Test the new unified system") + print("=" * 60) diff --git a/v1.5/lib/FastLED/ci/native/platformio.ini b/v1.5/lib/FastLED/ci/native/platformio.ini new file mode 100644 index 0000000..c98b97e --- /dev/null +++ b/v1.5/lib/FastLED/ci/native/platformio.ini @@ -0,0 +1,17 @@ +[platformio] +src_dir = symlink://../../../src + +[env:dev] +platform = platformio/native + +lib_deps = + FastLED + +build_flags = + -DFASTLED_STUB_IMPL + -DFASTLED_USE_STUB_ARDUINO + -DFASTLED_STUB_MAIN_INCLUDE_INO="../examples/Blink/Blink.ino" + -std=c++17 + -I../../src/platforms + -isystem ../../src/platforms/stub ; for Arduino.h + diff --git a/v1.5/lib/FastLED/ci/optimization_report.py b/v1.5/lib/FastLED/ci/optimization_report.py new file mode 100644 index 0000000..acc21ef --- /dev/null +++ b/v1.5/lib/FastLED/ci/optimization_report.py @@ -0,0 +1,48 @@ +import argparse +import sys +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Convert a binary file to ELF using map file." + ) + parser.add_argument("--first", action="store_true", help="Inspect the first board") + parser.add_argument("--cwd", type=Path, help="Custom working directory") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if args.cwd: + root_build_dir = args.cwd / ".build" + else: + root_build_dir = Path(".build") + + # Support nested PlatformIO structure: .build/pio/ + nested_pio_dir = root_build_dir / "pio" + if nested_pio_dir.is_dir(): + root_build_dir = nested_pio_dir + + board_dirs = [d for d in root_build_dir.iterdir() if d.is_dir()] + if not board_dirs: + print(f"No board directories found in {root_build_dir.absolute()}") + return 1 + print("Available boards:") + for i, board_dir in enumerate(board_dirs): + print(f"[{i}]: {board_dir.name}") + which = ( + 0 + if args.first + else int(input("Enter the number of the board you want to inspect: ")) + ) + board_dir = board_dirs[which] + # build_info_json = board_dir / "build_info.json" + optimization_report = board_dir / "optimization_report.txt" + text = optimization_report.read_text(encoding="utf-8", errors="replace") + print(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/pyright-cached.py b/v1.5/lib/FastLED/ci/pyright-cached.py new file mode 100644 index 0000000..dab701d --- /dev/null +++ b/v1.5/lib/FastLED/ci/pyright-cached.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +""" +Caching wrapper for pyright that only runs if files have changed. +This significantly speeds up repeated pyright runs during development. +""" + +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional + + +def get_file_hash(file_path: Path) -> str: + """Get SHA256 hash of a file.""" + try: + with open(file_path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + except (OSError, IOError): + return "" + + +def get_directory_hash(directory: Path, extensions: Optional[List[str]] = None) -> str: + """Get combined hash of all files in a directory.""" + if extensions is None: + extensions = [".py"] + + hashes: List[str] = [] + if directory.exists(): + for ext in extensions: + for file_path in directory.rglob(f"*{ext}"): + if file_path.is_file(): + hashes.append( + f"{file_path.relative_to(directory)}:{get_file_hash(file_path)}" + ) + + hashes.sort() # Ensure consistent ordering + return hashlib.sha256("\n".join(hashes).encode()).hexdigest() + + +def get_cache_file() -> Path: + """Get the cache file path.""" + cache_dir = Path(".cache/pyright") + cache_dir.mkdir(parents=True, exist_ok=True) + return cache_dir / "file_hashes.json" + + +def load_cache() -> Dict[str, str]: + """Load the cache file.""" + cache_file = get_cache_file() + if cache_file.exists(): + try: + with open(cache_file, "r") as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + pass + return {} + + +def save_cache(cache: Dict[str, str]) -> None: + """Save the cache file.""" + cache_file = get_cache_file() + try: + with open(cache_file, "w") as f: + json.dump(cache, f, indent=2) + except IOError: + pass # Ignore cache save errors + + +def has_files_changed() -> bool: + """Check if any relevant files have changed since last run.""" + cache = load_cache() + changed = False + + # Files and directories to check + files_to_check = [ + Path("pyrightconfig.json"), + Path(".pyrightconfig.json"), + Path("pyproject.toml"), + ] + + directories_to_check = [ + Path("ci"), + ] + + # Check individual files + for file_path in files_to_check: + if file_path.exists(): + current_hash = get_file_hash(file_path) + if cache.get(str(file_path)) != current_hash: + cache[str(file_path)] = current_hash + changed = True + + # Check directories + for dir_path in directories_to_check: + if dir_path.exists(): + current_hash = get_directory_hash(dir_path) + cache_key = f"dir:{dir_path}" + if cache.get(cache_key) != current_hash: + cache[cache_key] = current_hash + changed = True + + # Save cache if any changes were detected + if changed: + save_cache(cache) + + return changed + + +def run_pyright(args: List[str]) -> int: + """Run pyright with the given arguments.""" + env = os.environ.copy() + # env["PYRIGHT_PYTHON_FORCE_VERSION"] = "latest" + env["PYRIGHT_PYTHON_CACHE_DIR"] = ".cache/pyright" + + cmd = ["uv", "run", "pyright"] + args + + try: + result = subprocess.run(cmd, env=env) + return result.returncode + except KeyboardInterrupt: + return 1 + except Exception as e: + print(f"Error running pyright: {e}", file=sys.stderr) + return 1 + + +def main() -> int: + """Main entry point.""" + args = sys.argv[1:] # Remove script name + + # If --force is passed, skip cache check + if "--force" in args: + args.remove("--force") + print("Forcing pyright run (--force flag detected)") + return run_pyright(args) + + # Check if files have changed + if not has_files_changed(): + print("No changes detected, skipping pyright (use --force to override)") + return 0 + + print("Changes detected, running pyright...") + return run_pyright(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/qemu-esp32.py b/v1.5/lib/FastLED/ci/qemu-esp32.py new file mode 100644 index 0000000..fe26a9d --- /dev/null +++ b/v1.5/lib/FastLED/ci/qemu-esp32.py @@ -0,0 +1,548 @@ +#!/usr/bin/env python3 +""" +ESP32 QEMU Runner + +Runs ESP32 firmware in QEMU with logging and validation. +Designed to replace the tobozo/esp32-qemu-sim GitHub Action. +""" + +import argparse +import platform +import re +import subprocess +import sys +import threading +import time +from pathlib import Path +from typing import List, Optional, TextIO + +from ci.util.running_process import RunningProcess + + +def get_binary_name() -> str: + """Get platform-specific QEMU binary name.""" + return ( + "qemu-system-xtensa.exe" + if platform.system().lower() == "windows" + else "qemu-system-xtensa" + ) + + +def find_executable(binary_name: str) -> Optional[Path]: + """Find executable in PATH using platform-appropriate command.""" + try: + cmd = [ + "where" if platform.system().lower() == "windows" else "which", + binary_name, + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode == 0: + # Take first line in case of multiple results (Windows 'where') + return Path(result.stdout.strip().split("\n")[0]) + except (subprocess.SubprocessError, FileNotFoundError): + pass + return None + + +def find_qemu_binary() -> Optional[Path]: + """Find QEMU ESP32 binary in common locations.""" + binary_name = get_binary_name() + + # ESP-IDF installation paths and portable installation + esp_paths = [ + Path(".cache") / "qemu", # Project-local portable installation directory + Path.home() / ".espressif" / "tools" / "qemu-xtensa", + Path.home() / ".espressif" / "python_env", + ] + + for base_path in esp_paths: + if base_path.exists(): + for qemu_path in base_path.rglob(binary_name): + if qemu_path.is_file(): + return qemu_path + + # Check system PATH + return find_executable(binary_name) + + +def show_installation_help(): + """Display platform-specific installation instructions.""" + binary_name = get_binary_name() + print(f"ERROR: QEMU ESP32 emulator ({binary_name}) not found!", file=sys.stderr) + print() + + if platform.system().lower() == "windows": + print("To install QEMU on Windows:") + print("1. Chocolatey: choco install qemu -y") + print("2. winget: winget install SoftwareFreedomConservancy.QEMU --scope user") + print("3. Manual: Download from https://qemu.weilnetz.de/w64/") + print("4. ESP-IDF: uv run ci/install-qemu.py") + else: + print("Run: uv run ci/install-qemu.py") + + +class QEMURunner: + def __init__(self, qemu_binary: Optional[Path] = None): + self.qemu_binary = qemu_binary or self._get_qemu_binary() + self.output_lines: List[str] = [] + self.interrupt_met = False + + def _get_qemu_binary(self) -> Path: + """Get QEMU binary path or raise error.""" + qemu_path = find_qemu_binary() + if not qemu_path: + raise FileNotFoundError( + f"Could not find {get_binary_name()}. Please run install-qemu.py first." + ) + return qemu_path + + def build_qemu_command(self, firmware_path: Path, flash_size: int = 4) -> List[str]: + """Build QEMU command line arguments. + + Args: + firmware_path: Path to firmware.bin or directory containing build artifacts + flash_size: Flash size in MB + """ + # Validate that flash_size is used correctly internally only + if not isinstance(flash_size, int) or flash_size <= 0: + raise ValueError( + f"Invalid flash_size: {flash_size}. Must be a positive integer." + ) + + # Determine if we have a direct firmware.bin or a build directory + if firmware_path.is_file() and firmware_path.suffix == ".bin": + # Direct firmware.bin path + flash_image_path = firmware_path.parent / "flash.bin" + print(f"Creating flash image for firmware: {firmware_path}") + self._create_flash_image_from_firmware( + firmware_path, flash_image_path, flash_size + ) + else: + # Build directory path + flash_image_path = firmware_path / "flash.bin" + print( + f"Creating combined flash image from build directory: {firmware_path}" + ) + self._create_flash_image_from_build_dir( + firmware_path, flash_image_path, flash_size + ) + + # Check for ROM file in multiple locations + rom_paths = [ + Path(".cache") / "qemu" / "esp32-v3-rom.bin", + Path(".cache") / "qemu" / "qemu" / "share" / "qemu" / "esp32-v3-rom.bin", + Path(".cache") / "qemu" / "extracted" / "share" / "esp32-v3-rom.bin", + ] + + rom_path = None + for path in rom_paths: + if path.exists(): + rom_path = path + break + + # Ensure flash image path is absolute and exists + flash_image_path = flash_image_path.resolve() + if not flash_image_path.exists(): + raise FileNotFoundError(f"Flash image not found: {flash_image_path}") + + cmd = [ + str(self.qemu_binary), + "-nographic", + "-machine", + "esp32", + "-drive", + f"file={str(flash_image_path)},if=mtd,format=raw", + "-global", + "driver=timer.esp32.timg,property=wdt_disable,value=true", + ] + + # Set QEMU data directory if ROM files are available + if rom_path and rom_path.exists(): + qemu_data_dir = rom_path.parent + cmd.extend(["-L", str(qemu_data_dir)]) + print(f"Setting QEMU data directory: {qemu_data_dir}") + + # Note: ESP32 ROM is built into QEMU machine, no need to load external ROM + # ESP32 QEMU machine has the ROM integrated, so we don't need to specify it + if rom_path and rom_path.exists(): + print( + f"ROM file available at: {rom_path} (but not needed for ESP32 machine)" + ) + else: + print("Note: ROM file not found, but ESP32 machine has integrated ROM") + + # Validate that no custom script arguments are accidentally included + custom_args = ["--flash-size", "--timeout", "--interrupt-regex"] + for arg in cmd: + if any(custom_arg in str(arg) for custom_arg in custom_args): + raise ValueError( + f"Custom script argument {arg} should not be passed to QEMU" + ) + + return cmd + + def _create_flash_image_from_firmware( + self, firmware_bin: Path, flash_bin: Path, flash_size: int + ): + """Create flash image from a direct firmware.bin file.""" + if flash_bin.exists(): + print(f"Using existing flash image: {flash_bin}") + return + + print(f"Creating flash image from firmware: {firmware_bin}") + + # Create empty flash image + flash_size_bytes = flash_size * 1024 * 1024 + flash_bin.write_bytes(b"\xff" * flash_size_bytes) + + try: + esptool_cmd = self._find_esptool() + if esptool_cmd: + # Use esptool to create proper flash image + self._merge_with_esptool_firmware(esptool_cmd, firmware_bin, flash_bin) + else: + # Simple manual merge - just write firmware at 0x10000 + self._merge_manually_firmware(firmware_bin, flash_bin) + except Exception as e: + print(f"Warning: Could not create proper flash image: {e}") + + def _create_flash_image_from_build_dir( + self, build_folder: Path, flash_bin: Path, flash_size: int + ): + """Create combined flash image from ESP32 build artifacts in a directory.""" + if flash_bin.exists(): + print(f"Using existing flash image: {flash_bin}") + return + + print("Creating combined flash image from build directory...") + + # Create empty flash image + flash_size_bytes = flash_size * 1024 * 1024 + flash_bin.write_bytes(b"\xff" * flash_size_bytes) + + try: + esptool_cmd = self._find_esptool() + if esptool_cmd: + self._merge_with_esptool(esptool_cmd, build_folder, flash_bin) + else: + self._merge_manually(build_folder, flash_bin) + except Exception as e: + print(f"Warning: Could not create proper flash image: {e}") + + def _find_esptool(self) -> Optional[str]: + """Find esptool.py executable.""" + # Try direct esptool.py + if find_executable("esptool.py"): + return "esptool.py" + + # Try python -m esptool + try: + result = subprocess.run( + [sys.executable, "-m", "esptool", "--help"], + capture_output=True, + timeout=5, + ) + if result.returncode == 0: + return f"{sys.executable} -m esptool" + except (subprocess.SubprocessError, subprocess.TimeoutExpired): + pass + + return None + + def _merge_with_esptool( + self, esptool_cmd: str, build_folder: Path, flash_bin: Path + ): + """Merge using esptool.py merge_bin command.""" + cmd = esptool_cmd.split() + [ + "--chip", + "esp32", # Use esp32 chip type for esp32dev machine + "merge-bin", # Updated command name + "-o", + str(flash_bin), + "--flash-mode", # Updated option name + "dio", + "--flash-freq", # Updated option name + "40m", # Standard frequency for esp32 + "--flash-size", # Updated option name + "4MB", + ] + + # Add binaries at their offsets - check if we're in the actual build directory + if (build_folder / "firmware.bin").exists(): + firmware_path = build_folder / "firmware.bin" + else: + firmware_path = ( + build_folder / ".pio" / "build" / "esp32dev" / "firmware.bin" + ) + + if not firmware_path.exists(): + print( + f"FATAL: ESP32dev firmware not found at {firmware_path}", + file=sys.stderr, + ) + print("Please build for esp32dev target first.", file=sys.stderr) + sys.exit(1) + + binaries = [ + ("0x1000", build_folder / "bootloader.bin"), # ESP32 bootloader offset + ("0x8000", build_folder / "partitions.bin"), + ("0x10000", firmware_path), + ] + + for offset, bin_path in binaries: + if bin_path.exists(): + cmd.extend([offset, str(bin_path)]) + + # Add padding to 4MB at the end + cmd.extend(["--pad-to-size", "4MB"]) + + print(f"Running esptool command: {' '.join(cmd)}") + esptool_proc = RunningProcess(cmd, timeout=60, auto_run=True) + + # Stream esptool output and check for errors + has_errors = False + with esptool_proc.line_iter(timeout=None) as it: + for line in it: + print(f"[esptool] {line}") + # Check for common esptool error patterns + if any( + error_pattern in line.lower() + for error_pattern in ["error", "failed", "exception"] + ): + has_errors = True + + returncode = esptool_proc.wait() + if returncode != 0: + raise subprocess.CalledProcessError(returncode, cmd) + if has_errors: + print("Warning: Errors detected in esptool output") + print("Flash image created with esptool") + + def _merge_with_esptool_firmware( + self, esptool_cmd: str, firmware_bin: Path, flash_bin: Path + ): + """Merge firmware.bin using esptool with minimal bootloader.""" + cmd = esptool_cmd.split() + [ + "--chip", + "esp32", + "merge-bin", # Updated command name + "-o", + str(flash_bin), + "--flash-mode", # Updated option name + "dio", + "--flash-freq", # Updated option name + "40m", + "--flash-size", # Updated option name + "4MB", + "0x10000", # Application offset + str(firmware_bin), + ] + + # Note: Without bootloader and partition table, QEMU may not boot properly + # but we'll try with just the app for now + print(f"Running esptool firmware command: {' '.join(cmd)}") + esptool_proc = RunningProcess(cmd, timeout=60, auto_run=True) + + # Stream esptool output and check for errors + has_errors = False + with esptool_proc.line_iter(timeout=None) as it: + for line in it: + print(f"[esptool] {line}") + # Check for common esptool error patterns + if any( + error_pattern in line.lower() + for error_pattern in ["error", "failed", "exception"] + ): + has_errors = True + + returncode = esptool_proc.wait() + if returncode != 0: + raise subprocess.CalledProcessError(returncode, cmd) + if has_errors: + print("Warning: Errors detected in esptool output") + print("Flash image created with esptool (firmware only)") + + def _merge_manually_firmware(self, firmware_bin: Path, flash_bin: Path): + """Manually write firmware.bin to flash image.""" + print("WARNING: Manually merging firmware (esptool not found)") + print("Note: Without bootloader, QEMU may not boot properly") + + with open(flash_bin, "r+b") as flash: + # Write firmware at application offset + flash.seek(0x10000) + flash.write(firmware_bin.read_bytes()) + print(f"Firmware written to flash image at offset 0x10000") + + def _merge_manually(self, build_folder: Path, flash_bin: Path): + """Manually merge binary files at correct offsets.""" + print("WARNING: Manually merging binaries (esptool not found)") + + # Find firmware binary - check if we're in the actual build directory + if (build_folder / "firmware.bin").exists(): + firmware_path = build_folder / "firmware.bin" + else: + firmware_path = ( + build_folder / ".pio" / "build" / "esp32dev" / "firmware.bin" + ) + + if not firmware_path.exists(): + print( + f"FATAL: ESP32dev firmware not found at {firmware_path}", + file=sys.stderr, + ) + print("Please build for esp32dev target first.", file=sys.stderr) + sys.exit(1) + + binaries = [ + ("bootloader.bin", 0x1000), # ESP32 bootloader offset + ("partitions.bin", 0x8000), + (firmware_path, 0x10000), + ] + + with open(flash_bin, "r+b") as flash: + for filename, offset in binaries: + bin_path = build_folder / filename + if bin_path.exists(): + flash.seek(offset) + flash.write(bin_path.read_bytes()) + + def run( + self, + firmware_path: Path, + timeout: int = 30, + interrupt_regex: Optional[str] = None, + flash_size: int = 4, + ) -> int: + """Run ESP32 firmware in QEMU. + + Args: + firmware_path: Path to firmware.bin file or build directory + timeout: Timeout in seconds + interrupt_regex: Regex pattern to interrupt execution + flash_size: Flash size in MB + """ + print("=== Running ESP32 firmware in QEMU ===") + print(f"Firmware path: {firmware_path}") + print(f"Timeout: {timeout}s") + print(f"Flash size: {flash_size}MB") + if interrupt_regex: + print(f"Interrupt pattern: {interrupt_regex}") + + if not firmware_path.exists(): + print(f"ERROR: Path does not exist: {firmware_path}", file=sys.stderr) + return 1 + + try: + cmd = self.build_qemu_command(firmware_path, flash_size) + print(f"QEMU command: {' '.join(cmd)}") + except Exception as e: + print(f"ERROR: Failed to build QEMU command: {e}", file=sys.stderr) + return 1 + + try: + qemu_proc = RunningProcess( + cmd, + timeout=timeout, + auto_run=True, + ) + + self.interrupt_met = False + interrupt_pattern = re.compile(interrupt_regex) if interrupt_regex else None + self.output_lines = [] + + try: + with qemu_proc.line_iter(timeout=timeout) as it: + for line in it: + print(line, flush=True) + self.output_lines.append(line) + if interrupt_pattern and interrupt_pattern.search(line): + print(f"SUCCESS: Interrupt condition met: {line}") + self.interrupt_met = True + time.sleep(2) + except TimeoutError: + print( + f"ERROR: QEMU timed out after {timeout} seconds of no output.", + file=sys.stderr, + ) + qemu_proc.kill() + return 1 + + return_code = qemu_proc.wait() + + log_file = Path("qemu_output.log") + log_file.write_text("\n".join(self.output_lines)) + print(f"QEMU output saved to: {log_file}") + print(f"Total output lines: {len(self.output_lines)}") + + if self.interrupt_met: + return 0 + + return return_code + + except Exception as e: + print(f"ERROR: Error running QEMU: {e}", file=sys.stderr) + return 1 + + +def main(): + parser = argparse.ArgumentParser(description="Run ESP32 firmware in QEMU") + parser.add_argument( + "firmware_path", + type=Path, + help="Path to firmware.bin file or build directory containing firmware files", + ) + parser.add_argument( + "--flash-size", type=int, default=4, help="Flash size in MB (default: 4)" + ) + parser.add_argument( + "--timeout", type=int, default=30, help="Timeout in seconds (default: 30)" + ) + parser.add_argument( + "--interrupt-regex", type=str, help="Regex pattern to interrupt execution" + ) + + args = parser.parse_args() + + # Validate arguments to prevent issues + if args.flash_size <= 0: + print( + f"ERROR: Invalid flash size: {args.flash_size}. Must be positive.", + file=sys.stderr, + ) + return 1 + + if args.timeout <= 0: + print( + f"ERROR: Invalid timeout: {args.timeout}. Must be positive.", + file=sys.stderr, + ) + return 1 + + # Ensure we're not accidentally being called as a wrapper for qemu-system-xtensa + if len(sys.argv) > 1 and any("qemu-system-xtensa" in arg for arg in sys.argv): + print( + "ERROR: This script should not be used as a wrapper for qemu-system-xtensa", + file=sys.stderr, + ) + print( + "Use this script directly: python qemu-esp32.py [options]", + file=sys.stderr, + ) + return 1 + + # Check if QEMU is available + if not find_qemu_binary(): + show_installation_help() + return 1 + + runner = QEMURunner() + return runner.run( + firmware_path=args.firmware_path, + timeout=args.timeout, + interrupt_regex=args.interrupt_regex, + flash_size=args.flash_size, + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/run-clang-format.py b/v1.5/lib/FastLED/ci/run-clang-format.py new file mode 100644 index 0000000..c9e1b2d --- /dev/null +++ b/v1.5/lib/FastLED/ci/run-clang-format.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python +# pyright: reportUnknownMemberType=false +"""A wrapper script around clang-format, suitable for linting multiple files +and to use for continuous integration. + +This is an alternative API for the clang-format command line. +It runs over multiple files and directories in parallel. +A diff output is produced and a sensible exit code is returned. + +""" + +from __future__ import print_function, unicode_literals + +import argparse +import codecs +import difflib +import errno +import fnmatch +import io +import multiprocessing +import os +import signal +import subprocess +import sys +import traceback +from functools import partial +from typing import Any, Dict, Generator, List, Optional, Tuple, Union + + +try: + from subprocess import DEVNULL # py3k +except ImportError: + DEVNULL = open(os.devnull, "wb") # type: ignore + + +DEFAULT_EXTENSIONS = "c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx" +DEFAULT_CLANG_FORMAT_IGNORE = ".clang-format-ignore" + + +class ExitStatus: + SUCCESS = 0 + DIFF = 1 + TROUBLE = 2 + + +def excludes_from_file(ignore_file: str) -> List[str]: + excludes: List[str] = [] + try: + with io.open(ignore_file, "r", encoding="utf-8") as f: + for line in f: + if line.startswith("#"): + # ignore comments + continue + pattern = line.rstrip() + if not pattern: + # allow empty lines + continue + excludes.append(pattern) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + return excludes + + +def list_files( + files: List[str], + recursive: bool = False, + extensions: Optional[List[str]] = None, + exclude: Optional[List[str]] = None, +) -> List[str]: + if extensions is None: + extensions = [] + if exclude is None: + exclude = [] + + out: List[str] = [] + for file in files: + if recursive and os.path.isdir(file): + for dirpath, dnames, fnames in os.walk(file): + fpaths = [os.path.join(dirpath, fname) for fname in fnames] + for pattern in exclude: + # os.walk() supports trimming down the dnames list + # by modifying it in-place, + # to avoid unnecessary directory listings. + dnames[:] = [ + x + for x in dnames + if not fnmatch.fnmatch(os.path.join(dirpath, x), pattern) + ] + fpaths = [x for x in fpaths if not fnmatch.fnmatch(x, pattern)] + for f in fpaths: + ext = os.path.splitext(f)[1][1:] + if ext in extensions: + out.append(f) + else: + out.append(file) + return out + + +def make_diff(file: str, original: List[str], reformatted: List[str]) -> List[str]: + return list( + difflib.unified_diff( + original, + reformatted, + fromfile="{}\t(original)".format(file), + tofile="{}\t(reformatted)".format(file), + n=3, + ) + ) + + +class DiffError(Exception): + def __init__(self, message: str, errs: Optional[List[str]] = None): + super(DiffError, self).__init__(message) + self.errs: List[str] = errs or [] + + +class UnexpectedError(Exception): + def __init__(self, message: str, exc: Optional[Exception] = None): + super(UnexpectedError, self).__init__(message) + self.formatted_traceback = traceback.format_exc() + self.exc = exc + + +def run_clang_format_diff_wrapper(args: Any, file: str) -> Tuple[List[str], List[str]]: + try: + ret = run_clang_format_diff(args, file) + return ret + except DiffError: + raise + except Exception as e: + raise UnexpectedError("{}: {}: {}".format(file, e.__class__.__name__, e), e) + + +def run_clang_format_diff(args: Any, file: str) -> Tuple[List[str], List[str]]: + try: + with io.open(file, "r", encoding="utf-8") as f: + original = f.readlines() + except IOError as exc: + raise DiffError(str(exc)) + + if args.in_place: + invocation = [args.clang_format_executable, "-i", file] + else: + invocation = [args.clang_format_executable, file] + + if args.style: + invocation.extend(["--style", args.style]) + + if args.dry_run: + print(" ".join(invocation)) + return [], [] + + # Use of utf-8 to decode the process output. + # + # Hopefully, this is the correct thing to do. + # + # It's done due to the following assumptions (which may be incorrect): + # - clang-format will returns the bytes read from the files as-is, + # without conversion, and it is already assumed that the files use utf-8. + # - if the diagnostics were internationalized, they would use utf-8: + # > Adding Translations to Clang + # > + # > Not possible yet! + # > Diagnostic strings should be written in UTF-8, + # > the client can translate to the relevant code page if needed. + # > Each translation completely replaces the format string + # > for the diagnostic. + # > -- http://clang.llvm.org/docs/InternalsManual.html#internals-diag-translation + # + # It's not pretty, due to Python 2 & 3 compatibility. + encoding_py3: Dict[str, str] = {} + if sys.version_info[0] >= 3: + encoding_py3["encoding"] = "utf-8" + + try: + if sys.version_info[0] >= 3: + proc: subprocess.Popen[str] = subprocess.Popen( + invocation, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding="utf-8", + ) + else: + proc: subprocess.Popen[str] = subprocess.Popen( + invocation, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + except OSError as exc: + raise DiffError( + "Command '{}' failed to start: {}".format( + subprocess.list2cmdline(invocation), exc + ) + ) + proc_stdout = proc.stdout + proc_stderr = proc.stderr + assert proc_stdout is not None + assert proc_stderr is not None + if sys.version_info[0] < 3: + # make the pipes compatible with Python 3, + # reading lines should output unicode + encoding = "utf-8" + proc_stdout = codecs.getreader(encoding)(proc_stdout) + proc_stderr = codecs.getreader(encoding)(proc_stderr) + # hopefully the stderr pipe won't get full and block the process + outs = list(proc_stdout.readlines()) + errs = list(proc_stderr.readlines()) + proc.wait() + if proc.returncode: + raise DiffError( + "Command '{}' returned non-zero exit status {}".format( + subprocess.list2cmdline(invocation), proc.returncode + ), + errs, + ) + if args.in_place: + return [], errs + return make_diff(file, original, outs), errs + + +def bold_red(s: str) -> str: + return "\x1b[1m\x1b[31m" + s + "\x1b[0m" + + +def colorize(diff_lines: List[str]) -> Generator[str, None, None]: + def bold(s: str) -> str: + return "\x1b[1m" + s + "\x1b[0m" + + def cyan(s: str) -> str: + return "\x1b[36m" + s + "\x1b[0m" + + def green(s: str) -> str: + return "\x1b[32m" + s + "\x1b[0m" + + def red(s: str) -> str: + return "\x1b[31m" + s + "\x1b[0m" + + for line in diff_lines: + if line[:4] in ["--- ", "+++ "]: + yield bold(line) + elif line.startswith("@@ "): + yield cyan(line) + elif line.startswith("+"): + yield green(line) + elif line.startswith("-"): + yield red(line) + else: + yield line + + +def print_diff(diff_lines: List[str], use_color: bool) -> None: + if use_color: + diff_lines = list(colorize(diff_lines)) + if sys.version_info[0] < 3: + sys.stdout.writelines((line.encode("utf-8") for line in diff_lines)) + else: + sys.stdout.writelines(diff_lines) + + +def print_trouble(prog: str, message: str, use_colors: bool) -> None: + error_text = "error:" + if use_colors: + error_text = bold_red(error_text) + print("{}: {} {}".format(prog, error_text, message), file=sys.stderr) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--clang-format-executable", + metavar="EXECUTABLE", + help="path to the clang-format executable", + default="clang-format", + ) + parser.add_argument( + "--extensions", + help="comma separated list of file extensions (default: {})".format( + DEFAULT_EXTENSIONS + ), + default=DEFAULT_EXTENSIONS, + ) + parser.add_argument( + "-r", + "--recursive", + action="store_true", + help="run recursively over directories", + ) + parser.add_argument( + "-d", "--dry-run", action="store_true", help="just print the list of files" + ) + parser.add_argument( + "-i", + "--in-place", + action="store_true", + help="format file instead of printing differences", + ) + parser.add_argument("files", metavar="file", nargs="+") + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="disable output, useful for the exit code", + ) + parser.add_argument( + "-j", + metavar="N", + type=int, + default=0, + help="run N clang-format jobs in parallel (default number of cpus + 1)", + ) + parser.add_argument( + "--color", + default="auto", + choices=["auto", "always", "never"], + help="show colored diff (default: auto)", + ) + parser.add_argument( + "-e", + "--exclude", + metavar="PATTERN", + action="append", + default=[], + help="exclude paths matching the given glob-like pattern(s)" + " from recursive search", + ) + parser.add_argument( + "--style", + help="formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)", + ) + + args = parser.parse_args() + + # use default signal handling, like diff return SIGINT value on ^C + # https://bugs.python.org/issue14229#msg156446 + signal.signal(signal.SIGINT, signal.SIG_DFL) + + try: + signal.SIGPIPE # type: ignore + except AttributeError: + # compatibility, SIGPIPE does not exist on Windows + pass + else: + signal.signal(signal.SIGPIPE, signal.SIG_DFL) # type: ignore + + colored_stdout = False + colored_stderr = False + if args.color == "always": + colored_stdout = True + colored_stderr = True + elif args.color == "auto": + colored_stdout = sys.stdout.isatty() + colored_stderr = sys.stderr.isatty() + + version_invocation = [args.clang_format_executable, str("--version")] + try: + subprocess.check_call(version_invocation, stdout=DEVNULL) + except subprocess.CalledProcessError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + return ExitStatus.TROUBLE + except OSError as e: + print_trouble( + parser.prog, + "Command '{}' failed to start: {}".format( + subprocess.list2cmdline(version_invocation), e + ), + use_colors=colored_stderr, + ) + return ExitStatus.TROUBLE + + retcode = ExitStatus.SUCCESS + + excludes = excludes_from_file(DEFAULT_CLANG_FORMAT_IGNORE) + excludes.extend(args.exclude) + + files = list_files( + args.files, + recursive=args.recursive, + exclude=excludes, + extensions=args.extensions.split(","), + ) + + if not files: + return retcode + + njobs = args.j + if njobs == 0: + njobs = multiprocessing.cpu_count() + 1 + njobs = min(len(files), njobs) + + if njobs == 1: + # execute directly instead of in a pool, + # less overhead, simpler stacktraces + it = (run_clang_format_diff_wrapper(args, file) for file in files) + pool = None + else: + pool = multiprocessing.Pool(njobs) + it = pool.imap_unordered(partial(run_clang_format_diff_wrapper, args), files) + pool.close() + while True: + try: + outs, errs = next(it) + except StopIteration: + break + except DiffError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + retcode = ExitStatus.TROUBLE + sys.stderr.writelines(e.errs) + except UnexpectedError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + sys.stderr.write(e.formatted_traceback) + retcode = ExitStatus.TROUBLE + # stop at the first unexpected error, + # something could be very wrong, + # don't process all files unnecessarily + if pool: + pool.terminate() + break + else: + sys.stderr.writelines(errs) + if outs == []: + continue + if not args.quiet: + print_diff(outs, use_color=colored_stdout) + if retcode == ExitStatus.SUCCESS: + retcode = ExitStatus.DIFF + if pool: + pool.join() + return retcode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/run_test_wasm_local_compile.py b/v1.5/lib/FastLED/ci/run_test_wasm_local_compile.py new file mode 100644 index 0000000..5ebdb94 --- /dev/null +++ b/v1.5/lib/FastLED/ci/run_test_wasm_local_compile.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +""" +Unit test file. +""" + +import unittest + + +class ApiTester(unittest.TestCase): + """Main tester class.""" + + def test_build_all_examples(self) -> None: + """Test command line interface (CLI).""" + from fastled import Api, Test # type: ignore + + with Api.server(auto_updates=True) as server: + exception_map = Test.test_examples(host=server) + if len(exception_map) > 0: + exception: Exception + msg: str = "" + for example, exception in exception_map.items(): + msg += f"Failed to compile example: {example}, error: {exception}\n" + self.fail(msg) + + # self.assertEqual(0, len(out), f"Failed tests: {out}") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/run_tests.py b/v1.5/lib/FastLED/ci/run_tests.py new file mode 100644 index 0000000..0b444aa --- /dev/null +++ b/v1.5/lib/FastLED/ci/run_tests.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +""" +Simple test runner for FastLED unit tests. +Discovers and runs test executables in parallel with clean output handling. +""" + +import argparse +import os +import subprocess +import sys +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional + +from ci.util.running_process import RunningProcess +from ci.util.test_exceptions import ( + TestExecutionFailedException, + TestFailureInfo, +) + + +_HERE = Path(__file__).parent +_PROJECT_ROOT = _HERE.parent + + +@dataclass +class TestResult: + """Result of a test execution""" + + name: str + success: bool + duration: float + output: str + + +def _is_test_executable(f: Path) -> bool: + """Check if a file is a valid test executable""" + return ( + f.is_file() and f.suffix not in [".o", ".obj", ".pdb"] and os.access(f, os.X_OK) + ) + + +def _get_test_patterns() -> List[str]: + """Get test patterns based on platform""" + # On Windows, check both .exe and no extension (Clang generates without .exe) + return ["test_*.exe"] if sys.platform == "win32" else ["test_*"] + + +def discover_tests(build_dir: Path, specific_test: Optional[str] = None) -> List[Path]: + """Find test executables in the build directory""" + # Check test directory + possible_test_dirs = [ + _PROJECT_ROOT / "tests" / "bin", # Python build system + ] + + test_dir = None + for possible_dir in possible_test_dirs: + if possible_dir.exists(): + # Check if this directory has actual executable test files + executable_tests = [ + f + for pattern in _get_test_patterns() + for f in possible_dir.glob(pattern) + if _is_test_executable(f) + ] + if executable_tests: + test_dir = possible_dir + break + + if not test_dir: + print(f"Error: No test directory found. Checked: {possible_test_dirs}") + sys.exit(1) + + test_files: List[Path] = [] + for pattern in _get_test_patterns(): + for test_file in test_dir.glob(pattern): + if not _is_test_executable(test_file): + continue + if specific_test: + # Support both "test_name" and "name" formats (case-insensitive) + test_stem = test_file.stem + test_name = test_stem.replace("test_", "") + if ( + test_stem.lower() == specific_test.lower() + or test_name.lower() == specific_test.lower() + ): + test_files.append(test_file) + else: + test_files.append(test_file) + + return test_files + + +def run_test(test_file: Path, verbose: bool = False) -> TestResult: + """Run a single test and capture its output""" + start_time = time.time() + + # Build command with doctest flags + # Convert to absolute path for Windows compatibility + test_executable = test_file.resolve() + cmd = [str(test_executable)] + if not verbose: + cmd.append("--minimal") # Only show output for failures + + try: + process = RunningProcess( + command=cmd, + cwd=Path(".").absolute(), + check=False, + shell=False, + auto_run=True, + ) + + with process.line_iter(timeout=30) as it: + for line in it: + print(line) + + # Wait for process to complete + process.wait() + success = process.poll() == 0 + output = process.stdout + + except Exception as e: + success = False + output = f"Error running test: {e}" + + duration = time.time() - start_time + return TestResult( + name=test_file.stem, success=success, duration=duration, output=output + ) + + +def main() -> None: + try: + parser = argparse.ArgumentParser(description="Run FastLED unit tests") + parser.add_argument("--test", help="Run specific test (without test_ prefix)") + parser.add_argument( + "--verbose", "-v", action="store_true", help="Show all test output" + ) + parser.add_argument("--jobs", "-j", type=int, help="Number of parallel jobs") + args = parser.parse_args() + + # Find build directory + build_dir = Path.cwd() / ".build" + if not build_dir.exists(): + print("Error: Build directory not found. Run compilation first.") + sys.exit(1) + + # Discover tests + test_files = discover_tests(build_dir, args.test) + if not test_files: + if args.test: + print(f"Error: No test found matching '{args.test}'") + else: + print("Error: No tests found") + sys.exit(1) + + print(f"Running {len(test_files)} tests...") + if args.test: + print(f"Filter: {args.test}") + + # Determine number of parallel jobs + if os.environ.get("NO_PARALLEL"): + max_workers = 1 + print( + "NO_PARALLEL environment variable set - forcing sequential test execution" + ) + elif args.jobs: + max_workers = args.jobs + else: + import multiprocessing + + max_workers = max(1, multiprocessing.cpu_count() - 1) + + # Run tests in parallel + start_time = time.time() + results: List[TestResult] = [] + failed_tests: List[TestResult] = [] + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_test = { + executor.submit(run_test, test_file, args.verbose): test_file + for test_file in test_files + } + + completed = 0 + for future in as_completed(future_to_test): + test_file = future_to_test[future] + completed += 1 + try: + result = future.result() + results.append(result) + + # Show progress + status = "PASS" if result.success else "FAIL" + print( + f"[{completed}/{len(test_files)}] {status} {result.name} ({result.duration:.2f}s)" + ) + + # Show output for failures or in verbose mode + if not result.success or args.verbose: + if result.output: + print(result.output) + + if not result.success: + failed_tests.append(result) + + # Early abort after 3 failures + if len(failed_tests) >= 3: + print( + "Reached failure threshold (3). Aborting remaining unit tests." + ) + # Cancel remaining futures and stop accepting new work + try: + executor.shutdown(wait=False, cancel_futures=True) + except TypeError: + # Python < 3.9: cancel_futures not available + pass + break + + except Exception as e: + print(f"Error running {test_file.name}: {e}") + failed_tests.append( + TestResult( + name=test_file.stem, + success=False, + duration=0.0, + output=str(e), + ) + ) + if len(failed_tests) >= 3: + print( + "Reached failure threshold (3) due to errors. Aborting remaining unit tests." + ) + try: + executor.shutdown(wait=False, cancel_futures=True) + except TypeError: + pass + break + + # Show summary + total_duration = time.time() - start_time + success_count = len(results) - len(failed_tests) + + print("\nTest Summary:") + print(f"Total tests: {len(results)}") + print(f"Passed: {success_count}") + print(f"Failed: {len(failed_tests)}") + print(f"Total time: {total_duration:.2f}s") + + if failed_tests: + print("\nFailed tests:") + failures: list[TestFailureInfo] = [] + for test in failed_tests: + print(f" {test.name}") + failures.append( + TestFailureInfo( + test_name=test.name, + command=f"test_{test.name}", + return_code=1, # Failed tests get return code 1 + output=test.output, + error_type="test_execution_failure", + ) + ) + + raise TestExecutionFailedException( + f"{len(failed_tests)} test(s) failed", failures + ) + + except TestExecutionFailedException as e: + # Print detailed failure information + print("\n" + "=" * 60) + print("FASTLED TEST EXECUTION FAILURE DETAILS") + print("=" * 60) + print(e.get_detailed_failure_info()) + print("=" * 60) + + # Exit with appropriate code + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/setup-js-linting-fast.py b/v1.5/lib/FastLED/ci/setup-js-linting-fast.py new file mode 100644 index 0000000..bb95b69 --- /dev/null +++ b/v1.5/lib/FastLED/ci/setup-js-linting-fast.py @@ -0,0 +1,284 @@ +#!/usr/bin/env -S uv run --script + + +""" +Fast JavaScript linting setup for FastLED using Node.js + ESLint +Downloads Node.js binary and sets up fast linting - much faster than Deno +Uses uv run --script for dynamic package management +""" + +import io +import json +import os +import platform +import shutil +import subprocess +import sys +import tarfile +import zipfile +from pathlib import Path + +# Import httpx for HTTP requests (dynamically managed by uv) +import httpx + + +# Force UTF-8 output for Windows consoles +if sys.platform.startswith("win"): + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + +# Configuration +NODE_VERSION = "20.11.0" # LTS version +TOOLS_DIR = Path(".cache/js-tools") +NODE_DIR = TOOLS_DIR / "node" + + +def get_node_download_info(): + """Get Node.js download URL and filename based on platform""" + system = platform.system().lower() + machine = platform.machine().lower() + + # Map architecture names + if machine in ["x86_64", "amd64"]: + arch = "x64" + elif machine in ["aarch64", "arm64"]: + arch = "arm64" + else: + raise ValueError(f"Unsupported architecture: {machine}") + + if system == "windows": + filename = f"node-v{NODE_VERSION}-win-{arch}.zip" + is_zip = True + elif system == "darwin": + filename = f"node-v{NODE_VERSION}-darwin-{arch}.tar.gz" + is_zip = False + elif system == "linux": + filename = f"node-v{NODE_VERSION}-linux-{arch}.tar.xz" + is_zip = False + else: + raise ValueError(f"Unsupported platform: {system}") + + url = f"https://nodejs.org/dist/v{NODE_VERSION}/{filename}" + return url, filename, is_zip + + +def download_and_extract_node(): + """Download and extract Node.js binary""" + url, filename, is_zip = get_node_download_info() + download_path = TOOLS_DIR / filename + + # Get architecture for extraction path + machine = platform.machine().lower() + if machine in ["x86_64", "amd64"]: + arch = "x64" + elif machine in ["aarch64", "arm64"]: + arch = "arm64" + else: + raise ValueError(f"Unsupported architecture: {machine}") + + print(f"Downloading Node.js v{NODE_VERSION}...") + TOOLS_DIR.mkdir(exist_ok=True) + NODE_DIR.mkdir(exist_ok=True) + + if not download_path.exists(): + print(f"Downloading from: {url}") + with httpx.stream("GET", url, follow_redirects=True) as response: + response.raise_for_status() + with open(download_path, "wb") as f: + for chunk in response.iter_bytes(chunk_size=8192): + f.write(chunk) + print(f"SUCCESS: Downloaded {filename}") + + # Extract Node.js + if platform.system() == "Windows": + node_exe = NODE_DIR / "node.exe" + else: + node_exe = NODE_DIR / "bin" / "node" + + if not node_exe.exists(): + print("Extracting Node.js...") + + if is_zip: + with zipfile.ZipFile(download_path, "r") as zip_ref: + zip_ref.extractall(NODE_DIR) + # Move contents from nested folder to NODE_DIR + nested_dir = NODE_DIR / f"node-v{NODE_VERSION}-win-{arch}" + if nested_dir.exists(): + for item in nested_dir.iterdir(): + if item.is_dir(): + # For directories, move contents recursively + target_dir = NODE_DIR / item.name + if target_dir.exists(): + shutil.rmtree(target_dir) + shutil.move(str(item), str(target_dir)) + else: + # For files, move directly + target_file = NODE_DIR / item.name + if target_file.exists(): + target_file.unlink() + item.rename(target_file) + nested_dir.rmdir() + else: + with tarfile.open(download_path, "r:*") as tar_ref: + tar_ref.extractall(NODE_DIR) + # Move contents from nested folder to NODE_DIR + nested_dir = ( + NODE_DIR / f"node-v{NODE_VERSION}-{platform.system().lower()}-x64" + ) + if nested_dir.exists(): + for item in nested_dir.iterdir(): + item.rename(NODE_DIR / item.name) + nested_dir.rmdir() + + print("SUCCESS: Node.js extracted") + + # Clean up download file + if download_path.exists(): + download_path.unlink() + + return node_exe + + +def setup_eslint(): + """Install ESLint and create configuration""" + if platform.system() == "Windows": + node_exe = NODE_DIR / "node.exe" + npm_exe = NODE_DIR / "npm.cmd" + else: + node_exe = NODE_DIR / "bin" / "node" + npm_exe = NODE_DIR / "bin" / "npm" + + print("Installing ESLint...") + + # Create package.json + package_json = { + "name": "fastled-js-linting", + "version": "1.0.0", + "private": True, + "dependencies": {"eslint": "^8.56.0"}, + } + + with open(TOOLS_DIR / "package.json", "w") as f: + json.dump(package_json, f, indent=2) + + # Install ESLint + try: + # Use absolute path for npm executable + npm_exe_abs = ( + (TOOLS_DIR / "node" / "npm.cmd").resolve() + if platform.system() == "Windows" + else (TOOLS_DIR / "node" / "bin" / "npm").resolve() + ) + + # On Windows, use shell=True to properly execute .cmd files + if platform.system() == "Windows": + result = subprocess.run( + [str(npm_exe_abs), "install"], + cwd=TOOLS_DIR, + check=True, + capture_output=True, + text=True, + shell=True, + ) + else: + result = subprocess.run( + [str(npm_exe_abs), "install"], + cwd=TOOLS_DIR, + check=True, + capture_output=True, + text=True, + ) + # npm install succeeded + except subprocess.CalledProcessError as e: + # Show stderr output for debugging + print(f"npm install stderr: {e.stderr}") + print(f"npm install stdout: {e.stdout}") + raise + + # ESLint config is now in ci/.eslintrc.js (tracked in git) + # No need to create it here since it's already in version control + + print("SUCCESS: ESLint configured") + + +def create_fast_lint_script(): + """Create fast linting script""" + if platform.system() == "Windows": + node_exe = NODE_DIR / "node.exe" + eslint_exe = TOOLS_DIR / "node_modules" / ".bin" / "eslint.cmd" + else: + node_exe = NODE_DIR / "bin" / "node" + eslint_exe = TOOLS_DIR / "node_modules" / ".bin" / "eslint" + + script_content = f"""#!/bin/bash +# FastLED JavaScript Linting Script (Node.js + ESLint - FAST!) + +# Colors for output +RED='\\033[0;31m' +GREEN='\\033[0;32m' +YELLOW='\\033[1;33m' +BLUE='\\033[0;34m' +NC='\\033[0m' # No Color + +echo -e "${{BLUE}}FAST FastLED JavaScript Linting (Node.js + ESLint - FAST!)${{NC}}" + +# Check if ESLint is installed +if [ ! -f ".cache/js-tools/node_modules/.bin/eslint{".cmd" if platform.system() == "Windows" else ""}" ]; then + echo -e "${{RED}}ERROR: ESLint not found. Run: uv run ci/setup-js-linting-fast.py${{NC}}" + exit 1 +fi + +# Find JavaScript files in WASM platform +JS_FILES=$(find src/platforms/wasm -name "*.js" -type f 2>/dev/null) + +if [ -z "$JS_FILES" ]; then + echo -e "${{YELLOW}}WARNING: No JavaScript files found in src/platforms/wasm/${{NC}}" + exit 0 +fi + +echo -e "${{BLUE}}Found JavaScript files:${{NC}}" +echo "$JS_FILES" | sed 's/^/ /' + +# Run ESLint +echo -e "${{BLUE}}Running ESLint...${{NC}}" +cd .cache/js-tools +if "./node_modules/.bin/eslint{".cmd" if platform.system() == "Windows" else ""}" --no-eslintrc --no-inline-config -c .eslintrc.js ../../src/platforms/wasm/compiler/*.js ../../src/platforms/wasm/compiler/modules/*.js; then + echo -e "${{GREEN}}SUCCESS: JavaScript linting completed successfully${{NC}}" +else + echo -e "${{RED}}ERROR: JavaScript linting failed${{NC}}" + exit 1 +fi +""" + + # Lint script is now in ci/lint-js-fast (tracked in git) + # No need to create it here since it's already in version control + script_path = Path("ci/lint-js-fast") + # Script already exists in ci/ and is tracked in git + # Just ensure it's executable + if platform.system() != "Windows" and script_path.exists(): + os.chmod(script_path, 0o755) + + print("SUCCESS: Fast lint script created") + + +def main(): + """Main setup function""" + print("Setting up fast JavaScript linting (Node.js + ESLint)...") + + try: + node_exe = download_and_extract_node() + setup_eslint() + create_fast_lint_script() + + print("\\nFast JavaScript linting setup complete!") + print("\\nUsage:") + print(" bash ci/lint-js-fast # Fast linting with ESLint") + print(" For more info: ci/js/README.md") + + except Exception as e: + print(f"Setup failed: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/setup-llvm.py b/v1.5/lib/FastLED/ci/setup-llvm.py new file mode 100644 index 0000000..c1f3dbd --- /dev/null +++ b/v1.5/lib/FastLED/ci/setup-llvm.py @@ -0,0 +1,969 @@ +#!/usr/bin/env python3 + +import _thread +import atexit +import json +import lzma +import os +import platform +import shutil +import subprocess +import sys +import tarfile +import tempfile +import urllib.error +import urllib.request +import zipfile +from pathlib import Path +from typing import Any, Dict, List, Optional, Protocol, Tuple, cast + +from ci.util.resumable_downloader import ResumableDownloader + + +class _SysConf(Protocol): + def short_os_name_and_version(self) -> str: ... + + architecture: str + + +import sys_detection +from llvm_installer import LlvmInstaller + + +_HERE = Path(__file__).parent +_PROJECT_ROOT = _HERE.parent + +# Global download directory - lazily initialized +_download_dir: Optional[Path] = None + + +def _get_download_dir() -> Path: + """Get or create the download directory lazily.""" + global _download_dir + if _download_dir is None: + _download_dir = _PROJECT_ROOT / ".cache" / "downloads" + _download_dir.mkdir(parents=True, exist_ok=True) + print(f"Created download directory: {_download_dir}") + # Register cleanup on exit as fallback + atexit.register(_cleanup_download_dir) + return _download_dir + + +def _cleanup_download_dir() -> None: + """Clean up the download directory.""" + global _download_dir + if _download_dir and _download_dir.exists(): + try: + shutil.rmtree(_download_dir) + print(f"Cleaned up download directory: {_download_dir}") + except Exception as e: + print( + f"Warning: Failed to clean up download directory {_download_dir}: {e}" + ) + _download_dir = None + + +def _download(url: str, filename: str) -> Path: + """Download to the managed download directory. + + Args: + url: URL to download + filename: Filename to save as (not full path) + + Returns: + Path to the downloaded file + """ + download_dir = _get_download_dir() + file_path = download_dir / filename + + print(f"download {url} to {file_path}") + + # Use resumable downloader for large files + downloader = ResumableDownloader(chunk_size=1024 * 1024) # 1MB chunks + + try: + downloader.download(url, file_path) + except Exception as e: + # Print download failure banner + url_truncated = url[:100] + banner_width = max(35, len(url_truncated) + 6) + banner_line = "#" * banner_width + print(f"\n{banner_line}") + print( + f"# DOWNLOAD FAILED FOR {url_truncated}{'...' if len(url) > 100 else ''} #" + ) + print(f"{banner_line}") + print(f"Error: {e}") + print() + raise + + return file_path + + +def is_linux_or_unix() -> Tuple[bool, str]: + """Check if running on Linux or Unix-like system.""" + system = platform.system().lower() + if system == "linux": + return True, "linux" + elif system in ["darwin", "freebsd", "openbsd", "netbsd"]: + return True, system + else: + return False, system + + +def get_tool_version(tool_name: str) -> Optional[str]: + """Get version of a tool if it exists on system PATH.""" + try: + result = subprocess.run( + [tool_name, "--version"], capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + return result.stdout.strip() + return None + except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): + return None + + +def get_clang_version_number(version_output: str) -> Optional[int]: + """Extract major version number from clang version output.""" + try: + # Look for "clang version X.Y.Z" pattern + import re + + match = re.search(r"clang version (\d+)\.", version_output) + if match: + return int(match.group(1)) + return None + except (ValueError, AttributeError): + return None + + +def find_tool_path(tool_name: str) -> Optional[Path]: + """Find the full path to a tool if it exists.""" + tool_path = shutil.which(tool_name) + return Path(tool_path) if tool_path else None + + +def check_required_tools() -> Tuple[ + Dict[str, Optional[Path]], Dict[str, Optional[Path]] +]: + """Check for essential and extra LLVM/Clang tools on system PATH. + + On Windows, prefer lld-link as the linker front-end. + """ + system_name = platform.system().lower() + + essential_tools: List[str] = [ + "clang", + "clang++", + "llvm-ar", + "llvm-nm", + "llvm-objdump", + "llvm-addr2line", + "llvm-strip", + "llvm-objcopy", + ] + + # Linker tools differ across platforms + if system_name == "windows": + essential_tools.append("lld-link") + else: + essential_tools.append("lld") + + # Useful developer tools (not strictly required for building) + extra_tools: List[str] = [ + "clangd", + "clang-format", + "clang-tidy", + "lldb", + ] + + found_essential: Dict[str, Optional[Path]] = {} + for tool in essential_tools: + found_essential[tool] = find_tool_path(tool) + + found_extras: Dict[str, Optional[Path]] = {} + for tool in extra_tools: + found_extras[tool] = find_tool_path(tool) + + return found_essential, found_extras + + +def create_hard_links(source_tools: Dict[str, Path], target_dir: Path) -> None: + """Create hard links from system tools to target directory.""" + bin_dir = target_dir / "bin" + bin_dir.mkdir(parents=True, exist_ok=True) + + linked_count: int = 0 + copied_count: int = 0 + failed_tools: list[str] = [] + + for tool_name, source_path in source_tools.items(): + if source_path and source_path.exists(): + # Preserve original filename to keep extensions on Windows (e.g., .exe) + target_filename = source_path.name + target_path = bin_dir / target_filename + try: + # Remove existing file if it exists + if target_path.exists(): + target_path.unlink() + + # Create hard link + target_path.hardlink_to(source_path) + print(f"[OK] Linked {tool_name}: {source_path} -> {target_path}") + linked_count += 1 + except (OSError, PermissionError) as e: + print(f"[WARN] Failed to link {tool_name}: {e}") + # Fallback to copy if hard link fails + try: + shutil.copy2(source_path, target_path) + target_path.chmod(0o755) + print(f"[OK] Copied {tool_name}: {source_path} -> {target_path}") + copied_count += 1 + except (OSError, PermissionError): + print(f"[ERROR] Failed to copy {tool_name}") + failed_tools.append(tool_name) + + total_prepared: int = linked_count + copied_count + print( + f"Summary: prepared {total_prepared} tools (linked: {linked_count}, copied: {copied_count})" + ) + if failed_tools: + print(f"ERROR: Failed to mirror tools: {', '.join(failed_tools)}") + + +def find_tools_in_directories( + tool_names: List[str], directories: List[Path] +) -> Dict[str, Optional[Path]]: + """Search for tools across a list of directories, returning first matches. + + On Windows, will match executables with .exe suffix. + """ + found: Dict[str, Optional[Path]] = {} + for tool in tool_names: + found[tool] = None + + for directory in directories: + if not directory.exists(): + continue + for tool in tool_names: + if found[tool] is not None: + continue + candidate: Path = directory / tool + if candidate.exists(): + found[tool] = candidate + continue + # Try platform-specific suffix + if platform.system().lower() == "windows": + exe_candidate: Path = directory / f"{tool}.exe" + if exe_candidate.exists(): + found[tool] = exe_candidate + return found + + +def get_breadcrumb_file(target_dir: Path) -> Path: + """Get the path to the installation completion breadcrumb file.""" + cache_dir = target_dir / ".cache" / "cc" + cache_dir.mkdir(parents=True, exist_ok=True) + return cache_dir / "done.txt" + + +def create_breadcrumb(target_dir: Path) -> None: + """Create breadcrumb file to mark successful installation.""" + breadcrumb = get_breadcrumb_file(target_dir) + breadcrumb.write_text(f"Installation completed at {os.path.basename(target_dir)}\n") + print(f"Created installation breadcrumb: {breadcrumb}") + + +def check_existing_installation(target_dir: Path) -> Tuple[bool, bool]: + """Check if there's an existing installation and if it's complete. + + Returns: + (has_artifacts, has_breadcrumb) - bool tuple indicating installation state + """ + breadcrumb = get_breadcrumb_file(target_dir) + has_breadcrumb = breadcrumb.exists() + + # Check for any installation artifacts + bin_dir = target_dir / "bin" + has_artifacts = bin_dir.exists() and any(bin_dir.iterdir()) + + return has_artifacts, has_breadcrumb + + +def verify_clang_installation(target_dir: Path) -> bool: + """Test if clang is properly installed in target directory.""" + bin_dir = target_dir / "bin" + clang_exe = ( + bin_dir / "clang.exe" + if platform.system().lower() == "windows" + else bin_dir / "clang" + ) + + if not clang_exe.exists(): + return False + + # Test that clang can run and report version + try: + result = subprocess.run( + [str(clang_exe), "--version"], capture_output=True, text=True, timeout=5 + ) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): + return False + + +def get_windows_default_llvm_bins() -> List[Path]: + """Return common Windows LLVM bin directories to probe.""" + roots: List[Path] = [] + program_files: Optional[str] = os.environ.get("ProgramFiles") + program_files_x86: Optional[str] = os.environ.get("ProgramFiles(x86)") + chocolatey_lib: Optional[str] = os.environ.get("ChocolateyInstall") + scoop: Optional[str] = os.environ.get("SCOOP") + + if program_files: + roots.append(Path(program_files) / "LLVM" / "bin") + if program_files_x86: + roots.append(Path(program_files_x86) / "LLVM" / "bin") + if chocolatey_lib: + roots.append(Path(chocolatey_lib) / "lib" / "llvm" / "tools" / "llvm" / "bin") + if scoop: + roots.append(Path(scoop) / "apps" / "llvm" / "current" / "bin") + + # Also consider PATH entries explicitly + path_entries: List[str] = os.environ.get("PATH", "").split(os.pathsep) + for entry in path_entries: + # Only consider entries that look like LLVM bin directories + if "llvm" in entry.lower() and "bin" in entry.lower(): + roots.append(Path(entry)) + return roots + + +def _run_subprocess(args: List[str], timeout_seconds: int) -> tuple[int, str, str]: + """Run a subprocess command and return (returncode, stdout, stderr).""" + try: + result = subprocess.run( + args, + capture_output=True, + text=True, + timeout=timeout_seconds, + ) + return result.returncode, result.stdout, result.stderr + except subprocess.TimeoutExpired as e: + return 124, "", f"Timeout after {timeout_seconds}s: {e}" + except subprocess.SubprocessError as e: + return 1, "", f"Subprocess error: {e}" + + +def install_llvm_windows_direct(target_dir: Path) -> Path: + """Download and extract official LLVM Windows tar.xz archive into target_dir. + + Returns the extracted LLVM bin directory path on success. + Fails fast with a descriptive error on failure. + """ + # Prefer dynamically discovered releases; as fallback, try a small curated list + preferred_versions: List[str] = [ + "20.1.8", + "20.1.7", + "20.1.6", + "19.1.8", + "19.1.7", + "19.1.6", + ] + + download_errors: List[str] = [] + + # First, try to discover the latest available Windows tar.xz for majors 20 and 19 + for probe_major in [20, 19]: + print(f"Attempting discovery for LLVM major version {probe_major}...") + discovered_url = discover_latest_github_llvm_win_zip(probe_major) + if discovered_url is not None: + print(f"Discovered URL for LLVM {probe_major}: {discovered_url}") + # Put discovered URL at the front of attempts + target_dir.mkdir(parents=True, exist_ok=True) + extract_root: Path = target_dir + + tmp_archive_path: Optional[Path] = None + try: + print(f"Attempting to download and extract LLVM {probe_major}...") + # Download to managed directory + tmp_archive_path = _download( + discovered_url, f"llvm-discovered-{probe_major}.tar.xz" + ) + extract_root = target_dir + with lzma.open(tmp_archive_path, "rb") as xz_file: + with tarfile.open(fileobj=xz_file, mode="r") as tar: + tar.extractall(extract_root, filter="data") + except KeyboardInterrupt: + print("Operation interrupted by user") + _thread.interrupt_main() + raise + except Exception as e: + print(f"Failed to download/extract LLVM {probe_major}: {e}") + download_errors.append(f"discovered-{probe_major}: {e}") + finally: + # Clean up the downloaded file immediately + if tmp_archive_path and tmp_archive_path.exists(): + try: + tmp_archive_path.unlink() + print(f"Cleaned up: {tmp_archive_path}") + except Exception as e: + print(f"Warning: Failed to clean up {tmp_archive_path}: {e}") + + extracted_bin: Optional[Path] = None + for entry in extract_root.iterdir(): + if not entry.is_dir(): + continue + candidate = entry / "bin" / "clang.exe" + if candidate.exists(): + extracted_bin = entry / "bin" + break + + if extracted_bin is not None: + print( + f"SUCCESS: LLVM {probe_major} installed successfully via discovery!" + ) + print("OK: Successfully installed LLVM (Windows tar.xz, discovered)") + print(f" Installation directory: {extracted_bin.parent}") + print(f" Bin directory: {extracted_bin}") + create_breadcrumb(target_dir) + return extracted_bin + else: + print( + f"WARNING: LLVM {probe_major} downloaded but clang.exe not found in expected location" + ) + else: + print(f"No URL discovered for LLVM major version {probe_major}") + + # If discovery did not succeed, try curated versions list + print("\n" + "=" * 60) + print("Discovery failed for all major versions. Falling back to curated list...") + print("=" * 60 + "\n") + + for version in preferred_versions: + base_url = ( + "https://github.com/llvm/llvm-project/releases/download/" + f"llvmorg-{version}/clang+llvm-{version}-x86_64-pc-windows-msvc.tar.xz" + ) + print(f"Attempting direct LLVM download for Windows: {base_url}") + try: + target_dir.mkdir(parents=True, exist_ok=True) + + tmp_archive_path: Optional[Path] = None + try: + # Download to managed directory + tmp_archive_path = _download(base_url, f"llvm-{version}.tar.xz") + except KeyboardInterrupt: + print("Operation interrupted by user") + _thread.interrupt_main() + raise + except Exception as e: + download_errors.append(f"{version}: {e}") + # Clean up on download error + if tmp_archive_path and tmp_archive_path.exists(): + try: + tmp_archive_path.unlink() + print(f"Cleaned up after error: {tmp_archive_path}") + except Exception: + pass + continue + + extract_root = target_dir + try: + with lzma.open(tmp_archive_path, "rb") as xz_file: + with tarfile.open(fileobj=xz_file, mode="r") as tar: + tar.extractall(extract_root, filter="data") + finally: + # Clean up the downloaded file immediately + if tmp_archive_path and tmp_archive_path.exists(): + try: + tmp_archive_path.unlink() + print(f"Cleaned up: {tmp_archive_path}") + except Exception as e: + print(f"Warning: Failed to clean up {tmp_archive_path}: {e}") + + # Find extracted directory that contains bin/clang.exe + extracted_bin: Optional[Path] = None + for entry in extract_root.iterdir(): + if not entry.is_dir(): + continue + candidate = entry / "bin" / "clang.exe" + if candidate.exists(): + extracted_bin = entry / "bin" + break + + if extracted_bin is None: + print( + "ERROR: LLVM downloaded but clang.exe not found in expected location" + ) + # Try next version + continue + + print("OK: Successfully installed LLVM (Windows tar.xz)") + print(f" Installation directory: {extracted_bin.parent}") + print(f" Bin directory: {extracted_bin}") + create_breadcrumb(target_dir) + return extracted_bin + + except KeyboardInterrupt: + print("Operation interrupted by user") + _thread.interrupt_main() + raise + except Exception as e: + download_errors.append(f"{version}: {e}") + continue + + # If we reach here, all attempts failed + print("CRITICAL: Failed to download LLVM for Windows from official releases.") + if download_errors: + for err in download_errors: + print(f" - {err}") + print("Please ensure internet access is available and retry.") + sys.exit(1) + + +def discover_latest_github_llvm_win_zip(major: int) -> Optional[str]: + """Find the latest LLVM Windows tar.xz asset for a given major version via GitHub API. + + Returns a browser_download_url string if found, otherwise None. + """ + try: + per_page: int = 50 + page: int = 1 + max_pages: int = 3 + while page <= max_pages: + api_url: str = ( + "https://api.github.com/repos/llvm/llvm-project/releases" + f"?per_page={per_page}&page={page}" + ) + req = urllib.request.Request( + api_url, headers={"User-Agent": "fastled-setup-llvm"} + ) + with urllib.request.urlopen(req, timeout=20) as resp: + raw: str = resp.read().decode("utf-8") + releases_any: Any = json.loads(raw) + releases: list[dict[str, Any]] = [] + if isinstance(releases_any, list): + tmp_list: list[Any] = cast(list[Any], releases_any) + filtered: list[dict[str, Any]] = [] + k: int = 0 + while k < len(tmp_list): + item: Any = tmp_list[k] + k += 1 + if isinstance(item, dict): + filtered.append(cast(dict[str, Any], item)) + releases = filtered + + # Iterate releases + idx: int = 0 + while idx < len(releases): + rel: dict[str, Any] = releases[idx] + idx += 1 + tag_val: Any = rel.get("tag_name") + tag_name: Optional[str] = tag_val if isinstance(tag_val, str) else None + if tag_name is None: + continue + # Expect tags like llvmorg-19.1.6 + prefix: str = f"llvmorg-{major}." + if not tag_name.startswith(prefix): + continue + assets_val: Any = rel.get("assets") + assets: list[dict[str, Any]] = ( + cast(list[dict[str, Any]], assets_val) + if isinstance(assets_val, list) + else [] + ) + filtered_assets: list[dict[str, Any]] = [ + a for a in assets if isinstance(a, dict) + ] + j: int = 0 + while j < len(filtered_assets): + asset: dict[str, Any] = filtered_assets[j] + j += 1 + name_val: Any = asset.get("name") + url_val: Any = asset.get("browser_download_url") + name: Optional[str] = ( + name_val if isinstance(name_val, str) else None + ) + url: Optional[str] = url_val if isinstance(url_val, str) else None + if name is not None and url is not None: + # Match e.g., clang+llvm-19.1.6-x86_64-pc-windows-msvc.tar.xz + if ( + name.startswith(f"clang+llvm-{major}.") + and "x86_64-pc-windows-msvc.tar.xz" in name + ): + return url + page += 1 + return None + except urllib.error.URLError: + return None + + +def download_and_install_llvm(target_dir: Path) -> None: + """Download and install LLVM using llvm-installer package.""" + print("Detecting system configuration...") + try: + sys_conf_obj2: Any = sys_detection.local_sys_conf() + local_conf2: _SysConf = cast(_SysConf, sys_conf_obj2) + detected_os: str = local_conf2.short_os_name_and_version() + architecture: str = local_conf2.architecture + print(f"Detected OS: {detected_os}") + print(f"Architecture: {architecture}") + + # Use Ubuntu 24.04 as fallback since it's known to work + # This package seems to be from YugabyteDB and has limited OS support + working_os: str = "ubuntu24.04" + if detected_os.startswith("ubuntu24") or detected_os.startswith("ubuntu22"): + working_os = detected_os + + print(f"Using OS variant: {working_os}") + + llvm_installer: Any = LlvmInstaller( + short_os_name_and_version=working_os, architecture=architecture + ) + + # Prefer most recent supported major release for linux tarball path + llvm_url: str = "" + selected_version: Optional[int] = None + for version in [20, 19]: + try: + print(f"Installing LLVM version {version}...") + llvm_url = llvm_installer.get_llvm_url(major_llvm_version=version) + print(f"Found LLVM {version} at: {llvm_url}") + selected_version = version + break + except KeyboardInterrupt: + print("Operation interrupted by user") + _thread.interrupt_main() + raise + except Exception as e: + print(f"WARN: Could not resolve LLVM {version}: {e}") + + if not llvm_url: + raise RuntimeError( + "Could not resolve a suitable LLVM download URL for Linux" + ) + + # Download and extract + print("Downloading LLVM package...") + + # Download to managed directory + tmp_archive_path: Path = _download( + llvm_url, f"llvm-linux-{selected_version or 'unknown'}.tar.gz" + ) + + try: + print("Extracting LLVM package...") + with tarfile.open(tmp_archive_path, "r:gz") as tar: + tar.extractall(target_dir, filter="data") + finally: + # Clean up immediately after extraction + try: + tmp_archive_path.unlink() + print(f"Cleaned up: {tmp_archive_path}") + except Exception as e: + print(f"Warning: Failed to clean up {tmp_archive_path}: {e}") + + # Find the extracted directory and verify clang exists + for extracted_dir in target_dir.iterdir(): + if extracted_dir.is_dir(): + clang_path = extracted_dir / "bin" / "clang" + + if clang_path.exists(): + print( + f"OK: Successfully installed LLVM {selected_version if selected_version is not None else 'unknown'}" + ) + print(f" Installation directory: {extracted_dir}") + print(f" Clang binary: {clang_path}") + + # List available tools (first few) and provide PATH guidance + bin_dir = extracted_dir / "bin" + if bin_dir.exists(): + tools = [ + f.name + for f in bin_dir.iterdir() + if f.is_file() and not f.name.endswith(".dll") + ] + print( + f" Available tools: {', '.join(sorted(tools[:10]))}{'...' if len(tools) > 10 else ''}" + ) + print("") + print("Next steps:") + print(f" - Add to PATH: {bin_dir}") + print(" - Example:") + print(f' export PATH="{bin_dir}:$PATH"') + print(" - Verify:") + print(" clang --version") + + return + + print("ERROR: LLVM downloaded but clang not found in expected location") + sys.exit(1) + + except KeyboardInterrupt: + print("Operation interrupted by user") + _thread.interrupt_main() + raise + except Exception as e: + print(f"Error during installation: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +def main() -> None: + """Setup LLVM toolchain - check system first, fallback to package installation.""" + # Get target directory from command line (optional) + if len(sys.argv) > 2: + print("Usage: uv run python ci/setup-llvm.py [target_directory]") + print(" target_directory: Optional. Defaults to .cache/cc in project root") + sys.exit(1) + + if len(sys.argv) == 2: + target_dir = Path(sys.argv[1]) + else: + # Default to project root .cache/cc directory + target_dir = _PROJECT_ROOT / ".cache" / "cc" + print(f"Using default target directory: {target_dir}") + + target_dir.mkdir(parents=True, exist_ok=True) + + # Check platform compatibility + is_unix_like, platform_name = is_linux_or_unix() + + if platform_name == "windows": + print("Checking for existing LLVM/Clang tools on system (Windows)...") + + # Check for existing installation in target directory + has_artifacts, has_breadcrumb = check_existing_installation(target_dir) + + if has_artifacts and not has_breadcrumb: + print("WARN: Found installation artifacts without completion marker") + print(" This suggests an incomplete or corrupted installation") + print(" Re-downloading LLVM to ensure proper installation...") + elif has_artifacts and has_breadcrumb: + if verify_clang_installation(target_dir): + print("OK: Valid LLVM installation found in target directory") + print(" Skipping download - clang installation verified") + return + else: + print( + "WARN: Installation breadcrumb exists but clang verification failed" + ) + print(" Re-downloading LLVM to fix installation...") + + clang_version_output = get_tool_version("clang") + if clang_version_output: + clang_version = get_clang_version_number(clang_version_output) + print( + f"Found system clang: version {clang_version if clang_version else 'unknown'}" + ) + else: + clang_version = None + + found_essential, found_extras = check_required_tools() + available_essential = { + name: path for name, path in found_essential.items() if path + } + missing_essential = [name for name, path in found_essential.items() if not path] + + if len(available_essential) != len(found_essential): + # Probe common install directories (e.g., C:\Program Files\LLVM\bin) + probe_dirs = get_windows_default_llvm_bins() + print( + f"Probing default Windows LLVM locations: {', '.join(str(p) for p in probe_dirs)}" + ) + probed = find_tools_in_directories(list(found_essential.keys()), probe_dirs) + for name, p in probed.items(): + if p is not None and found_essential.get(name) is None: + found_essential[name] = p + + available_essential = { + name: path for name, path in found_essential.items() if path + } + missing_essential = [ + name for name, path in found_essential.items() if not path + ] + + if len(available_essential) == len(found_essential) and ( + clang_version is None or clang_version < 19 + ): + print( + "Note: Detected toolchain but clang < 19. Continuing; ensure compatibility with your build settings." + ) + + # On Windows, require core tools including llvm-addr2line for stack traces + core_required = ["clang", "clang++", "lld-link", "llvm-addr2line"] + missing_core = [name for name in core_required if not found_essential.get(name)] + + if not missing_core: + print("OK: Core tools present - proceeding with available tools") + all_available_tools: Dict[str, Path] = {} + for name, path in found_essential.items(): + if path is not None: + all_available_tools[name] = cast(Path, path) + for name, path in found_extras.items(): + if path is not None: + all_available_tools[name] = cast(Path, path) + + print(f"Creating hard links in {target_dir}...") + create_hard_links(all_available_tools, target_dir) + + version_file = target_dir / "VERSION" + version_file.write_text( + f"system-clang-{clang_version if clang_version else 'unknown'}\n" + ) + bin_dir = target_dir / "bin" + existing = [p for p in bin_dir.iterdir() if p.is_file()] + print(f"OK: Tools available in {bin_dir}: {len(existing)} files") + create_breadcrumb(target_dir) + + # Warn about missing non-core tools + missing_noncore = [ + name + for name in list(found_essential.keys()) + if name not in core_required and not found_essential.get(name) + ] + if missing_noncore: + print(f"WARN: Missing non-core tools: {', '.join(missing_noncore)}") + missing_extras = [name for name, path in found_extras.items() if not path] + if missing_extras: + print(f"WARN: Missing extra tools: {', '.join(missing_extras)}") + return + + # Try to auto-install on Windows via direct GitHub download + print(f"ERROR: Missing core tools: {', '.join(missing_core)}") + print("Downloading LLVM for Windows from GitHub releases...") + extracted_bin_dir = install_llvm_windows_direct(target_dir) + + # Collect tools from the extracted bin directory and link/copy them + tool_names: List[str] = [ + "clang", + "clang++", + "lld-link", + "llvm-addr2line", + "llvm-ar", + "llvm-nm", + "llvm-objdump", + "llvm-strip", + "llvm-objcopy", + "clangd", + "clang-format", + "clang-tidy", + "lldb", + ] + discovered: Dict[str, Optional[Path]] = find_tools_in_directories( + tool_names, [extracted_bin_dir] + ) + available_tools: Dict[str, Path] = {} + for name, p in discovered.items(): + if p is not None: + available_tools[name] = cast(Path, p) + + if not available_tools: + print( + "CRITICAL: Extracted LLVM bin directory does not contain expected tools." + ) + sys.exit(1) + + print(f"Creating hard links in {target_dir}...") + create_hard_links(available_tools, target_dir) + + version_file = target_dir / "VERSION" + version_file.write_text("downloaded-llvm-windows\n") + bin_dir = target_dir / "bin" + existing = [p for p in bin_dir.iterdir() if p.is_file()] + print(f"OK: Tools available in {bin_dir}: {len(existing)} files") + create_breadcrumb(target_dir) + return + + print("Checking for existing LLVM/Clang tools on system...") + + # Check if clang is available and get its version + clang_version_output = get_tool_version("clang") + if clang_version_output: + clang_version = get_clang_version_number(clang_version_output) + print( + f"Found system clang: version {clang_version if clang_version else 'unknown'}" + ) + + if clang_version and clang_version >= 19: + print("OK: System clang 19+ detected - checking tools") + + # Check for essential and extra tools + found_essential, found_extras = check_required_tools() + available_essential = { + name: path for name, path in found_essential.items() if path + } + missing_essential = [ + name for name, path in found_essential.items() if not path + ] + available_extras = { + name: path for name, path in found_extras.items() if path + } + missing_extras = [name for name, path in found_extras.items() if not path] + + # Check if all essential tools are present + if len(available_essential) == len(found_essential): + print("OK: All essential tools found - using system tools") + + # Combine available tools for linking + all_available_tools = {**available_essential, **available_extras} + + print(f"Creating hard links in {target_dir}...") + create_hard_links(all_available_tools, target_dir) + + # Create a simple version file + version_file = target_dir / "VERSION" + version_file.write_text(f"system-clang-{clang_version}\n") + + print( + f"OK: Successfully linked {len(all_available_tools)} system tools" + ) + + # Warn about missing extras in one line + if missing_extras: + print(f"WARN: Missing extra tools: {', '.join(missing_extras)}") + + return + else: + # Missing essential tools - need to install LLVM + print(f"ERROR: Missing essential tools: {', '.join(missing_essential)}") + if platform_name == "linux": + print("Installing LLVM package to provide missing tools...") + download_and_install_llvm(target_dir) + return + else: + print( + f"WARN: Cannot auto-install on {platform_name} - please install manually" + ) + return + else: + print(f"System clang version {clang_version} < 19") + if platform_name == "linux": + print("Installing LLVM 18 package...") + download_and_install_llvm(target_dir) + return + else: + print( + f"WARN: Cannot auto-install on {platform_name} - please install manually" + ) + return + else: + print("No system clang found") + if platform_name == "linux": + print("Installing LLVM package...") + download_and_install_llvm(target_dir) + else: + print( + f"WARN: Cannot auto-install on {platform_name} - please install manually" + ) + + +if __name__ == "__main__": + try: + main() + finally: + # Clean up download directory at the end + _cleanup_download_dir() diff --git a/v1.5/lib/FastLED/ci/symbol_analysis_runner.py b/v1.5/lib/FastLED/ci/symbol_analysis_runner.py new file mode 100644 index 0000000..09f0c42 --- /dev/null +++ b/v1.5/lib/FastLED/ci/symbol_analysis_runner.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Generic Symbol Analysis Runner for GitHub Actions +Wrapper script to run symbol analysis with proper error handling for any platform +""" + +import argparse +import sys +from pathlib import Path + +from ci.util.symbol_analysis import main as symbol_analysis + + +def main(): + parser = argparse.ArgumentParser( + description="Run symbol analysis for any platform with GitHub Actions support" + ) + parser.add_argument("--board", required=True, help="Board name being analyzed") + parser.add_argument("--example", default="Blink", help="Example that was compiled") + parser.add_argument( + "--skip-on-failure", + action="store_true", + help="Don't fail the build if symbol analysis fails", + ) + args = parser.parse_args() + + print(f"Symbol Analysis Runner - Board: {args.board}, Example: {args.example}") + + # Check if build_info.json exists + build_info_path = Path(".build") / args.board / "build_info.json" + if not build_info_path.exists(): + # Fall back to nested pio structure + build_info_path = Path(".build") / "pio" / args.board / "build_info.json" + + if not build_info_path.exists(): + message = ( + f"Build info not found at {build_info_path}. Skipping symbol analysis." + ) + if args.skip_on_failure: + print(f"Warning: {message}") + return 0 + else: + print(f"Error: {message}") + return 1 + + try: + # Import and run the generic symbol analysis + print("Running symbol analysis...") + + # Override sys.argv to pass the board argument to the symbol analysis script + original_argv = sys.argv + sys.argv = ["symbol_analysis.py", "--board", args.board] + + try: + symbol_analysis() + print("Symbol analysis completed successfully!") + return 0 + finally: + sys.argv = original_argv + + except ImportError as e: + print(f"Failed to import symbol_analysis module: {e}") + if args.skip_on_failure: + return 0 + return 1 + except Exception as e: + print(f"Symbol analysis failed: {e}") + if args.skip_on_failure: + return 0 + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/test_integration/test_archive_creation.py b/v1.5/lib/FastLED/ci/test_integration/test_archive_creation.py new file mode 100644 index 0000000..4683f37 --- /dev/null +++ b/v1.5/lib/FastLED/ci/test_integration/test_archive_creation.py @@ -0,0 +1,1155 @@ +#!/usr/bin/env python3 + +""" +Test suite for FastLED Archive Generation Infrastructure +Tests REAL archive creation functionality - no mocks! +""" + +import os +import stat +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from typing import List + +from ci.compiler.clang_compiler import ( + BuildFlags, + Compiler, + CompilerOptions, + LibarchiveOptions, + LinkOptions, + Result, + create_archive_sync, + detect_linker, + get_configured_archiver_command, + link_program_sync, +) +from ci.util.paths import PROJECT_ROOT + + +class TestRealArchiveCreation(unittest.TestCase): + """Test suite for REAL archive creation functionality.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = Path(tempfile.mkdtemp()) + self.project_root = PROJECT_ROOT + + # Set working directory to project root + os.chdir(self.project_root) + + def tearDown(self): + """Clean up test fixtures.""" + import shutil + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir) + + def _load_build_flags(self) -> BuildFlags: + """STRICT: Load BuildFlags explicitly - NO defaults allowed.""" + build_flags_path = Path(__file__).parent.parent / "build_unit.toml" + if not build_flags_path.exists(): + raise RuntimeError( + f"CRITICAL: build_unit.toml not found at {build_flags_path}" + ) + return BuildFlags.parse(build_flags_path) + + def test_archiver_configuration_real(self): + """Test that archiver is properly configured in TOML.""" + try: + build_flags = self._load_build_flags() + configured_cmd = get_configured_archiver_command(build_flags) + self.assertIsNotNone(configured_cmd, "Archiver must be configured in TOML") + # Explicit type assertion for type checker + assert configured_cmd is not None # Make type checker happy + self.assertTrue( + len(configured_cmd) > 0, "Archiver command must not be empty" + ) + import subprocess + + archiver_str = subprocess.list2cmdline(configured_cmd) + print(f"Configured archiver: {archiver_str}") + except RuntimeError as e: + self.fail(f"No archiver found on system: {e}") + + def test_compile_and_archive_blink(self): + """Test compiling Blink.ino and creating a real archive.""" + # Verify Blink example exists + blink_ino = self.project_root / "examples/Blink/Blink.ino" + self.assertTrue(blink_ino.exists(), f"Blink.ino not found at {blink_ino}") + + # Set up compiler with proper configuration for testing + settings = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=[ + "ARDUINO=10607", + "ARDUINO_AVR_UNO", + "F_CPU=16000000L", + "__AVR_ATmega328P__", + "ARDUINO_ARCH_AVR", + "STUB", + "FASTLED_TESTING", + ], + std_version="c++17", + compiler_args=[ + f"-I{self.project_root}/src/platforms/stub", + f"-I{self.project_root}/src/platforms/wasm/compiler", # Arduino.h emulation + ], + ) + + build_flags = self._load_build_flags() + compiler = Compiler(settings, build_flags) + + # Create C++ file from .ino + blink_cpp = self.temp_dir / "Blink.cpp" + blink_content = blink_ino.read_text() + + cpp_content = f"""#include "FastLED.h" +{blink_content} +""" + blink_cpp.write_text(cpp_content) + + # Compile to object file + blink_obj = self.temp_dir / "Blink.o" + + compile_future = compiler.compile_cpp_file( + blink_cpp, + blink_obj, + additional_flags=["-c"], # Only compile, don't link + ) + + compile_result = compile_future.result() + + # Verify compilation succeeded + self.assertTrue( + compile_result.ok, f"Compilation failed: {compile_result.stderr}" + ) + self.assertTrue(blink_obj.exists(), f"Object file not created: {blink_obj}") + + # Verify object file has reasonable size + obj_size = blink_obj.stat().st_size + self.assertGreater(obj_size, 1000, "Object file seems too small") + print(f"Object file size: {obj_size} bytes") + + # Test archive creation + archive_path = self.temp_dir / "libfastled_blink.a" + options = LibarchiveOptions(use_thin=False) + + archive_result = create_archive_sync([blink_obj], archive_path, options) + + # Verify archive creation succeeded + self.assertTrue( + archive_result.ok, f"Archive creation failed: {archive_result.stderr}" + ) + self.assertTrue( + archive_path.exists(), f"Archive file not created: {archive_path}" + ) + + # Verify archive has reasonable size + archive_size = archive_path.stat().st_size + self.assertGreater( + archive_size, obj_size, "Archive should be larger than object file" + ) + print(f"Archive size: {archive_size} bytes") + + return archive_path + + def test_thin_archive_creation(self): + """Test creating thin archives.""" + # First create an object file + archive_path = self.test_compile_and_archive_blink() + + # Get the object file used in previous test + blink_obj = self.temp_dir / "Blink.o" + self.assertTrue(blink_obj.exists()) + + # Create thin archive + thin_archive_path = self.temp_dir / "libfastled_blink_thin.a" + thin_options = LibarchiveOptions(use_thin=True) + + thin_result = create_archive_sync([blink_obj], thin_archive_path, thin_options) + + # Verify thin archive creation succeeded + self.assertTrue( + thin_result.ok, f"Thin archive creation failed: {thin_result.stderr}" + ) + self.assertTrue( + thin_archive_path.exists(), + f"Thin archive file not created: {thin_archive_path}", + ) + + # Verify thin archive is smaller than regular archive + regular_size = archive_path.stat().st_size + thin_size = thin_archive_path.stat().st_size + self.assertLess( + thin_size, + regular_size, + "Thin archive should be smaller than regular archive", + ) + print( + f"Thin archive size: {thin_size} bytes (vs regular: {regular_size} bytes)" + ) + + def test_multiple_object_files(self): + """Test creating archive from multiple object files.""" + # Create multiple simple C++ files + cpp_files: list[Path] = [] + obj_files: list[Path] = [] + + for i in range(3): + cpp_file = self.temp_dir / f"test{i}.cpp" + cpp_content = f""" +#include "FastLED.h" + +void function{i}() {{ + // Test function {i} + CRGB leds[10]; + fill_solid(leds, 10, CRGB::Red); +}} +""" + cpp_file.write_text(cpp_content) + cpp_files.append(cpp_file) + + # Set up compiler + settings = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=[ + "ARDUINO=10607", + "STUB", + "FASTLED_TESTING", + ], + std_version="c++17", + compiler_args=[ + f"-I{self.project_root}/src/platforms/stub", + f"-I{self.project_root}/src/platforms/wasm/compiler", + ], + ) + + build_flags = self._load_build_flags() + compiler = Compiler(settings, build_flags) + + # Compile all files + for i, cpp_file in enumerate(cpp_files): + obj_file = self.temp_dir / f"test{i}.o" + + compile_future = compiler.compile_cpp_file( + cpp_file, obj_file, additional_flags=["-c"] + ) + + compile_result = compile_future.result() + self.assertTrue( + compile_result.ok, + f"Compilation failed for {cpp_file}: {compile_result.stderr}", + ) + self.assertTrue(obj_file.exists()) + obj_files.append(obj_file) + + # Create archive from multiple object files + multi_archive = self.temp_dir / "libmulti.a" + options = LibarchiveOptions(use_thin=False) + + archive_result = create_archive_sync(obj_files, multi_archive, options) + + self.assertTrue( + archive_result.ok, + f"Multi-object archive creation failed: {archive_result.stderr}", + ) + self.assertTrue(multi_archive.exists()) + + # Verify archive size is reasonable + total_obj_size = sum(obj.stat().st_size for obj in obj_files) + archive_size = multi_archive.stat().st_size + self.assertGreater( + archive_size, + total_obj_size * 0.8, + "Archive should contain most of the object data", + ) + + print(f"Multi-object archive: {len(obj_files)} objects, {archive_size} bytes") + + def test_archive_content_verification(self): + """Test verifying archive contents using real archiver tool.""" + # Create an archive first + archive_path = self.test_compile_and_archive_blink() + + # Use configured archiver tool to list contents + build_flags = self._load_build_flags() + configured_cmd = get_configured_archiver_command(build_flags) + self.assertIsNotNone(configured_cmd, "Archiver must be configured in TOML") + # Use the full configured archiver command (e.g., ["uv", "run", "python", "-m", "ziglang", "ar"]) + assert configured_cmd is not None # Type checker hint + archiver_cmd = configured_cmd[:] # Full command including uv run python + + # Build full command for archive listing + full_cmd = archiver_cmd + ["t", str(archive_path)] + list_result = subprocess.run(full_cmd, capture_output=True, text=True) + + self.assertEqual( + list_result.returncode, 0, f"Archive listing failed: {list_result.stderr}" + ) + + # Verify the object file is listed in the archive + contents = list_result.stdout.strip() + self.assertIn( + "Blink.o", contents, f"Blink.o not found in archive contents: {contents}" + ) + + print(f"Archive contents verified: {contents}") + + def test_error_handling_missing_objects(self): + """Test error handling for missing object files - REAL error case.""" + missing_objects = [Path("/nonexistent/file.o")] + archive_path = self.temp_dir / "test.a" + options = LibarchiveOptions() + + result = create_archive_sync(missing_objects, archive_path, options) + + self.assertFalse(result.ok, "Should fail with missing object files") + self.assertEqual(result.return_code, 1) + self.assertIn("Object file not found", result.stderr) + + def test_error_handling_no_objects(self): + """Test error handling when no object files are provided - REAL error case.""" + empty_objects: List[Path] = [] + archive_path = self.temp_dir / "test.a" + options = LibarchiveOptions() + + result = create_archive_sync(empty_objects, archive_path, options) + + self.assertFalse(result.ok, "Should fail with no object files") + self.assertEqual(result.return_code, 1) + self.assertIn("No object files provided", result.stderr) + + def test_unity_build_basic(self): + """Test basic UNITY build functionality with multiple .cpp files.""" + # Create multiple test .cpp files + cpp_files: list[Path] = [] + + for i in range(3): + cpp_file = self.temp_dir / f"unity_test_{i}.cpp" + cpp_content = f""" +#include "FastLED.h" + +// Test function {i} for unity build +void test_function_{i}() {{ + CRGB leds[{10 + i}]; + fill_solid(leds, {10 + i}, CRGB::Red); + // Some unique code for function {i} + int value = {i * 10}; + (void)value; // Suppress unused variable warning +}} + +// Global variable for function {i} +static int global_var_{i} = {i * 100}; +""" + cpp_file.write_text(cpp_content) + cpp_files.append(cpp_file) + + # Set up compiler + settings = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=[ + "ARDUINO=10607", + "STUB", + "FASTLED_TESTING", + ], + std_version="c++17", + compiler_args=[ + f"-I{self.project_root}/src/platforms/stub", + f"-I{self.project_root}/src/platforms/wasm/compiler", + ], + ) + + build_flags = self._load_build_flags() + compiler = Compiler(settings, build_flags) + + # Test unity compilation + unity_options = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + use_pch=False, # Disable PCH for this test to avoid complications + additional_flags=["-c"], + ) + + unity_future = compiler.compile_unity(unity_options, cpp_files) + unity_result = unity_future.result() + + # Verify unity compilation succeeded + self.assertTrue( + unity_result.ok, f"Unity compilation failed: {unity_result.stderr}" + ) + + # Verify that unity.cpp was created and compiled to an object file + # Since we're using temp files, we can't easily check the exact path, + # but we can verify the compilation was successful + print(f"Unity build completed successfully") + + # Now test unity with custom output path + custom_unity_path = self.temp_dir / "custom_unity.cpp" + + # Test unity compilation with custom output + unity_options_custom = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + use_pch=False, + additional_flags=["-c"], + ) + + unity_future_custom = compiler.compile_unity( + unity_options_custom, cpp_files, custom_unity_path + ) + unity_result_custom = unity_future_custom.result() + + # Verify custom unity compilation succeeded + self.assertTrue( + unity_result_custom.ok, + f"Custom unity compilation failed: {unity_result_custom.stderr}", + ) + + # Verify that the custom unity.cpp file was created + self.assertTrue( + custom_unity_path.exists(), + f"Custom unity file was not created at: {custom_unity_path}", + ) + + print(f"Custom unity.cpp created and validated: {custom_unity_path}") + + def test_unity_build_error_handling(self): + """Test UNITY build error handling with invalid inputs.""" + settings = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + std_version="c++17", + ) + + build_flags = self._load_build_flags() + compiler = Compiler(settings, build_flags) + + # Test with empty file list + empty_options = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + ) + + unity_future = compiler.compile_unity(empty_options, []) + unity_result = unity_future.result() + + self.assertFalse(unity_result.ok, "Should fail with empty file list") + self.assertIn("No .cpp files provided", unity_result.stderr) + + # Test with non-existent file + nonexistent_file = self.temp_dir / "nonexistent.cpp" + + unity_future = compiler.compile_unity(empty_options, [nonexistent_file]) + unity_result = unity_future.result() + + self.assertFalse(unity_result.ok, "Should fail with non-existent file") + self.assertIn("Source file not found", unity_result.stderr) + + # Test with non-.cpp file + txt_file = self.temp_dir / "not_cpp.txt" + txt_file.write_text("This is not a C++ file") + + unity_future = compiler.compile_unity(empty_options, [txt_file]) + unity_result = unity_future.result() + + self.assertFalse(unity_result.ok, "Should fail with non-.cpp file") + self.assertIn("File is not a .cpp file", unity_result.stderr) + + def test_unity_build_and_archive_integration(self): + """Test complete workflow: unity build + archive creation.""" + # Create multiple test .cpp files + cpp_files: list[Path] = [] + + for i in range(3): + cpp_file = self.temp_dir / f"integration_test_{i}.cpp" + cpp_content = f""" +#include "FastLED.h" + +// Integration test function {i} +void integration_function_{i}() {{ + CRGB leds[5]; + fill_rainbow(leds, 5, {i * 32}, 20); +}} + +// Static data for testing +static const uint8_t test_data_{i}[] = {{1, 2, 3, {i}}}; +""" + cpp_file.write_text(cpp_content) + cpp_files.append(cpp_file) + + # Set up compiler + settings = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=[ + "ARDUINO=10607", + "STUB", + "FASTLED_TESTING", + ], + std_version="c++17", + compiler_args=[ + f"-I{self.project_root}/src/platforms/stub", + f"-I{self.project_root}/src/platforms/wasm/compiler", + ], + ) + + build_flags = self._load_build_flags() + compiler = Compiler(settings, build_flags) + + # Unity compilation + unity_obj_path = self.temp_dir / "integration_unity.o" + unity_options = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + use_pch=False, + additional_flags=["-c", "-o", str(unity_obj_path)], + ) + + unity_future = compiler.compile_unity(unity_options, cpp_files) + unity_result = unity_future.result() + + # Verify unity compilation succeeded + self.assertTrue( + unity_result.ok, + f"Integration unity compilation failed: {unity_result.stderr}", + ) + self.assertTrue( + unity_obj_path.exists(), + f"Unity object file not created: {unity_obj_path}", + ) + + # Verify object file has reasonable size + obj_size = unity_obj_path.stat().st_size + self.assertGreater(obj_size, 1000, "Unity object file seems too small") + print(f"Unity object file size: {obj_size} bytes") + + # Create archive from unity object file + archive_path = self.temp_dir / "libfastled_integration_unity.a" + options = LibarchiveOptions(use_thin=False) + + archive_result = create_archive_sync([unity_obj_path], archive_path, options) + + # Verify archive creation succeeded + self.assertTrue( + archive_result.ok, + f"Integration archive creation failed: {archive_result.stderr}", + ) + self.assertTrue( + archive_path.exists(), + f"Integration archive file not created: {archive_path}", + ) + + # Verify archive has reasonable size + archive_size = archive_path.stat().st_size + self.assertGreater( + archive_size, obj_size, "Archive should be larger than object file" + ) + print(f"Integration archive size: {archive_size} bytes") + + print("Complete unity build + archive integration test passed!") + + def test_unity_build_with_additional_flags(self): + """Test UNITY build with additional compiler flags.""" + # Create test .cpp files + cpp_files: list[Path] = [] + + for i in range(2): + cpp_file = self.temp_dir / f"flags_test_{i}.cpp" + cpp_content = f""" +#include "FastLED.h" + +void flags_test_function_{i}() {{ + CRGB led; + led = CRGB::Green; + // Test optimization flags {i} + volatile int value = {i * 42}; + (void)value; // Use the value to prevent optimization +}} +""" + cpp_file.write_text(cpp_content) + cpp_files.append(cpp_file) + + # Set up compiler + settings = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=[ + "ARDUINO=10607", + "STUB", + "FASTLED_TESTING", + ], + std_version="c++17", + compiler_args=[ + f"-I{self.project_root}/src/platforms/stub", + f"-I{self.project_root}/src/platforms/wasm/compiler", + ], + ) + + build_flags = self._load_build_flags() + compiler = Compiler(settings, build_flags) + + # Test unity compilation with additional flags + unity_options = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + use_pch=False, + additional_flags=["-c", "-O2", "-ffast-math"], + ) + + unity_future = compiler.compile_unity(unity_options, cpp_files) + unity_result = unity_future.result() + + # Verify unity compilation succeeded + self.assertTrue( + unity_result.ok, + f"Unity compilation with additional flags failed: {unity_result.stderr}", + ) + print(f"Unity build with additional flags completed successfully") + + def test_unity_build_identical_content_skip(self): + """Test that unity build skips writing when content is identical.""" + # Create test .cpp files + cpp_files: list[Path] = [] + + for i in range(2): + cpp_file = self.temp_dir / f"identical_test_{i}.cpp" + cpp_content = f""" +#include "FastLED.h" + +void identical_function_{i}() {{ + CRGB led; + led = CRGB::Red; + // Identical test implementation {i} +}} +""" + cpp_file.write_text(cpp_content) + cpp_files.append(cpp_file) + + # Set up compiler + settings = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + std_version="c++17", + ) + + build_flags = self._load_build_flags() + compiler = Compiler(settings, build_flags) + + # Custom unity.cpp path for testing + unity_path = self.temp_dir / "test_identical_unity.cpp" + + # Test first unity compilation - should create the file + unity_options = CompilerOptions( + include_path=str(self.project_root / "src"), + defines=["STUB", "FASTLED_TESTING"], + use_pch=False, + additional_flags=["-c"], + ) + + unity_future = compiler.compile_unity(unity_options, cpp_files, unity_path) + unity_result = unity_future.result() + + # Verify first compilation succeeded + self.assertTrue( + unity_result.ok, f"First unity compilation failed: {unity_result.stderr}" + ) + + # Verify unity file was created + self.assertTrue( + unity_path.exists(), f"Unity file was not created: {unity_path}" + ) + + # Get original file modification time + original_mtime = unity_path.stat().st_mtime + + # Wait a small amount to ensure mtime would be different if file was rewritten + import time + + time.sleep(0.01) + + # Run unity compilation again with identical inputs - should skip writing + unity_future_2 = compiler.compile_unity(unity_options, cpp_files, unity_path) + unity_result_2 = unity_future_2.result() + + # Verify second compilation succeeded + self.assertTrue( + unity_result_2.ok, + f"Second unity compilation failed: {unity_result_2.stderr}", + ) + + # Get new file modification time + new_mtime = unity_path.stat().st_mtime + + # Verify file was NOT modified (identical content optimization worked) + self.assertEqual( + original_mtime, + new_mtime, + f"Unity file was unnecessarily rewritten - mtime changed from {original_mtime} to {new_mtime}", + ) + + print(f"Unity identical content optimization test passed - file not modified") + + # Now test that different content DOES cause a rewrite + # Add another .cpp file to change the unity content + new_cpp_file = self.temp_dir / "identical_test_new.cpp" + new_cpp_content = """ +#include "FastLED.h" + +void new_function() { + CRGB led; + led = CRGB::Blue; + // New implementation +} +""" + new_cpp_file.write_text(new_cpp_content) + cpp_files_modified = cpp_files + [new_cpp_file] + + # Wait to ensure mtime would be different + time.sleep(0.01) + + # Run unity compilation with different inputs - should rewrite + unity_future_3 = compiler.compile_unity( + unity_options, cpp_files_modified, unity_path + ) + unity_result_3 = unity_future_3.result() + + # Verify third compilation succeeded + self.assertTrue( + unity_result_3.ok, + f"Third unity compilation failed: {unity_result_3.stderr}", + ) + + # Get final file modification time + final_mtime = unity_path.stat().st_mtime + + # Verify file WAS modified when content changed + self.assertNotEqual( + new_mtime, + final_mtime, + f"Unity file should have been rewritten when content changed", + ) + + print( + f"Unity content change detection test passed - file was properly modified" + ) + + +class TestProgramLinking(unittest.TestCase): + """Test suite for program linking functionality.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = Path(tempfile.mkdtemp()) + self.project_root = PROJECT_ROOT + os.chdir(self.project_root) + + def tearDown(self): + """Clean up test fixtures.""" + import shutil + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir) + + def _load_build_flags(self) -> BuildFlags: + """STRICT: Load BuildFlags explicitly - NO defaults allowed.""" + build_flags_path = Path(__file__).parent.parent / "build_unit.toml" + if not build_flags_path.exists(): + raise RuntimeError( + f"CRITICAL: build_unit.toml not found at {build_flags_path}" + ) + return BuildFlags.parse(build_flags_path) + + def test_linker_detection(self): + """Test automatic linker detection.""" + try: + linker = detect_linker() + self.assertIsInstance(linker, str) + self.assertTrue(len(linker) > 0) + print(f"Detected linker: {linker}") + except RuntimeError as e: + self.fail(f"Linker detection failed: {e}") + + def test_link_options_basic(self): + """Test LinkOptions dataclass functionality.""" + options = LinkOptions( + output_executable="test_program.exe", + object_files=["main.o", "utils.o"], + static_libraries=["libmath.a"], + linker_args=["/SUBSYSTEM:CONSOLE"], + ) + + self.assertEqual(options.output_executable, "test_program.exe") + self.assertEqual(len(options.object_files), 2) + self.assertEqual(len(options.static_libraries), 1) + self.assertEqual(len(options.linker_args), 1) + print("LinkOptions dataclass test passed") + + def test_linking_validation_missing_objects(self): + """Test validation of missing object files.""" + options = LinkOptions( + output_executable="test.exe", + object_files=["nonexistent.o"], + static_libraries=[], + linker_args=[], + ) + + build_flags = self._load_build_flags() + result = link_program_sync(options, build_flags) + self.assertFalse(result.ok) + self.assertIn("Object file not found", result.stderr) + print("Missing object file validation works") + + def test_linking_validation_missing_libraries(self): + """Test validation of missing static libraries.""" + # Create a dummy object file + dummy_obj = self.temp_dir / "dummy.o" + dummy_obj.write_text("dummy content") + + options = LinkOptions( + output_executable="test.exe", + object_files=[str(dummy_obj)], + static_libraries=["nonexistent.a"], + linker_args=[], + ) + + build_flags = self._load_build_flags() + result = link_program_sync(options, build_flags) + self.assertFalse(result.ok) + self.assertIn("Static library not found", result.stderr) + print("Missing library validation works") + + def test_linking_validation_no_objects(self): + """Test validation when no object files provided.""" + options = LinkOptions( + output_executable="test.exe", + object_files=[], + static_libraries=[], + linker_args=[], + ) + + build_flags = self._load_build_flags() + result = link_program_sync(options, build_flags) + self.assertFalse(result.ok) + self.assertIn("No object files provided", result.stderr) + print("No object files validation works") + + +class TestFullProgramLinking(unittest.TestCase): + """Test complete program linking using actual FastLED src and examples.""" + + def setUp(self): + """Set up test environment with FastLED source.""" + self.temp_dir = Path(tempfile.mkdtemp()) + self.project_root = PROJECT_ROOT + self.src_dir = self.project_root / "src" + self.examples_dir = self.project_root / "examples" + os.chdir(self.project_root) + + # Verify FastLED source structure + self.assertTrue(self.src_dir.exists(), "FastLED src directory not found") + self.assertTrue((self.src_dir / "FastLED.h").exists(), "FastLED.h not found") + + def tearDown(self): + """Clean up temporary files.""" + import shutil + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir) + + def _load_build_flags(self) -> BuildFlags: + """STRICT: Load BuildFlags explicitly - NO defaults allowed.""" + build_flags_path = Path(__file__).parent.parent / "build_unit.toml" + if not build_flags_path.exists(): + raise RuntimeError( + f"CRITICAL: build_unit.toml not found at {build_flags_path}" + ) + return BuildFlags.parse(build_flags_path) + + def test_link_blink_example_full_pipeline(self): + """Test complete pipeline: Blink.ino + FastLED src to executable.""" + + # Step 1: Set up compiler with FastLED source + compiler_options = CompilerOptions( + include_path=str(self.src_dir), + defines=["STUB_PLATFORM", "ARDUINO=10808"], + std_version="c++17", + compiler_args=[ + f"-I{self.src_dir}/platforms/stub", + f"-I{self.src_dir}/platforms/wasm/compiler", # Arduino.h compatibility + ], + ) + build_flags = self._load_build_flags() + compiler = Compiler(compiler_options, build_flags) + + # Step 2: Compile Blink.ino to object file + blink_ino = self.examples_dir / "Blink" / "Blink.ino" + if not blink_ino.exists(): + self.skipTest("Blink example not found") + + blink_obj = self.temp_dir / "blink.o" + compile_future = compiler.compile_ino_file(blink_ino, blink_obj) + compile_result = compile_future.result() + + if not compile_result.ok: + print(f"WARNING: Blink compilation failed: {compile_result.stderr}") + self.skipTest("Blink compilation failed - skipping link test") + + self.assertTrue(blink_obj.exists(), "Blink object file not created") + + # Step 3: Create minimal FastLED static library from core sources + fastled_objects = self._compile_fastled_core(compiler) + if not fastled_objects: + self.skipTest("FastLED core compilation failed - skipping link test") + + fastled_lib = self.temp_dir / "libfastled.a" + archive_future = compiler.create_archive(fastled_objects, fastled_lib) + archive_result = archive_future.result() + + if not archive_result.ok: + print(f"WARNING: FastLED library creation failed: {archive_result.stderr}") + self.skipTest("FastLED library creation failed - skipping link test") + + # Step 4: Link Blink + FastLED into executable + import platform + + executable_name = ( + "blink_program.exe" if platform.system() == "Windows" else "blink_program" + ) + executable_path = self.temp_dir / executable_name + + link_options = LinkOptions( + output_executable=str(executable_path), + object_files=[str(blink_obj)], + static_libraries=[str(fastled_lib)], + linker_args=self._get_platform_linker_args(executable_name), + ) + + link_future = compiler.link_program(link_options) + link_result = link_future.result() + + if not link_result.ok: + # Linking should succeed - fail the test if it doesn't + self.fail( + f"Program linking failed - this needs to be fixed:\n{link_result.stderr}" + ) + + self.assertTrue(executable_path.exists(), "Executable not created") + self._verify_executable_properties(executable_path) + + print( + f"SUCCESS: Full pipeline test passed: {blink_ino.name} to {executable_path.name}" + ) + + def test_link_multiple_examples_basic(self): + """Test basic linking validation of multiple examples.""" + + # Test examples that should compile with stub platform + test_examples = ["Blink", "FirstLight"] + successful_compiles = 0 + successful_links = 0 + + compiler_options = CompilerOptions( + include_path=str(self.src_dir), + defines=["STUB_PLATFORM", "ARDUINO=10808"], + std_version="c++17", + compiler_args=[ + f"-I{self.src_dir}/platforms/stub", + f"-I{self.src_dir}/platforms/wasm/compiler", # Arduino.h compatibility + ], + ) + build_flags = self._load_build_flags() + compiler = Compiler(compiler_options, build_flags) + + # Create FastLED library once + fastled_objects = self._compile_fastled_core(compiler) + if not fastled_objects: + self.skipTest("FastLED core compilation failed") + + fastled_lib = self.temp_dir / "libfastled.a" + archive_result = compiler.create_archive(fastled_objects, fastled_lib).result() + if not archive_result.ok: + self.skipTest("FastLED library creation failed") + + for example_name in test_examples: + example_dir = self.examples_dir / example_name + if not example_dir.exists(): + continue + + ino_file = example_dir / f"{example_name}.ino" + if not ino_file.exists(): + continue + + try: + # Compile example + obj_file = self.temp_dir / f"{example_name}.o" + compile_result = compiler.compile_ino_file(ino_file, obj_file).result() + + if not compile_result.ok: + print(f"WARNING: {example_name} compilation failed") + continue + + successful_compiles += 1 + + # Attempt linking + import platform + + executable_name = ( + f"{example_name}.exe" + if platform.system() == "Windows" + else example_name + ) + executable_path = self.temp_dir / executable_name + + link_options = LinkOptions( + output_executable=str(executable_path), + object_files=[str(obj_file)], + static_libraries=[str(fastled_lib)], + linker_args=self._get_platform_linker_args(executable_name), + ) + + link_result = compiler.link_program(link_options).result() + + if link_result.ok and executable_path.exists(): + successful_links += 1 + print(f"SUCCESS: {example_name} linked successfully") + else: + self.fail( + f"Linking failed for {example_name} - this needs to be fixed:\n{link_result.stderr}" + ) + + except Exception as e: + print(f"ERROR: {example_name} failed with exception: {e}") + + # Require at least some examples to compile + self.assertGreater(successful_compiles, 0, "No examples compiled successfully") + + print( + f"SUCCESS: Batch test completed: {successful_compiles} compiled, {successful_links} linked" + ) + + def _compile_fastled_core(self, compiler: Compiler) -> list[Path]: + """Compile all FastLED source files to object files.""" + fastled_objects: list[Path] = [] + + # Glob all .cpp files in src/** + print("Compiling all FastLED source files from src/**") + cpp_files = list(self.src_dir.rglob("*.cpp")) + + # Sort for consistent compilation order + cpp_files.sort() + + compiled_count = 0 + for cpp_file in cpp_files: + # Create unique object file name based on relative path + rel_path = cpp_file.relative_to(self.src_dir) + obj_name = ( + str(rel_path).replace("/", "_").replace("\\", "_").replace(".cpp", ".o") + ) + obj_file = self.temp_dir / obj_name + + compile_result = compiler.compile_cpp_file(cpp_file, obj_file).result() + + if compile_result.ok: + fastled_objects.append(obj_file) + compiled_count += 1 + else: + print(f"WARNING: Failed to compile {rel_path}: {compile_result.stderr}") + + # Compile STUB platform implementation (includes delay, millis, micros) + # This is a .hpp file with implementations that needs to be compiled + stub_impl_file = ( + self.src_dir / "platforms" / "stub" / "generic" / "led_sysdefs_generic.hpp" + ) + if stub_impl_file.exists(): + # Create a temporary .cpp file that includes the .hpp implementation + temp_cpp = self.temp_dir / "stub_platform_impl.cpp" + temp_cpp.write_text(f''' +#define FASTLED_STUB_IMPL +#include "{stub_impl_file}" + ''') + + obj_file = self.temp_dir / "stub_platform_impl.o" + compile_result = compiler.compile_cpp_file(temp_cpp, obj_file).result() + + if compile_result.ok: + fastled_objects.append(obj_file) + compiled_count += 1 + print(f"SUCCESS: Compiled STUB platform implementation") + else: + print( + f"WARNING: Failed to compile STUB platform implementation: {compile_result.stderr}" + ) + + # Create main entry point for Arduino-style programs + # This provides the missing main() function that calls setup() and loop() + main_cpp = self.temp_dir / "arduino_main.cpp" + main_cpp.write_text(""" +// Arduino-style main entry point +// Forward declarations for setup() and loop() functions from .ino file +// Note: .ino files compile as C++, not C, so don't use extern "C" +void setup(); +void loop(); + +int main() { + // Call setup() once + setup(); + + // Call loop() a few times (not infinite to avoid hanging in tests) + for (int i = 0; i < 3; ++i) { + loop(); + } + + return 0; +} + """) + + obj_file = self.temp_dir / "arduino_main.o" + compile_result = compiler.compile_cpp_file(main_cpp, obj_file).result() + + if compile_result.ok: + fastled_objects.append(obj_file) + compiled_count += 1 + print(f"SUCCESS: Compiled Arduino main entry point") + else: + print( + f"WARNING: Failed to compile Arduino main entry point: {compile_result.stderr}" + ) + + print(f"Compiled {compiled_count} FastLED source files successfully") + return fastled_objects + + def _get_platform_linker_args(self, executable_name: str) -> list[str]: + """Get platform-appropriate linker arguments using build_unit.toml approach.""" + # Use basic linking flags from build_unit.toml + return ["-pthread"] # Basic linking flags from build_unit.toml [linking.base] + + def _verify_executable_properties(self, executable_path: Path): + """Verify that the created executable has expected properties.""" + + # Check file exists and has reasonable size + self.assertTrue(executable_path.exists(), "Executable file not found") + + file_size = executable_path.stat().st_size + self.assertGreater(file_size, 100, f"Executable too small: {file_size} bytes") + self.assertLess( + file_size, 100 * 1024 * 1024, f"Executable too large: {file_size} bytes" + ) + + # Check executable permissions on Unix + import platform + + if platform.system() != "Windows": + file_mode = executable_path.stat().st_mode + self.assertTrue( + file_mode & stat.S_IXUSR, "Executable not marked as executable" + ) + + print( + f"SUCCESS: Executable verification passed: {executable_path.name} ({file_size:,} bytes)" + ) + + +if __name__ == "__main__": + print("=== FastLED REAL Archive Creation Tests (No Mocks!) ===") + unittest.main(verbosity=2) diff --git a/v1.5/lib/FastLED/ci/test_integration/test_xcache.py b/v1.5/lib/FastLED/ci/test_integration/test_xcache.py new file mode 100644 index 0000000..fb855e4 --- /dev/null +++ b/v1.5/lib/FastLED/ci/test_integration/test_xcache.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +""" +Integration tests for xcache.py using real clang compiler. + +This test suite verifies that xcache works correctly with actual compilation +scenarios, including response files and direct execution modes. +""" + +import os +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + + +@dataclass +class TestResults: + """Test results container.""" + + passed: int = 0 + failed: int = 0 + errors: Optional[list[str]] = None + + def __post_init__(self) -> None: + if self.errors is None: + self.errors = [] + + +def find_clang() -> Optional[str]: + """Find clang compiler in PATH.""" + clang_path = shutil.which("clang") + if clang_path: + return clang_path + + # Check common locations + common_paths = [ + "/usr/bin/clang", + "/usr/local/bin/clang", + "/opt/local/bin/clang", + "C:/Program Files/LLVM/bin/clang.exe", + "C:/msys64/mingw64/bin/clang.exe", + ] + + for path in common_paths: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + + return None + + +def find_sccache() -> Optional[str]: + """Find sccache in PATH.""" + return shutil.which("sccache") + + +def create_test_source_file(temp_dir: Path) -> Path: + """Create a simple C source file for testing.""" + source_file = temp_dir / "test.c" + source_content = """ +#include + +int main() { + printf("Hello from xcache test!\\n"); + return 0; +} +""" + source_file.write_text(source_content) + return source_file + + +def create_response_file(temp_dir: Path, args: list[str]) -> Path: + """Create a response file with the given arguments.""" + response_file = temp_dir / "compile_args.rsp" + + # Write arguments to response file (one per line or space-separated) + content = " ".join(args) + response_file.write_text(content) + + return response_file + + +def test_xcache_direct_execution(results: TestResults) -> None: + """Test xcache with direct execution (no response files).""" + print("🧪 Testing xcache direct execution...") + + clang_path = find_clang() + if not clang_path: + results.errors.append( + "clang compiler not found - skipping direct execution test" + ) + return + + sccache_path = find_sccache() + if not sccache_path: + results.errors.append("sccache not found - skipping direct execution test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file + source_file = create_test_source_file(temp_path) + output_file = temp_path / "test_direct.o" + + # Test xcache with direct arguments (no response files) + env = os.environ.copy() + env["XCACHE_DEBUG"] = "1" + + cmd = [ + sys.executable, + "ci/util/xcache.py", + clang_path, + "-c", + str(source_file), + "-o", + str(output_file), + ] + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, env=env, timeout=30 + ) + + if result.returncode == 0 and output_file.exists(): + print(" ✅ Direct execution test passed") + print(f" Output file created: {output_file.stat().st_size} bytes") + results.passed += 1 + else: + print( + f" ❌ Direct execution test failed (return code: {result.returncode})" + ) + print(f" Stderr: {result.stderr}") + results.failed += 1 + results.errors.append(f"Direct execution failed: {result.stderr}") + + except subprocess.TimeoutExpired: + print(" ❌ Direct execution test timed out") + results.failed += 1 + results.errors.append("Direct execution test timed out") + except Exception as e: + print(f" ❌ Direct execution test error: {e}") + results.failed += 1 + results.errors.append(f"Direct execution error: {e}") + + +def test_xcache_response_file_execution(results: TestResults) -> None: + """Test xcache with response file execution.""" + print("\n🧪 Testing xcache response file execution...") + + clang_path = find_clang() + if not clang_path: + results.errors.append("clang compiler not found - skipping response file test") + return + + sccache_path = find_sccache() + if not sccache_path: + results.errors.append("sccache not found - skipping response file test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file + source_file = create_test_source_file(temp_path) + output_file = temp_path / "test_response.o" + + # Create response file with compilation arguments + compile_args = ["-c", str(source_file), "-o", str(output_file), "-O2", "-Wall"] + response_file = create_response_file(temp_path, compile_args) + + # Test xcache with response file + env = os.environ.copy() + env["XCACHE_DEBUG"] = "1" + + cmd = [sys.executable, "ci/util/xcache.py", clang_path, f"@{response_file}"] + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, env=env, timeout=30 + ) + + if result.returncode == 0 and output_file.exists(): + print(" ✅ Response file execution test passed") + print(f" Output file created: {output_file.stat().st_size} bytes") + print(f" Response file handled: @{response_file}") + + # Verify that response file was passed through (not expanded) + if f"@{response_file}" in result.stderr: + print(" ✅ Response file passed through correctly") + else: + print( + " ⚠️ Response file passthrough not clearly visible in debug output" + ) + + results.passed += 1 + else: + print( + f" ❌ Response file execution test failed (return code: {result.returncode})" + ) + print(f" Stderr: {result.stderr}") + results.failed += 1 + results.errors.append( + f"Response file execution failed: {result.stderr}" + ) + + except subprocess.TimeoutExpired: + print(" ❌ Response file execution test timed out") + results.failed += 1 + results.errors.append("Response file execution test timed out") + except Exception as e: + print(f" ❌ Response file execution test error: {e}") + results.failed += 1 + results.errors.append(f"Response file execution error: {e}") + + +def test_xcache_mixed_arguments(results: TestResults) -> None: + """Test xcache with mixed regular arguments and response files.""" + print("\n🧪 Testing xcache mixed arguments...") + + clang_path = find_clang() + if not clang_path: + results.errors.append( + "clang compiler not found - skipping mixed arguments test" + ) + return + + sccache_path = find_sccache() + if not sccache_path: + results.errors.append("sccache not found - skipping mixed arguments test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file + source_file = create_test_source_file(temp_path) + output_file = temp_path / "test_mixed.o" + + # Create response file with some arguments + response_args = ["-O2", "-Wall", "-Wextra"] + response_file = create_response_file(temp_path, response_args) + + # Test xcache with mixed arguments + env = os.environ.copy() + env["XCACHE_DEBUG"] = "1" + + cmd = [ + sys.executable, + "ci/util/xcache.py", + clang_path, + "-c", + str(source_file), + f"@{response_file}", + "-o", + str(output_file), + ] + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, env=env, timeout=30 + ) + + if result.returncode == 0 and output_file.exists(): + print(" ✅ Mixed arguments test passed") + print(f" Output file created: {output_file.stat().st_size} bytes") + print(f" Mixed args: regular + @{response_file}") + results.passed += 1 + else: + print( + f" ❌ Mixed arguments test failed (return code: {result.returncode})" + ) + print(f" Stderr: {result.stderr}") + results.failed += 1 + results.errors.append(f"Mixed arguments failed: {result.stderr}") + + except subprocess.TimeoutExpired: + print(" ❌ Mixed arguments test timed out") + results.failed += 1 + results.errors.append("Mixed arguments test timed out") + except Exception as e: + print(f" ❌ Mixed arguments test error: {e}") + results.failed += 1 + results.errors.append(f"Mixed arguments error: {e}") + + +def test_xcache_cache_effectiveness(results: TestResults) -> None: + """Test that xcache actually caches compilation results.""" + print("\n🧪 Testing xcache cache effectiveness...") + + clang_path = find_clang() + if not clang_path: + results.errors.append( + "clang compiler not found - skipping cache effectiveness test" + ) + return + + sccache_path = find_sccache() + if not sccache_path: + results.errors.append("sccache not found - skipping cache effectiveness test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file + source_file = create_test_source_file(temp_path) + + # Set up environment for caching + env = os.environ.copy() + env["XCACHE_DEBUG"] = "1" + cache_dir = temp_path / "sccache" + cache_dir.mkdir() + env["SCCACHE_DIR"] = str(cache_dir) + + # First compilation (should be cache miss) + output_file1 = temp_path / "test_cache1.o" + cmd1 = [ + sys.executable, + "ci/util/xcache.py", + clang_path, + "-c", + str(source_file), + "-o", + str(output_file1), + ] + + # Second compilation (should be cache hit) + output_file2 = temp_path / "test_cache2.o" + cmd2 = [ + sys.executable, + "ci/util/xcache.py", + clang_path, + "-c", + str(source_file), + "-o", + str(output_file2), + ] + + try: + # First compilation + result1 = subprocess.run( + cmd1, capture_output=True, text=True, env=env, timeout=30 + ) + + # Second compilation + result2 = subprocess.run( + cmd2, capture_output=True, text=True, env=env, timeout=30 + ) + + if ( + result1.returncode == 0 + and output_file1.exists() + and result2.returncode == 0 + and output_file2.exists() + ): + print(" ✅ Cache effectiveness test passed") + print(f" First compilation: {output_file1.stat().st_size} bytes") + print(f" Second compilation: {output_file2.stat().st_size} bytes") + + # Check if sccache cache directory has content + cache_files = list(cache_dir.rglob("*")) + if cache_files: + print(f" Cache directory has {len(cache_files)} entries") + else: + print(" ⚠️ Cache directory appears empty") + + results.passed += 1 + else: + print(f" ❌ Cache effectiveness test failed") + print( + f" First result: {result1.returncode}, {output_file1.exists()}" + ) + print( + f" Second result: {result2.returncode}, {output_file2.exists()}" + ) + results.failed += 1 + results.errors.append("Cache effectiveness test failed") + + except subprocess.TimeoutExpired: + print(" ❌ Cache effectiveness test timed out") + results.failed += 1 + results.errors.append("Cache effectiveness test timed out") + except Exception as e: + print(f" ❌ Cache effectiveness test error: {e}") + results.failed += 1 + results.errors.append(f"Cache effectiveness error: {e}") + + +def run_all_tests() -> TestResults: + """Run all xcache tests.""" + print("🚀 XCACHE INTEGRATION TESTS WITH CLANG") + print("=" * 60) + + results = TestResults() + + # Check prerequisites + clang_path = find_clang() + sccache_path = find_sccache() + + print(f"📋 Test Environment:") + print(f" Clang: {clang_path or 'NOT FOUND'}") + print(f" Sccache: {sccache_path or 'NOT FOUND'}") + print(f" xcache: ci/util/xcache.py") + print() + + if not clang_path: + print("❌ Clang compiler not found - install clang to run tests") + results.errors.append("Clang compiler not available") + return results + + if not sccache_path: + print("❌ Sccache not found - install sccache to run tests") + results.errors.append("Sccache not available") + return results + + # Run tests + test_xcache_direct_execution(results) + test_xcache_response_file_execution(results) + test_xcache_mixed_arguments(results) + test_xcache_cache_effectiveness(results) + + return results + + +def main() -> int: + """Main test function.""" + results = run_all_tests() + + print("\n" + "=" * 60) + print("📊 TEST RESULTS SUMMARY") + print("=" * 60) + + total_tests = results.passed + results.failed + if total_tests > 0: + success_rate = (results.passed / total_tests) * 100 + print(f" Tests passed: {results.passed}") + print(f" Tests failed: {results.failed}") + print(f" Success rate: {success_rate:.1f}%") + else: + print(" No tests were executed") + + if results.errors: + print(f"\n🚨 Errors encountered:") + for i, error in enumerate(results.errors, 1): + print(f" {i}. {error}") + + if results.failed == 0 and results.passed > 0: + print("\n✅ All xcache tests passed! Ready for ESP32S3 integration.") + return 0 + elif results.passed > 0: + print( + f"\n⚠️ Some tests passed but {results.failed} failed. Check errors above." + ) + return 1 + else: + print("\n❌ No tests passed. Check prerequisites and errors above.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/test_integration/test_xcache_standalone.py b/v1.5/lib/FastLED/ci/test_integration/test_xcache_standalone.py new file mode 100644 index 0000000..c7e943b --- /dev/null +++ b/v1.5/lib/FastLED/ci/test_integration/test_xcache_standalone.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python3 +""" +Standalone tests for xcache.py using clang compiler without requiring sccache server. + +This test suite verifies that xcache works correctly by testing the wrapper +functionality and ensuring proper command construction and execution. +""" + +import os +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + + +@dataclass +class TestResults: + """Test results container.""" + + passed: int = 0 + failed: int = 0 + errors: Optional[list[str]] = None + + def __post_init__(self) -> None: + if self.errors is None: + self.errors = [] + + +def find_clang() -> Optional[str]: + """Find clang compiler in PATH.""" + clang_path = shutil.which("clang") + if clang_path: + return clang_path + + # Check common locations + common_paths = [ + "/usr/bin/clang", + "/usr/local/bin/clang", + "/opt/local/bin/clang", + "C:/Program Files/LLVM/bin/clang.exe", + "C:/msys64/mingw64/bin/clang.exe", + ] + + for path in common_paths: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + + return None + + +def create_test_source_file(temp_dir: Path) -> Path: + """Create a simple C source file for testing.""" + source_file = temp_dir / "test.c" + source_content = """ +#include + +int main() { + printf("Hello from xcache test!\\n"); + return 0; +} +""" + source_file.write_text(source_content) + return source_file + + +def create_response_file(temp_dir: Path, args: list[str]) -> Path: + """Create a response file with the given arguments.""" + response_file = temp_dir / "compile_args.rsp" + + # Write arguments to response file (space-separated) + # Properly quote arguments that contain spaces (Windows paths) + # Use forward slashes for Windows paths to avoid backslash escape issues + quoted_args: list[str] = [] + for arg in args: + # Convert Windows backslashes to forward slashes for response files + normalized_arg = arg.replace("\\", "/") + if " " in normalized_arg and not ( + normalized_arg.startswith('"') and normalized_arg.endswith('"') + ): + quoted_args.append(f'"{normalized_arg}"') + else: + quoted_args.append(normalized_arg) + + content = " ".join(quoted_args) + response_file.write_text(content) + + return response_file + + +def test_clang_direct_compilation(results: TestResults) -> None: + """Test that clang compilation works directly (baseline test).""" + print("🧪 Testing direct clang compilation (baseline)...") + + clang_path = find_clang() + if not clang_path: + results.errors.append("clang compiler not found - skipping baseline test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file + source_file = create_test_source_file(temp_path) + output_file = temp_path / "test_baseline.o" + + # Test direct clang compilation + cmd = [clang_path, "-c", str(source_file), "-o", str(output_file)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + if result.returncode == 0 and output_file.exists(): + print(" ✅ Direct clang compilation works") + print(f" Output file created: {output_file.stat().st_size} bytes") + results.passed += 1 + else: + print( + f" ❌ Direct clang compilation failed (return code: {result.returncode})" + ) + print(f" Stderr: {result.stderr}") + results.failed += 1 + results.errors.append( + f"Direct clang compilation failed: {result.stderr}" + ) + + except subprocess.TimeoutExpired: + print(" ❌ Direct clang compilation timed out") + results.failed += 1 + results.errors.append("Direct clang compilation timed out") + except Exception as e: + print(f" ❌ Direct clang compilation error: {e}") + results.failed += 1 + results.errors.append(f"Direct clang compilation error: {e}") + + +def test_clang_response_file_compilation(results: TestResults) -> None: + """Test that clang can handle response files directly.""" + print("\n🧪 Testing clang response file compilation...") + + clang_path = find_clang() + if not clang_path: + results.errors.append("clang compiler not found - skipping response file test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file + source_file = create_test_source_file(temp_path) + output_file = temp_path / "test_response.o" + + # Create response file with compilation arguments + compile_args = ["-c", str(source_file), "-o", str(output_file)] + response_file = create_response_file(temp_path, compile_args) + + # Test clang with response file + cmd = [clang_path, f"@{response_file}"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + if result.returncode == 0 and output_file.exists(): + print(" ✅ Clang response file compilation works") + print(f" Output file created: {output_file.stat().st_size} bytes") + print(f" Response file: @{response_file}") + results.passed += 1 + else: + print( + f" ❌ Clang response file compilation failed (return code: {result.returncode})" + ) + print(f" Stderr: {result.stderr}") + results.failed += 1 + results.errors.append( + f"Clang response file compilation failed: {result.stderr}" + ) + + except subprocess.TimeoutExpired: + print(" ❌ Clang response file compilation timed out") + results.failed += 1 + results.errors.append("Clang response file compilation timed out") + except Exception as e: + print(f" ❌ Clang response file compilation error: {e}") + results.failed += 1 + results.errors.append(f"Clang response file compilation error: {e}") + + +def test_xcache_wrapper_script_creation(results: TestResults) -> None: + """Test that xcache creates wrapper scripts correctly.""" + print("\n🧪 Testing xcache wrapper script creation...") + + clang_path = find_clang() + if not clang_path: + results.errors.append("clang compiler not found - skipping wrapper script test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file and response file + source_file = create_test_source_file(temp_path) + compile_args = ["-c", str(source_file), "-o", "test.o"] + response_file = create_response_file(temp_path, compile_args) + + # Create a mock sccache script that just echoes its arguments + mock_sccache = ( + temp_path / "sccache.bat" if os.name == "nt" else temp_path / "sccache" + ) + + if os.name == "nt": + mock_content = """@echo off +echo MOCK_SCCACHE_CALLED: %* +""" + else: + mock_content = """#!/bin/bash +echo "MOCK_SCCACHE_CALLED: $@" +""" + + mock_sccache.write_text(mock_content) + if os.name != "nt": + mock_sccache.chmod(0o755) + + # Set up environment to use mock sccache + env = os.environ.copy() + env["PATH"] = f"{temp_path}{os.pathsep}{env.get('PATH', '')}" + env["XCACHE_DEBUG"] = "1" + + # Test xcache with response file (should create wrapper script) + cmd = [sys.executable, "ci/util/xcache.py", clang_path, f"@{response_file}"] + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, env=env, timeout=30 + ) + + # Check if wrapper script was created and executed + if "Created compiler wrapper:" in result.stderr: + print(" ✅ Wrapper script creation detected") + + # Check if wrapper was executed + if "Executing with wrapper:" in result.stderr: + print(" ✅ Wrapper script execution detected") + + # Check if mock sccache was called + if "MOCK_SCCACHE_CALLED:" in result.stdout: + print(" ✅ Mock sccache was called through wrapper") + print(f" Sccache called with: {result.stdout.strip()}") + results.passed += 1 + else: + print(" ❌ Mock sccache was not called") + results.failed += 1 + results.errors.append( + "Mock sccache was not called through wrapper" + ) + else: + print(" ❌ Wrapper script was not executed") + results.failed += 1 + results.errors.append("Wrapper script was not executed") + else: + print(" ❌ Wrapper script was not created") + print(f" Stderr: {result.stderr}") + results.failed += 1 + results.errors.append( + f"Wrapper script creation failed: {result.stderr}" + ) + + except subprocess.TimeoutExpired: + print(" ❌ Wrapper script test timed out") + results.failed += 1 + results.errors.append("Wrapper script test timed out") + except Exception as e: + print(f" ❌ Wrapper script test error: {e}") + results.failed += 1 + results.errors.append(f"Wrapper script test error: {e}") + + +def test_xcache_direct_mode(results: TestResults) -> None: + """Test xcache direct mode (no response files).""" + print("\n🧪 Testing xcache direct mode...") + + clang_path = find_clang() + if not clang_path: + results.errors.append("clang compiler not found - skipping direct mode test") + return + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create test source file + source_file = create_test_source_file(temp_path) + + # Create a mock sccache script that just passes through to real compiler + mock_sccache = ( + temp_path / "sccache.bat" if os.name == "nt" else temp_path / "sccache" + ) + + if os.name == "nt": + mock_content = f'''@echo off +echo MOCK_SCCACHE_DIRECT: %* +"{clang_path}" %* +''' + else: + mock_content = f'''#!/bin/bash +echo "MOCK_SCCACHE_DIRECT: $@" +"{clang_path}" "$@" +''' + + mock_sccache.write_text(mock_content) + if os.name != "nt": + mock_sccache.chmod(0o755) + + # Set up environment to use mock sccache + env = os.environ.copy() + env["PATH"] = f"{temp_path}{os.pathsep}{env.get('PATH', '')}" + env["XCACHE_DEBUG"] = "1" + + output_file = temp_path / "test_direct.o" + + # Test xcache without response files (direct mode) + cmd = [ + sys.executable, + "ci/util/xcache.py", + clang_path, + "-c", + str(source_file), + "-o", + str(output_file), + ] + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, env=env, timeout=30 + ) + + if result.returncode == 0 and output_file.exists(): + print(" ✅ Direct mode compilation succeeded") + print(f" Output file created: {output_file.stat().st_size} bytes") + + # Check if direct execution was used (no wrapper) + if "Direct execution:" in result.stderr: + print(" ✅ Direct execution mode confirmed") + results.passed += 1 + else: + print(" ⚠️ Direct execution mode not clearly indicated") + results.passed += 1 # Still pass since compilation worked + + else: + print( + f" ❌ Direct mode compilation failed (return code: {result.returncode})" + ) + print(f" Stderr: {result.stderr}") + results.failed += 1 + results.errors.append( + f"Direct mode compilation failed: {result.stderr}" + ) + + except subprocess.TimeoutExpired: + print(" ❌ Direct mode test timed out") + results.failed += 1 + results.errors.append("Direct mode test timed out") + except Exception as e: + print(f" ❌ Direct mode test error: {e}") + results.failed += 1 + results.errors.append(f"Direct mode test error: {e}") + + +def run_all_tests() -> TestResults: + """Run all xcache standalone tests.""" + print("🚀 XCACHE STANDALONE TESTS WITH CLANG") + print("=" * 60) + + results = TestResults() + + # Check prerequisites + clang_path = find_clang() + + print(f"📋 Test Environment:") + print(f" Clang: {clang_path or 'NOT FOUND'}") + print(f" xcache: ci/util/xcache.py") + print(f" Note: Using mock sccache for testing wrapper functionality") + print() + + if not clang_path: + print("❌ Clang compiler not found - install clang to run tests") + results.errors.append("Clang compiler not available") + return results + + # Run tests + test_clang_direct_compilation(results) + test_clang_response_file_compilation(results) + test_xcache_wrapper_script_creation(results) + test_xcache_direct_mode(results) + + return results + + +def main() -> int: + """Main test function.""" + results = run_all_tests() + + print("\n" + "=" * 60) + print("📊 TEST RESULTS SUMMARY") + print("=" * 60) + + total_tests = results.passed + results.failed + if total_tests > 0: + success_rate = (results.passed / total_tests) * 100 + print(f" Tests passed: {results.passed}") + print(f" Tests failed: {results.failed}") + print(f" Success rate: {success_rate:.1f}%") + else: + print(" No tests were executed") + + if results.errors: + print(f"\n🚨 Errors encountered:") + for i, error in enumerate(results.errors, 1): + print(f" {i}. {error}") + + if results.failed == 0 and results.passed > 0: + print( + "\n✅ All xcache standalone tests passed! Wrapper functionality verified." + ) + return 0 + elif results.passed > 0: + print( + f"\n⚠️ Some tests passed but {results.failed} failed. Check errors above." + ) + return 1 + else: + print("\n❌ No tests passed. Check prerequisites and errors above.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/test_unified_build.py b/v1.5/lib/FastLED/ci/test_unified_build.py new file mode 100644 index 0000000..005f3d3 --- /dev/null +++ b/v1.5/lib/FastLED/ci/test_unified_build.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +""" +Test script to validate the new unified build system. + +This script tests the core functionality of the BuildAPI to ensure +the refactored build system works correctly. +""" + +import tempfile +from pathlib import Path + +from ci.build_api import BuildType, create_example_builder, create_unit_test_builder + + +def test_build_api_creation(): + """Test that BuildAPI instances can be created successfully""" + print("Testing BuildAPI creation...") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Test unit test builder creation + unit_builder = create_unit_test_builder( + build_dir=temp_path / "unit", use_pch=True, parallel=True, clean=False + ) + + assert unit_builder.build_type == BuildType.UNIT_TEST + assert unit_builder.build_dir == temp_path / "unit" + assert unit_builder.use_pch == True + + print("✓ Unit test builder created successfully") + + # Test example builder creation + example_builder = create_example_builder( + build_dir=temp_path / "examples", use_pch=True, parallel=True, clean=False + ) + + assert example_builder.build_type == BuildType.EXAMPLE + assert example_builder.build_dir == temp_path / "examples" + assert example_builder.use_pch == True + + print("✓ Example builder created successfully") + + +def test_build_configuration_loading(): + """Test that build configurations are loaded correctly""" + print("\nTesting build configuration loading...") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Test unit test configuration + unit_builder = create_unit_test_builder(build_dir=temp_path / "unit") + + # Check that unit test specific defines are present + unit_defines = unit_builder.compiler_settings.defines or [] + assert "FASTLED_FORCE_NAMESPACE=1" in unit_defines + assert "FASTLED_TESTING=1" in unit_defines + assert "STUB_PLATFORM" in unit_defines + + print("✓ Unit test configuration loaded correctly") + + # Test example configuration + example_builder = create_example_builder(build_dir=temp_path / "examples") + + # Check that example specific defines are present + example_defines = example_builder.compiler_settings.defines or [] + assert "STUB_PLATFORM" in example_defines + assert "ARDUINO=10808" in example_defines + # Unit test specific defines should NOT be present + assert "FASTLED_FORCE_NAMESPACE=1" not in example_defines + assert "FASTLED_TESTING=1" not in example_defines + + print("✓ Example configuration loaded correctly") + + +def test_build_directory_structure(): + """Test that build directory structure is created correctly""" + print("\nTesting build directory structure...") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + unit_builder = create_unit_test_builder(build_dir=temp_path / "unit") + + # Check that all required directories are created + assert unit_builder.build_dir.exists() + assert unit_builder.artifacts_dir.exists() + assert unit_builder.targets_dir.exists() + assert unit_builder.cache_dir.exists() + + # Check directory organization + expected_structure = [ + unit_builder.build_dir / "artifacts", + unit_builder.build_dir / "targets", + unit_builder.build_dir / "cache", + ] + + for directory in expected_structure: + assert directory.exists(), f"Directory {directory} should exist" + + print("✓ Build directory structure created correctly") + + +def test_build_info(): + """Test that build info is returned correctly""" + print("\nTesting build info...") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + unit_builder = create_unit_test_builder(build_dir=temp_path / "unit") + info = unit_builder.get_build_info() + + # Check required fields + assert "config_file" in info + assert "build_dir" in info + assert "build_type" in info + assert "use_pch" in info + assert "parallel" in info + + # Check values + assert info["build_type"] == "unit_test" + assert info["use_pch"] == True + assert str(temp_path / "unit") in info["build_dir"] + assert "build_unit.toml" in info["build_flags_toml"] + + print("✓ Build info returned correctly") + + +def test_clean_functionality(): + """Test that clean functionality works""" + print("\nTesting clean functionality...") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + unit_builder = create_unit_test_builder(build_dir=temp_path / "unit") + + # Create some fake files to test cleaning + test_file = unit_builder.build_dir / "test_file.txt" + test_file.write_text("test content") + assert test_file.exists() + + # Test clean functionality + unit_builder.clean_build_artifacts() + + # Build directory should still exist but be empty (except for subdirs) + assert unit_builder.build_dir.exists() + assert not test_file.exists() + + # Subdirectories should be recreated + assert unit_builder.artifacts_dir.exists() + assert unit_builder.targets_dir.exists() + assert unit_builder.cache_dir.exists() + + print("✓ Clean functionality works correctly") + + +def test_configuration_separation(): + """Test that unit test and example configurations are properly separated""" + print("\nTesting configuration separation...") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create both builders + unit_builder = create_unit_test_builder(build_dir=temp_path / "unit") + example_builder = create_example_builder(build_dir=temp_path / "examples") + + # Get build info for comparison + unit_info = unit_builder.get_build_info() + example_info = example_builder.get_build_info() + + # Configurations should use different files + assert "build_unit.toml" in unit_info["build_flags_toml"] + assert "build_example.toml" in example_info["build_flags_toml"] + + # Build types should be different + assert unit_info["build_type"] == "unit_test" + assert example_info["build_type"] == "example" + + # Build directories should be different + assert unit_info["build_dir"] != example_info["build_dir"] + + print("✓ Configuration separation works correctly") + + +def run_all_tests(): + """Run all validation tests""" + print("=" * 60) + print("FASTLED UNIFIED BUILD SYSTEM VALIDATION") + print("=" * 60) + + try: + test_build_api_creation() + test_build_configuration_loading() + test_build_directory_structure() + test_build_info() + test_clean_functionality() + test_configuration_separation() + + print("\n" + "=" * 60) + print("ALL TESTS PASSED ✓") + print("=" * 60) + print("The unified build system is working correctly!") + print("\nKey features validated:") + print("✓ BuildAPI creation for both unit tests and examples") + print("✓ Proper configuration loading from separate TOML files") + print("✓ Organized build directory structure") + print("✓ Clean separation between unit test and example builds") + print("✓ Build info reporting") + print("✓ Clean functionality") + + return True + + except Exception as e: + print(f"\n❌ TEST FAILED: {e}") + import traceback + + traceback.print_exc() + return False + + +if __name__ == "__main__": + success = run_all_tests() + exit(0 if success else 1) diff --git a/v1.5/lib/FastLED/ci/tests/check_namespace_includes.py b/v1.5/lib/FastLED/ci/tests/check_namespace_includes.py new file mode 100644 index 0000000..846f4b2 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/check_namespace_includes.py @@ -0,0 +1,114 @@ +# pyright: reportUnknownMemberType=false, reportMissingParameterType=false +"""Test for checking namespace includes in C++ headers.""" + +#!/usr/bin/env python3 +""" +Script to check for includes after namespace declarations in C++ files. +This is used in CI/CD to prevent bad code patterns. +""" + +import os +import re +import sys +from pathlib import Path +from typing import Any, Dict, List + + +def find_includes_after_namespace(file_path: Path) -> List[int]: + """ + Check if a C++ file has #include directives after namespace declarations. + + Args: + file_path (Path): Path to the C++ file to check + + Returns: + List[int]: List of line numbers where includes appear after namespaces + """ + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.readlines() + + violations: List[int] = [] + namespace_started = False + + # Basic patterns + namespace_pattern = re.compile(r"^\s*namespace\s+\w+\s*\{") + include_pattern = re.compile(r"^\s*#\s*include") + + for i, line in enumerate(content, 1): + line = line.strip() + + # Skip empty lines and comments + if not line or line.startswith("//") or line.startswith("/*"): + continue + + # Check for namespace declaration + if namespace_pattern.match(line): + namespace_started = True + + # Check for #include after namespace started + if namespace_started and include_pattern.match(line): + violations.append(i) + + return violations + except (UnicodeDecodeError, IOError): + # Skip files that can't be read + return [] + + +def scan_cpp_files(directory: str = ".") -> Dict[str, Any]: + """ + Scan all C++ files in a directory for includes after namespace declarations. + + Args: + directory (str): Directory to scan for C++ files + + Returns: + Dict[str, Any]: Dictionary mapping file paths to lists of violation line numbers + """ + cpp_extensions = [ + ".cpp", + ".cc", + ".cxx", + ".c++", + ".hpp", + ".h", + ".hh", + ".hxx", + ".h++", + ] + violations: Dict[str, Any] = {} + + for root, dirs, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + + # Check if it's a C++ file + if any(file.endswith(ext) for ext in cpp_extensions): + line_numbers: List[int] = find_includes_after_namespace(Path(file_path)) + if line_numbers: + violations[file_path] = line_numbers + + return violations + + +def main() -> None: + violations: Dict[str, Any] = scan_cpp_files() + + if violations: + print("Found #include directives after namespace declarations:") + for file_path, line_numbers in violations.items(): + print(f"\n{file_path}:") + for line_num in line_numbers: + print(f" Line {line_num}") + + failing: List[str] = list(violations.keys()) + print(f"\nFailing files: {failing}") + sys.exit(1) + else: + print("No violations found.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/tests/no_using_namespace_fl_in_headers.py b/v1.5/lib/FastLED/ci/tests/no_using_namespace_fl_in_headers.py new file mode 100644 index 0000000..842c96c --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/no_using_namespace_fl_in_headers.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false, reportMissingParameterType=false +import os +import unittest +from concurrent.futures import ThreadPoolExecutor +from typing import List + +from ci.util.paths import PROJECT_ROOT + + +SRC_ROOT = PROJECT_ROOT / "src" +PLATFORMS_DIR = os.path.join(SRC_ROOT, "platforms") + +NUM_WORKERS = 1 if os.environ.get("NO_PARALLEL") else (os.cpu_count() or 1) * 4 + + +class NoUsingNamespaceFlInHeaderTester(unittest.TestCase): + def check_file(self, file_path: str) -> List[str]: + if "FastLED.h" in file_path: + return [] + failings: List[str] = [] + with open(file_path, "r", encoding="utf-8") as f: + for line_number, line in enumerate(f, 1): + if line.startswith("//"): + continue + if "using namespace fl;" in line: + failings.append(f"{file_path}:{line_number}: {line.strip()}") + return failings + + def test_no_using_namespace(self) -> None: + """Searches through the program files to check for banned headers, excluding src/platforms.""" + files_to_check: List[str] = [] + for root, _, files in os.walk(SRC_ROOT): + for file in files: + if file.endswith( + (".h", ".hpp") + ): # Add or remove file extensions as needed + file_path = os.path.join(root, file) + files_to_check.append(file_path) + + all_failings: List[str] = [] + with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor: + futures = [ + executor.submit(self.check_file, file_path) + for file_path in files_to_check + ] + for future in futures: + all_failings.extend(future.result()) + + if all_failings: + msg = ( + f'Found {len(all_failings)} header file(s) "using namespace fl": \n' + + "\n".join(all_failings) + ) + for failing in all_failings: + print(failing) + self.fail(msg) + else: + print("No using namespace fl; found in headers.") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_bin_2_elf.py b/v1.5/lib/FastLED/ci/tests/test_bin_2_elf.py new file mode 100644 index 0000000..75a0402 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_bin_2_elf.py @@ -0,0 +1,74 @@ +import subprocess +import unittest +import warnings +from pathlib import Path + +from ci.util.bin_2_elf import bin_to_elf +from ci.util.elf import dump_symbol_sizes +from ci.util.paths import PROJECT_ROOT +from ci.util.tools import Tools, load_tools + + +HERE = Path(__file__).resolve().parent.absolute() +UNO = HERE / "uno" +OUTPUT = HERE / "output" + + +BUILD_INFO_PATH = PROJECT_ROOT / ".build" / "examples" / "uno" / "build_info.json" +BUILD_INFO_PATH2 = ( + PROJECT_ROOT / ".build" / "fled" / "examples" / "uno" / "build_info.json" +) + +DISABLED = True + + +class TestBinToElf(unittest.TestCase): + @classmethod + def setUpClass(cls): + if DISABLED: + return + uno_build = PROJECT_ROOT / ".build" / "uno" + print(f"Checking for Uno build in: {uno_build}") + if not uno_build.exists(): + print("Uno build not found. Running compilation...") + try: + subprocess.run( + "uv run python -m ci.ci-compile uno --examples Blink", + shell=True, + check=True, + ) + print("Compilation completed successfully.") + except subprocess.CalledProcessError as e: + print(f"Error during compilation: {e}") + raise + + @unittest.skip("Skip bin to elf conversion test") + def test_bin_to_elf_conversion(self) -> None: + if DISABLED: + return + tools: Tools + try: + tools = load_tools(BUILD_INFO_PATH) + except FileNotFoundError as e: + warnings.warn(f"Error while loading tools: {e}") + tools = load_tools(BUILD_INFO_PATH2) + bin_file = UNO / "firmware.hex" + map_file = UNO / "firmware.map" + output_elf = OUTPUT / "output.elf" + try: + bin_to_elf( + bin_file, + map_file, + tools.as_path, + tools.ld_path, + tools.objcopy_path, + output_elf, + ) + stdout = dump_symbol_sizes(tools.nm_path, tools.cpp_filt_path, output_elf) + print(stdout) + except Exception as e: + warnings.warn(f"Error while converting binary to ELF: {e}") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_board_to_platformio_ini.py b/v1.5/lib/FastLED/ci/tests/test_board_to_platformio_ini.py new file mode 100644 index 0000000..7144ee7 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_board_to_platformio_ini.py @@ -0,0 +1,53 @@ +import unittest + +from ci.boards import Board + + +class TestBoardToPlatformioIni(unittest.TestCase): + """Tests for Board.to_platformio_ini().""" + + def _ini_to_set(self, ini: str) -> set[str]: + """Return a set with each non-empty, stripped line of the ini snippet.""" + return {line.strip() for line in ini.splitlines() if line.strip()} + + def test_basic_fields(self): + board = Board(board_name="uno", platform="atmelavr", framework="arduino") + ini = board.to_platformio_ini() + lines = self._ini_to_set(ini) + expected = { + "[env:uno]", + "board = uno", + "platform = atmelavr", + "framework = arduino", + } + self.assertTrue(expected.issubset(lines)) + # Should not reference internal attributes + self.assertNotIn("platform_needs_install", ini) + + def test_real_board_name(self): + board = Board( + board_name="esp32c3", + real_board_name="esp32-c3-devkitm-1", + platform="espressif32", + ) + ini = board.to_platformio_ini() + lines = self._ini_to_set(ini) + self.assertIn("[env:esp32c3]", lines) + self.assertIn("board = esp32-c3-devkitm-1", lines) + + def test_flags_and_unflags(self): + board = Board( + board_name="custom", + defines=["FASTLED_TEST=1"], + build_flags=["-O2"], + ) + ini = board.to_platformio_ini() + lines = self._ini_to_set(ini) + # The build_flags are in multi-line format - check that both flags are present as separate lines + self.assertIn("build_flags =", lines) + self.assertIn("-DFASTLED_TEST=1", lines) + self.assertIn("-O2", lines) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_build_flags_toml.py b/v1.5/lib/FastLED/ci/tests/test_build_flags_toml.py new file mode 100644 index 0000000..715d2f3 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_build_flags_toml.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +""" +Unit tests for BuildFlags TOML parsing and serialization functionality. + +Tests the BuildFlags and BuildTools classes for parsing TOML build configuration +files and serializing back to TOML format. +""" + +import tempfile +import unittest +from pathlib import Path + +from ci.compiler.clang_compiler import ArchiveOptions, BuildFlags, BuildTools + + +class TestBuildFlagsToml(unittest.TestCase): + """Test BuildFlags TOML parsing and serialization""" + + def setUp(self) -> None: + """Set up test fixtures""" + self.temp_dir = Path(tempfile.mkdtemp()) + + def tearDown(self) -> None: + """Clean up test fixtures""" + # Clean up any test files + for file in self.temp_dir.glob("*.toml"): + file.unlink() + self.temp_dir.rmdir() + + def create_test_toml(self, content: str) -> Path: + """Create a test TOML file with the given content""" + test_file = self.temp_dir / "test_build_flags.toml" + with open(test_file, "w", encoding="utf-8") as f: + f.write(content) + return test_file + + def test_build_tools_defaults(self) -> None: + """Test BuildTools default values""" + tools = BuildTools( + cpp_compiler=[], + archiver=[], + linker=[], + c_compiler=[], + objcopy=[], + nm=[], + strip=[], + ranlib=[], + ) + + # Test modern command-based fields (should be empty by default) + self.assertEqual(tools.cpp_compiler, []) + self.assertEqual(tools.archiver, []) + self.assertEqual(tools.linker, []) + + # Test other important fields + self.assertEqual(tools.c_compiler, []) + self.assertEqual(tools.objcopy, []) + self.assertEqual(tools.nm, []) + self.assertEqual(tools.strip, []) + self.assertEqual(tools.ranlib, []) + + def test_parse_minimal_toml(self) -> None: + """Test parsing minimal TOML file with required tools section""" + toml_content = """ +[all] +defines = ["-DTEST=1"] +compiler_flags = ["-Wall"] +include_flags = ["-I."] + +[tools] +cpp_compiler = ["uv", "run", "python", "-m", "ziglang", "c++"] +linker = ["uv", "run", "python", "-m", "ziglang", "c++"] +c_compiler = ["clang"] +objcopy = ["uv", "run", "python", "-m", "ziglang", "objcopy"] +nm = ["uv", "run", "python", "-m", "ziglang", "nm"] +strip = ["uv", "run", "python", "-m", "ziglang", "strip"] +ranlib = ["uv", "run", "python", "-m", "ziglang", "ranlib"] +archiver = ["uv", "run", "python", "-m", "ziglang", "ar"] + +[archive] +flags = "rcsD" + +[linking.base] +flags = ["-pthread"] + +[strict_mode] +flags = ["-Werror"] +""" + + test_file = self.create_test_toml(toml_content) + flags = BuildFlags.parse(test_file, quick_build=False, strict_mode=False) + + # Check basic flags + self.assertEqual(flags.defines, ["-DTEST=1"]) + self.assertEqual(flags.compiler_flags, ["-Wall"]) + self.assertEqual(flags.include_flags, ["-I."]) + self.assertEqual(flags.link_flags, ["-pthread"]) + self.assertEqual(flags.strict_mode_flags, ["-Werror"]) + + # Check tools (from [tools] section) - modern command-based approach + self.assertEqual(flags.tools.c_compiler, ["clang"]) + self.assertEqual( + flags.tools.linker, ["uv", "run", "python", "-m", "ziglang", "c++"] + ) + self.assertEqual( + flags.tools.cpp_compiler, + ["uv", "run", "python", "-m", "ziglang", "c++"], + ) + self.assertEqual( + flags.tools.objcopy, ["uv", "run", "python", "-m", "ziglang", "objcopy"] + ) + self.assertEqual(flags.tools.nm, ["uv", "run", "python", "-m", "ziglang", "nm"]) + self.assertEqual( + flags.tools.strip, ["uv", "run", "python", "-m", "ziglang", "strip"] + ) + self.assertEqual( + flags.tools.ranlib, ["uv", "run", "python", "-m", "ziglang", "ranlib"] + ) + self.assertEqual( + flags.tools.archiver, ["uv", "run", "python", "-m", "ziglang", "ar"] + ) + + def test_parse_toml_with_tools(self) -> None: + """Test parsing TOML file with [tools] section""" + toml_content = """ +[all] +defines = ["-DTEST=1"] +compiler_flags = ["-Wall"] +include_flags = ["-I."] + +[tools] +cpp_compiler = ["g++"] +archiver = ["gcc-ar"] +linker = ["ld.gold"] +c_compiler = ["gcc"] +objcopy = ["arm-objcopy"] +nm = ["arm-nm"] +strip = ["arm-strip"] +ranlib = ["arm-ranlib"] + +[archive] +flags = "rcsD" + +[linking.base] +flags = ["-pthread"] +""" + + test_file = self.create_test_toml(toml_content) + flags = BuildFlags.parse(test_file, quick_build=False, strict_mode=False) + + # Check that tools were parsed correctly - modern command-based approach + self.assertEqual(flags.tools.cpp_compiler, ["g++"]) + self.assertEqual(flags.tools.archiver, ["gcc-ar"]) + self.assertEqual(flags.tools.linker, ["ld.gold"]) + self.assertEqual(flags.tools.c_compiler, ["gcc"]) + self.assertEqual(flags.tools.objcopy, ["arm-objcopy"]) + self.assertEqual(flags.tools.nm, ["arm-nm"]) + self.assertEqual(flags.tools.strip, ["arm-strip"]) + self.assertEqual(flags.tools.ranlib, ["arm-ranlib"]) + + def test_parse_partial_tools_section(self) -> None: + """Test parsing TOML with all required [tools] section fields""" + toml_content = """ +[all] +defines = ["-DTEST=1"] + +[tools] +cpp_compiler = ["custom-clang++"] +archiver = ["custom-ar"] +linker = ["custom-linker"] +c_compiler = ["clang"] +objcopy = ["custom-objcopy"] +nm = ["custom-nm"] +strip = ["custom-strip"] +ranlib = ["custom-ranlib"] +# All tools must be provided - no defaults allowed + +[archive] +flags = "rcsD" +""" + + test_file = self.create_test_toml(toml_content) + flags = BuildFlags.parse(test_file, quick_build=False, strict_mode=False) + + # Check all tools are set as specified - strict validation + self.assertEqual(flags.tools.cpp_compiler, ["custom-clang++"]) + self.assertEqual(flags.tools.archiver, ["custom-ar"]) + self.assertEqual(flags.tools.linker, ["custom-linker"]) + self.assertEqual(flags.tools.c_compiler, ["clang"]) + self.assertEqual(flags.tools.objcopy, ["custom-objcopy"]) + self.assertEqual(flags.tools.nm, ["custom-nm"]) + self.assertEqual(flags.tools.strip, ["custom-strip"]) + self.assertEqual(flags.tools.ranlib, ["custom-ranlib"]) + + def test_serialize_build_flags_with_tools(self) -> None: + """Test serializing BuildFlags with tools to TOML""" + # Create BuildFlags with custom tools + custom_tools = BuildTools( + linker=["arm-none-eabi-ld"], + c_compiler=["arm-none-eabi-gcc"], + objcopy=["arm-none-eabi-objcopy"], + nm=["arm-none-eabi-nm"], + strip=["arm-none-eabi-strip"], + ranlib=["arm-none-eabi-ranlib"], + cpp_compiler=["arm-none-eabi-g++"], + archiver=["arm-none-eabi-ar"], + ) + + flags = BuildFlags( + defines=["-DARM_BUILD=1"], + compiler_flags=["-mcpu=cortex-m4", "-mthumb"], + include_flags=["-I.", "-Iarm"], + link_flags=["-nostdlib"], + strict_mode_flags=["-Werror"], + tools=custom_tools, + archive=ArchiveOptions(flags="rcsD"), + ) + + # Serialize to TOML + toml_output = flags.serialize() + + # Check that tools section is present with new field names + self.assertIn("[tools]", toml_output) + self.assertIn("cpp_compiler = ['arm-none-eabi-g++']", toml_output) + self.assertIn("archiver = ['arm-none-eabi-ar']", toml_output) + self.assertIn("linker = ['arm-none-eabi-ld']", toml_output) + self.assertIn("c_compiler = ['arm-none-eabi-gcc']", toml_output) + self.assertIn("objcopy = ['arm-none-eabi-objcopy']", toml_output) + self.assertIn("nm = ['arm-none-eabi-nm']", toml_output) + self.assertIn("strip = ['arm-none-eabi-strip']", toml_output) + self.assertIn("ranlib = ['arm-none-eabi-ranlib']", toml_output) + + def test_serialize_with_none_linker(self) -> None: + """Test serializing BuildFlags when linker is None""" + flags = BuildFlags( + defines=["-DTEST=1"], + compiler_flags=[], + include_flags=[], + link_flags=[], + strict_mode_flags=[], + tools=BuildTools( + linker=[], # Empty list instead of None + cpp_compiler=["clang++"], + c_compiler=["clang"], + archiver=[], + objcopy=[], + nm=[], + strip=[], + ranlib=[], + ), + archive=ArchiveOptions(flags="rcsD"), + ) + + toml_output = flags.serialize() + + # Check that tools section is present but linker is omitted + self.assertIn("[tools]", toml_output) + self.assertIn("cpp_compiler = ['clang++']", toml_output) + self.assertNotIn("linker =", toml_output) # Should be omitted when empty + + def test_round_trip_toml_parsing(self) -> None: + """Test that parse -> serialize -> parse maintains data integrity""" + # Create original flags + original_tools = BuildTools( + linker=["test-ld"], + c_compiler=["test-gcc"], + cpp_compiler=["test-compiler"], + archiver=["test-ar"], + objcopy=["test-objcopy"], + nm=["test-nm"], + strip=["test-strip"], + ranlib=["test-ranlib"], + ) + + original_flags = BuildFlags( + defines=["-DROUND_TRIP=1"], + compiler_flags=["-Wall", "-O2"], + include_flags=["-I.", "-Itest"], + link_flags=["-pthread"], + strict_mode_flags=["-Werror"], + tools=original_tools, + archive=ArchiveOptions(flags="rcsD"), + ) + + # Serialize to TOML + toml_content = original_flags.serialize() + + # Write to temporary file + temp_file = self.temp_dir / "roundtrip.toml" + original_flags.to_toml_file(temp_file) + + # Parse back from file + parsed_flags = BuildFlags.parse(temp_file, quick_build=False, strict_mode=False) + + # Check that all data is preserved + self.assertEqual(parsed_flags.defines, original_flags.defines) + self.assertEqual(parsed_flags.compiler_flags, original_flags.compiler_flags) + self.assertEqual(parsed_flags.include_flags, original_flags.include_flags) + self.assertEqual(parsed_flags.link_flags, original_flags.link_flags) + self.assertEqual( + parsed_flags.strict_mode_flags, original_flags.strict_mode_flags + ) + + # Check tools - modern command-based approach + self.assertEqual( + parsed_flags.tools.cpp_compiler, original_flags.tools.cpp_compiler + ) + self.assertEqual(parsed_flags.tools.archiver, original_flags.tools.archiver) + self.assertEqual(parsed_flags.tools.linker, original_flags.tools.linker) + self.assertEqual(parsed_flags.tools.c_compiler, original_flags.tools.c_compiler) + + def test_parse_missing_file(self) -> None: + """Test parsing non-existent TOML file raises FileNotFoundError""" + nonexistent_file = self.temp_dir / "does_not_exist.toml" + + # Should raise FileNotFoundError when file is missing + with self.assertRaises(FileNotFoundError) as context: + BuildFlags.parse(nonexistent_file, quick_build=False, strict_mode=False) + + self.assertIn("Required build_flags.toml not found", str(context.exception)) + + def test_from_toml_file_alias(self) -> None: + """Test that from_toml_file() is an alias for parse()""" + toml_content = """ +[all] +defines = ["-DALIAS_TEST=1"] + +[tools] +cpp_compiler = ["alias-compiler"] +linker = ["alias-linker"] +c_compiler = ["clang"] +objcopy = ["alias-objcopy"] +nm = ["alias-nm"] +strip = ["alias-strip"] +ranlib = ["alias-ranlib"] +archiver = ["alias-archiver"] + +[archive] +flags = "rcsD" +""" + + test_file = self.create_test_toml(toml_content) + + # Parse using both methods + flags_parse = BuildFlags.parse(test_file, quick_build=True, strict_mode=False) + flags_alias = BuildFlags.from_toml_file( + test_file, quick_build=True, strict_mode=False + ) + + # Should be identical + self.assertEqual(flags_parse.defines, flags_alias.defines) + self.assertEqual(flags_parse.tools.cpp_compiler, flags_alias.tools.cpp_compiler) + self.assertEqual(flags_parse.tools.cpp_compiler, ["alias-compiler"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_buildflags.py b/v1.5/lib/FastLED/ci/tests/test_buildflags.py new file mode 100644 index 0000000..49a3257 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_buildflags.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +""" +Tests for BuildFlags TOML parsing in ci/ directory. + +This test discovers all top-level *.toml files under the project ci/ folder +and verifies they can be parsed by BuildFlags.parse without throwing. +""" + +import unittest +from pathlib import Path +from typing import List + +from ci.compiler.clang_compiler import BuildFlags + + +def find_ci_toml_files(ci_dir: Path) -> List[Path]: + """Return a list of top-level *.toml files in ci/. + + Args: + ci_dir: Absolute path to the ci/ directory + + Returns: + List of absolute paths to .toml files directly under ci/. + """ + toml_files: List[Path] = [] + for entry in ci_dir.iterdir(): + if entry.is_file() and entry.suffix == ".toml": + toml_files.append(entry.resolve()) + return toml_files + + +class TestBuildFlagsParsing(unittest.TestCase): + def test_parse_all_ci_tomls(self) -> None: + project_root: Path = Path(__file__).resolve().parent.parent.parent + ci_dir: Path = project_root / "ci" + self.assertTrue(ci_dir.exists(), f"ci directory not found at {ci_dir}") + + toml_files: List[Path] = find_ci_toml_files(ci_dir) + self.assertTrue(len(toml_files) > 0, "No .toml files found in ci/") + + for toml_path in toml_files: + with self.subTest(toml=str(toml_path)): + # Ensure each ci/*.toml parses without throwing + _ = BuildFlags.parse(toml_path, quick_build=True, strict_mode=False) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_elf.py b/v1.5/lib/FastLED/ci/tests/test_elf.py new file mode 100644 index 0000000..5627cc3 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_elf.py @@ -0,0 +1,65 @@ +import subprocess +import unittest +import warnings +from pathlib import Path + +from ci.util.elf import dump_symbol_sizes +from ci.util.paths import PROJECT_ROOT +from ci.util.tools import Tools, load_tools + + +HERE = Path(__file__).resolve().parent.absolute() +UNO = HERE / "uno" +OUTPUT = HERE / "output" +ELF_FILE = UNO / "firmware.elf" +BUILD_INFO_PATH = PROJECT_ROOT / ".build" / "uno" / "build_info.json" +BUILD_INFO_PATH2 = ( + PROJECT_ROOT / ".build" / "fled" / "examples" / "uno" / "build_info.json" +) + + +PLATFORMIO_PATH = Path.home() / ".platformio" +PLATFORMIO_PACKAGES_PATH = PLATFORMIO_PATH / "packages" +TOOLCHAIN_AVR = PLATFORMIO_PACKAGES_PATH / "toolchain-atmelavr" + + +def init() -> None: + uno_build = PROJECT_ROOT / ".build" / "uno" + print(f"Checking for Uno build in: {uno_build}") + if not BUILD_INFO_PATH.exists() or not TOOLCHAIN_AVR.exists(): + print("Uno build not found. Running compilation...") + try: + subprocess.run( + "uv run python -m ci.ci-compile uno --examples Blink", + shell=True, + check=True, + cwd=str(PROJECT_ROOT), + ) + print("Compilation completed successfully.") + except subprocess.CalledProcessError as e: + print(f"Error during compilation: {e}") + raise + + +class TestBinToElf(unittest.TestCase): + def test_bin_to_elf_conversion(self) -> None: + # Skip test if required UNO build directory is missing + uno_build_dir = PROJECT_ROOT / ".build" / "uno" + if not uno_build_dir.exists(): + warnings.warn( + "Skipping TestBinToElf::test_bin_to_elf_conversion because .build/uno does not exist. " + "Run 'uv run ci/ci-compile.py uno --examples Blink' to generate it." + ) + self.skipTest(".build/uno missing; skipping ELF conversion test") + tools: Tools + try: + tools = load_tools(BUILD_INFO_PATH) + except Exception as e: + warnings.warn(f"Error while loading tools: {e}") + tools = load_tools(BUILD_INFO_PATH2) + msg = dump_symbol_sizes(tools.nm_path, tools.cpp_filt_path, ELF_FILE) + print(msg) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_fingerprint_cache.py b/v1.5/lib/FastLED/ci/tests/test_fingerprint_cache.py new file mode 100644 index 0000000..cf43f2a --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_fingerprint_cache.py @@ -0,0 +1,450 @@ +""" +Comprehensive test suite for fingerprint cache functionality. + +Tests cover: +- Core functionality (cache hits, misses, content changes) +- Edge cases (touched files, copied files, corruption) +- Performance requirements +- Integration scenarios +""" + +import json +import os +import shutil +import tempfile +import time +from pathlib import Path +from typing import List, Optional +from unittest import TestCase + +# Import the fingerprint cache module +from ci.ci.fingerprint_cache import CacheEntry, FingerprintCache, has_changed + + +class TestFingerprintCache(TestCase): + """Test suite for fingerprint cache functionality.""" + + def setUp(self) -> None: + """Set up test environment with temporary directory.""" + self.test_dir = Path(tempfile.mkdtemp()) + self.cache_dir = self.test_dir / "cache" + self.cache_dir.mkdir(exist_ok=True) + self.temp_dir = self.test_dir / "temp" + self.temp_dir.mkdir(exist_ok=True) + + def tearDown(self) -> None: + """Clean up test environment.""" + shutil.rmtree(self.test_dir, ignore_errors=True) + + def create_test_file( + self, path: Path, content: str, modtime: Optional[float] = None + ) -> Path: + """Create a test file with specific content and optional modification time.""" + with open(path, "w") as f: + f.write(content) + + if modtime: + # Set specific modification time + os.utime(path, (modtime, modtime)) + + return path + + def touch_file(self, path: Path) -> float: + """Touch a file to update its modification time without changing content.""" + # Wait a small amount to ensure modtime changes + time.sleep(0.01) + path.touch() + return os.path.getmtime(path) + + def modify_file_content(self, path: Path, new_content: str) -> float: + """Modify file content and return new modification time.""" + time.sleep(0.01) # Ensure modtime changes + with open(path, "w") as f: + f.write(new_content) + return os.path.getmtime(path) + + def test_cache_hit_modtime_unchanged(self) -> None: + """Test that identical modtime returns False immediately.""" + cache_file = self.cache_dir / "test_cache.json" + cache = FingerprintCache(cache_file) + + # Create test file + test_file = self.temp_dir / "test.txt" + self.create_test_file(test_file, "original content") + original_modtime = os.path.getmtime(test_file) + + # Same modtime should return False (no change detected) + result1 = cache.has_changed(test_file, original_modtime) + self.assertFalse(result1, "Same modtime should return False") + + # Verify fast performance (cache hit) + start_time = time.time() + result2 = cache.has_changed(test_file, original_modtime) + elapsed = time.time() - start_time + + self.assertFalse(result2, "Same modtime should return False") + self.assertLess( + elapsed, 0.002, f"Cache hit should be <2ms, took {elapsed * 1000:.2f}ms" + ) + + def test_cache_hit_with_existing_cache(self) -> None: + """Test cache hit when file exists in cache with current modtime.""" + cache_file = self.cache_dir / "test_cache.json" + cache = FingerprintCache(cache_file) + + # Create and cache a file + test_file = self.temp_dir / "cached.txt" + self.create_test_file(test_file, "cached content") + current_modtime = os.path.getmtime(test_file) + + # Prime the cache + cache.has_changed( + test_file, current_modtime - 1 + ) # Different modtime to force caching + + # Test cache hit + result = cache.has_changed(test_file, current_modtime) + self.assertFalse( + result, "File with current modtime in cache should return False" + ) + + def test_modtime_changed_content_same(self) -> None: + """Critical test: modtime changes but content identical should return False.""" + cache_file = self.cache_dir / "test_cache.json" + cache = FingerprintCache(cache_file) + + # Create test file + test_file = self.temp_dir / "touched.txt" + original_content = "unchanged content" + self.create_test_file(test_file, original_content) + original_modtime = os.path.getmtime(test_file) + + # Prime cache with original state + cache.has_changed(test_file, original_modtime - 1) # Force caching + + # Touch file (change modtime but not content) + new_modtime = self.touch_file(test_file) + self.assertNotEqual( + new_modtime, original_modtime, "Touch should change modtime" + ) + + # Verify content is still the same + with open(test_file, "r") as f: + current_content = f.read() + self.assertEqual( + current_content, original_content, "Content should be unchanged" + ) + + # Test the critical behavior: should return False despite modtime change + result = cache.has_changed(test_file, original_modtime) + self.assertFalse( + result, "File with changed modtime but same content should return False" + ) + + def test_file_copy_different_timestamp(self) -> None: + """Test same file with different timestamps but identical content.""" + cache_file = self.cache_dir / "test_cache.json" + cache = FingerprintCache(cache_file) + + # Create original file + test_file = self.temp_dir / "timestamp_test.txt" + content = "file content for timestamp testing" + self.create_test_file(test_file, content) + original_modtime = os.path.getmtime(test_file) + + # Prime cache with original state + cache.has_changed(test_file, original_modtime - 1) + + # Touch file to change timestamp but keep content same + time.sleep(0.01) + new_modtime = time.time() + os.utime(test_file, (new_modtime, new_modtime)) + + # Verify content is still the same + with open(test_file, "r") as f: + current_content = f.read() + self.assertEqual(current_content, content, "Content should be unchanged") + + # Test: different modtime, same content should return False + result = cache.has_changed(test_file, original_modtime) + self.assertFalse( + result, "File with different timestamp but same content should return False" + ) + + def test_content_actually_changed(self) -> None: + """Test that actual content changes are detected.""" + cache_file = self.cache_dir / "test_cache.json" + cache = FingerprintCache(cache_file) + + # Create test file + test_file = self.temp_dir / "modified.txt" + original_content = "original content" + self.create_test_file(test_file, original_content) + original_modtime = os.path.getmtime(test_file) + + # Prime cache + cache.has_changed(test_file, original_modtime - 1) + + # Actually modify content + new_content = "modified content" + new_modtime = self.modify_file_content(test_file, new_content) + + # Should detect change + result = cache.has_changed(test_file, original_modtime) + self.assertTrue(result, "File with changed content should return True") + + def test_incremental_changes(self) -> None: + """Test detecting changes at different points in time.""" + cache_file = self.cache_dir / "test_cache.json" + cache = FingerprintCache(cache_file) + + test_file = self.temp_dir / "incremental.txt" + + # Version 1 - baseline + content_v1 = "version 1" + self.create_test_file(test_file, content_v1) + modtime_v1 = os.path.getmtime(test_file) + + # Test v1 against earlier time (should be changed) + result = cache.has_changed(test_file, modtime_v1 - 1) + self.assertTrue(result, "File should be detected as changed from earlier time") + + # Test v1 against same time (should be unchanged) + result = cache.has_changed(test_file, modtime_v1) + self.assertFalse(result, "File should be unchanged from same time") + + # Version 2 - modify content + content_v2 = "version 2" + modtime_v2 = self.modify_file_content(test_file, content_v2) + + # Test v2 against v1 time (should be changed) + result = cache.has_changed(test_file, modtime_v1) + self.assertTrue(result, "File should be detected as changed from v1 time") + + # Test v2 against v2 time (should be unchanged) + result = cache.has_changed(test_file, modtime_v2) + self.assertFalse(result, "File should be unchanged from v2 time") + + def test_cache_persistence(self) -> None: + """Test that cache persists across FingerprintCache instances.""" + cache_file = self.cache_dir / "persistent.json" + + # First cache instance + cache1 = FingerprintCache(cache_file) + test_file = self.temp_dir / "persistent.txt" + self.create_test_file(test_file, "persistent content") + modtime = os.path.getmtime(test_file) + + # Prime cache + cache1.has_changed(test_file, modtime - 1) + + # Second cache instance (reload from disk) + cache2 = FingerprintCache(cache_file) + + # Should use cached data + result = cache2.has_changed(test_file, modtime) + self.assertFalse(result, "New cache instance should load existing cache") + + def test_cache_corruption_recovery(self) -> None: + """Test graceful handling of corrupted cache files.""" + cache_file = self.cache_dir / "corrupted.json" + + # Create corrupted cache file + with open(cache_file, "w") as f: + f.write("{ invalid json content") + + # Should handle corruption gracefully + cache = FingerprintCache(cache_file) + test_file = self.temp_dir / "recovery.txt" + self.create_test_file(test_file, "recovery content") + modtime = os.path.getmtime(test_file) + + # Should work despite corrupted cache + result = cache.has_changed(test_file, modtime - 1) + self.assertTrue(result, "Should work with corrupted cache") + + def test_file_not_found_error(self) -> None: + """Test FileNotFoundError for non-existent files.""" + cache_file = self.cache_dir / "test_cache.json" + cache = FingerprintCache(cache_file) + + non_existent_file = self.temp_dir / "does_not_exist.txt" + + with self.assertRaises(FileNotFoundError): + cache.has_changed(non_existent_file, time.time()) + + def test_performance_cache_hit(self) -> None: + """Benchmark cache hit performance.""" + cache_file = self.cache_dir / "perf.json" + cache = FingerprintCache(cache_file) + + # Create test files + test_files: List[Path] = [] + for i in range(10): # Reduced for faster test execution + test_file = self.temp_dir / f"perf_{i}.txt" + self.create_test_file(test_file, f"content {i}") + test_files.append(test_file) + + # Measure cache hit performance + start_time = time.time() + for test_file in test_files: + modtime = os.path.getmtime(test_file) + cache.has_changed(test_file, modtime) # Cache hit + elapsed = time.time() - start_time + + avg_time = elapsed / len(test_files) * 1000 # ms per file + self.assertLess( + avg_time, 1.0, f"Cache hit average {avg_time:.2f}ms should be <1.0ms" + ) + + def test_performance_large_files(self) -> None: + """Test performance with moderately large files.""" + cache_file = self.cache_dir / "large.json" + cache = FingerprintCache(cache_file) + + # Create moderately large test file (100KB) + large_file = self.temp_dir / "large.txt" + large_content = "x" * (100 * 1024) + self.create_test_file(large_file, large_content) + modtime = os.path.getmtime(large_file) + + # Measure MD5 computation time + start_time = time.time() + result = cache.has_changed(large_file, modtime - 1) # Force MD5 computation + elapsed = time.time() - start_time + + self.assertTrue(result, "Large file should be detected as changed") + self.assertLess( + elapsed, + 0.5, + f"Large file processing {elapsed * 1000:.2f}ms should be <500ms", + ) + + def test_convenience_function(self) -> None: + """Test the convenience has_changed function.""" + cache_file = self.cache_dir / "convenience.json" + test_file = self.temp_dir / "convenience.txt" + self.create_test_file(test_file, "convenience test") + modtime = os.path.getmtime(test_file) + + # Test convenience function + result = has_changed(test_file, modtime, cache_file) + self.assertFalse(result, "Convenience function should work correctly") + + def test_cache_stats(self) -> None: + """Test cache statistics functionality.""" + cache_file = self.cache_dir / "stats.json" + cache = FingerprintCache(cache_file) + + # Initially empty cache + stats = cache.get_cache_stats() + self.assertEqual(stats["total_entries"], 0) + self.assertFalse(stats["cache_file_exists"]) + + # Add some entries + test_file = self.temp_dir / "stats.txt" + self.create_test_file(test_file, "stats content") + cache.has_changed(test_file, time.time() - 1) + + # Check stats after entries + stats = cache.get_cache_stats() + self.assertEqual(stats["total_entries"], 1) + self.assertTrue(stats["cache_file_exists"]) + self.assertGreater(stats["cache_file_size_bytes"], 0) + + def test_clear_cache(self) -> None: + """Test cache clearing functionality.""" + cache_file = self.cache_dir / "clear.json" + cache = FingerprintCache(cache_file) + + # Add an entry + test_file = self.temp_dir / "clear.txt" + self.create_test_file(test_file, "clear content") + cache.has_changed(test_file, time.time() - 1) + + # Verify cache has entry + self.assertEqual(len(cache.cache), 1) + self.assertTrue(cache_file.exists()) + + # Clear cache + cache.clear_cache() + + # Verify cache is cleared + self.assertEqual(len(cache.cache), 0) + self.assertFalse(cache_file.exists()) + + def test_build_system_workflow(self) -> None: + """Test complete build system workflow.""" + cache_file = self.cache_dir / "build.json" + cache = FingerprintCache(cache_file) + + # Simulate source files + source_files = [ + self.temp_dir / "main.cpp", + self.temp_dir / "utils.cpp", + self.temp_dir / "config.h", + ] + + # Create initial files + for i, src_file in enumerate(source_files): + self.create_test_file(src_file, f"source content {i}") + + # First build - all files should be "changed" (new) + changed_files: List[Path] = [] + baseline_time = time.time() - 3600 # 1 hour ago + + for src_file in source_files: + if cache.has_changed(src_file, baseline_time): + changed_files.append(src_file) + + self.assertEqual( + len(changed_files), 3, "All new files should be detected as changed" + ) + + # Second build - no changes + current_modtimes = [os.path.getmtime(f) for f in source_files] + changed_files: List[Path] = [] + + for src_file, modtime in zip(source_files, current_modtimes): + if cache.has_changed(src_file, modtime): + changed_files.append(src_file) + + self.assertEqual( + len(changed_files), 0, "Unchanged files should not be detected as changed" + ) + + # Third build - modify one file + self.modify_file_content(source_files[0], "modified main.cpp") + changed_files: List[Path] = [] + + for src_file, modtime in zip(source_files, current_modtimes): + if cache.has_changed(src_file, modtime): + changed_files.append(src_file) + + self.assertEqual(len(changed_files), 1, "Only modified file should be detected") + self.assertEqual( + changed_files[0], source_files[0], "Should detect the correct modified file" + ) + + +def run_all_tests() -> bool: + """Execute all test scenarios.""" + import unittest + + # Create test suite + suite = unittest.TestLoader().loadTestsFromTestCase(TestFingerprintCache) + + # Run tests + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + if result.wasSuccessful(): + print("✅ All fingerprint cache tests passed!") + return True + else: + print("❌ Some tests failed!") + return False + + +if __name__ == "__main__": + run_all_tests() diff --git a/v1.5/lib/FastLED/ci/tests/test_github_actions_security.py b/v1.5/lib/FastLED/ci/tests/test_github_actions_security.py new file mode 100644 index 0000000..96d1435 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_github_actions_security.py @@ -0,0 +1,276 @@ +# pyright: reportUnknownMemberType=false +import os +import unittest +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +import yaml + +from ci.util.paths import PROJECT_ROOT + + +@dataclass +class WorkflowStep: + name: Optional[str] = None + uses: Optional[str] = None + with_config: Optional[Dict[str, Any]] = None + run: Optional[str] = None + + +@dataclass +class WorkflowJob: + runs_on: Optional[Union[str, List[str]]] = None + steps: Optional[List[WorkflowStep]] = None + permissions: Optional[Dict[str, str]] = None + + def __post_init__(self) -> None: + if self.steps is None: + self.steps = [] + + +@dataclass +class WorkflowTrigger: + branches: Optional[List[str]] = None + paths: Optional[List[str]] = None + + +@dataclass +class GitHubWorkflow: + name: Optional[str] = None + on_config: Optional[Dict[str, Any]] = None + jobs: Optional[Dict[str, WorkflowJob]] = None + permissions: Optional[Dict[str, str]] = None + + def __post_init__(self) -> None: + if self.jobs is None: + self.jobs = {} + if self.on_config is None: + self.on_config = {} + + +class TestGitHubActionsSecurityTest(unittest.TestCase): + """ + Security tests for GitHub Actions workflows to prevent known vulnerabilities. + + This test ensures that workflows using pull_request_target have proper + permissions restrictions to prevent the "pwn request" vulnerability: + https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + """ + + def setUp(self): + self.workflows_dir = PROJECT_ROOT / ".github" / "workflows" + self.workflow_files = list(self.workflows_dir.glob("*.yml")) + list( + self.workflows_dir.glob("*.yaml") + ) + + def _load_workflow(self, workflow_path: Path) -> GitHubWorkflow: + """Load and parse a GitHub Actions workflow file into a dataclass.""" + try: + with open(workflow_path, "r", encoding="utf-8") as f: + raw_content: Any = yaml.safe_load(f) + + content: Dict[str, Any] = raw_content or {} + + # Handle the case where 'on' is parsed as boolean True instead of string 'on' + # This happens because 'on' is a YAML boolean keyword + if True in content and "on" not in content: + on_data = content.pop(True) # pyright: ignore[reportArgumentType] + content["on"] = on_data + + # Parse jobs into dataclass + jobs_dict: Dict[str, WorkflowJob] = {} + raw_jobs: Dict[str, Any] = content.get("jobs", {}) + for job_name, job_data in raw_jobs.items(): + if not isinstance(job_data, dict): + continue + + # Parse steps + steps: List[WorkflowStep] = [] + raw_steps: List[Any] = job_data.get("steps", []) # pyright: ignore[reportUnknownVariableType] + for step_data in raw_steps: # pyright: ignore[reportUnknownVariableType] + if isinstance(step_data, dict): + step_dict: Dict[str, Any] = step_data # pyright: ignore[reportUnknownVariableType] + step = WorkflowStep( + name=step_dict.get("name"), + uses=step_dict.get("uses"), + with_config=step_dict.get("with"), + run=step_dict.get("run"), + ) + steps.append(step) + + job_dict: Dict[str, Any] = job_data # pyright: ignore[reportUnknownVariableType] + job = WorkflowJob( + runs_on=job_dict.get("runs-on"), + steps=steps, + permissions=job_dict.get("permissions"), + ) + jobs_dict[job_name] = job + + return GitHubWorkflow( + name=content.get("name"), + on_config=content.get("on", {}), + jobs=jobs_dict, + permissions=content.get("permissions"), + ) + except Exception as e: + self.fail(f"Failed to parse workflow {workflow_path}: {e}") + + def _has_pull_request_target(self, workflow: GitHubWorkflow) -> bool: + """Check if workflow uses pull_request_target trigger.""" + if isinstance(workflow.on_config, list): + return "pull_request_target" in workflow.on_config + elif isinstance(workflow.on_config, dict): + return "pull_request_target" in workflow.on_config + return False + + def _has_untrusted_code_checkout(self, workflow: GitHubWorkflow) -> bool: + """Check if workflow checks out PR code that could be untrusted.""" + if workflow.jobs is None: + return False + + for job in workflow.jobs.values(): + if job.steps is None: + continue + + for step in job.steps: + # Check for actions/checkout with PR head reference + if step.uses and step.uses.startswith("actions/checkout"): + if step.with_config: + ref = step.with_config.get("ref", "") + if "pull_request.head" in ref: + return True + return False + + def _has_explicit_permissions(self, workflow: GitHubWorkflow) -> bool: + """Check if workflow has explicit permissions set.""" + # Check workflow-level permissions + if workflow.permissions: + return True + + # Check job-level permissions + if workflow.jobs is not None: + for job in workflow.jobs.values(): + if job.permissions: + return True + return False + + def _get_permissions(self, workflow: GitHubWorkflow) -> Dict[str, Any]: + """Get the permissions configuration from workflow.""" + permissions: Dict[str, Any] = {} + + # Workflow-level permissions + if workflow.permissions: + permissions.update(workflow.permissions) + + # Job-level permissions (overwrites workflow-level) + if workflow.jobs is not None: + for job in workflow.jobs.values(): + if job.permissions: + permissions.update(job.permissions) + + return permissions + + def _is_safe_permissions(self, permissions: Dict[str, Any]) -> bool: + """Check if permissions are safe (no dangerous write access).""" + # List of dangerous write permissions + dangerous_write_permissions = [ + "contents", # Can modify repository contents + "metadata", # Can modify repository metadata + "packages", # Can publish packages + "pages", # Can deploy to GitHub Pages + "deployments", # Can create deployments + "security-events", # Can create security events + ] + + for perm, value in permissions.items(): + if perm in dangerous_write_permissions and value == "write": + return False + + return True + + def test_pull_request_target_workflows_have_safe_permissions(self) -> None: + """ + Test that all workflows using pull_request_target have explicit + safe permissions to prevent pwn request vulnerabilities. + """ + vulnerable_workflows: List[str] = [] + unsafe_permission_workflows: List[str] = [] + + for workflow_path in self.workflow_files: + workflow = self._load_workflow(workflow_path) + + if not self._has_pull_request_target(workflow): + continue + + workflow_name = workflow_path.name + + # Check if it has untrusted code checkout (potential vulnerability) + if self._has_untrusted_code_checkout(workflow): + if not self._has_explicit_permissions(workflow): + vulnerable_workflows.append(workflow_name) + else: + permissions = self._get_permissions(workflow) + if not self._is_safe_permissions(permissions): + unsafe_permission_workflows.append( + f"{workflow_name}: {permissions}" + ) + + # Report findings + error_messages: List[str] = [] + + if vulnerable_workflows: + error_messages.append( + f"CRITICAL: Found {len(vulnerable_workflows)} workflows with pull_request_target " + f"that checkout untrusted code without explicit permissions:\n" + + "\n".join(f" - {w}" for w in vulnerable_workflows) + + "\n\nThis is a critical security vulnerability! These workflows can be exploited " + "to gain repository write access through malicious PRs." + ) + + if unsafe_permission_workflows: + error_messages.append( + f"UNSAFE: Found {len(unsafe_permission_workflows)} workflows with pull_request_target " + f"that have dangerous write permissions:\n" + + "\n".join(f" - {w}" for w in unsafe_permission_workflows) + + "\n\nThese workflows should use minimal read-only permissions." + ) + + if error_messages: + self.fail( + "\n\n".join(error_messages) + + "\n\nRecommended fix: Add explicit minimal permissions to these workflows:\n" + "permissions:\n" + " contents: read\n" + " actions: read\n" + " id-token: write\n" + " pull-requests: read" + ) + + def test_no_workflow_uses_excessive_permissions(self) -> None: + """ + Test that no workflow uses overly broad permissions like 'write-all'. + """ + excessive_permission_workflows: List[str] = [] + + for workflow_path in self.workflow_files: + workflow = self._load_workflow(workflow_path) + permissions = self._get_permissions(workflow) + + # Check for dangerous permission patterns + for perm, value in permissions.items(): + if value in ["write-all", "admin"]: + excessive_permission_workflows.append( + f"{workflow_path.name}: {perm}={value}" + ) + + if excessive_permission_workflows: + self.fail( + f"Found workflows with excessive permissions:\n" + + "\n".join(f" - {w}" for w in excessive_permission_workflows) + + "\n\nUse minimal required permissions instead." + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_map_parser.py b/v1.5/lib/FastLED/ci/tests/test_map_parser.py new file mode 100644 index 0000000..fb6782d --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_map_parser.py @@ -0,0 +1,25 @@ +import unittest +from pathlib import Path + +from ci.util.map_dump import map_dump + + +HERE = Path(__file__).resolve().parent.absolute() +UNO = HERE / "uno" + + +class TestMapParser(unittest.TestCase): + def test_map_parser(self): + # If the UNO fixture directory is missing, skip with a warning + if not UNO.exists(): + import warnings + + warnings.warn( + "Skipping TestMapParser::test_map_parser because ci/tests/uno is missing." + ) + self.skipTest("ci/tests/uno missing; skipping map parser test") + map_dump(UNO / "firmware.map") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_missing_pragma_once.py b/v1.5/lib/FastLED/ci/tests/test_missing_pragma_once.py new file mode 100644 index 0000000..3f0806a --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_missing_pragma_once.py @@ -0,0 +1,117 @@ +# pyright: reportUnknownMemberType=false +import os +import unittest +from concurrent.futures import ThreadPoolExecutor +from typing import List + +from ci.util.paths import PROJECT_ROOT + + +SRC_ROOT = PROJECT_ROOT / "src" + +NUM_WORKERS = 1 if os.environ.get("NO_PARALLEL") else (os.cpu_count() or 1) * 4 + +# Files that are allowed to not have #pragma once +EXCLUDED_FILES: List[str] = [ + # Add any exceptions here +] + +EXCLUDED_DIRS = [ + "third_party", + "platforms", +] + + +class TestMissingPragmaOnce(unittest.TestCase): + def check_file(self, file_path: str) -> list[str]: + """Check if a header file has #pragma once directive or if a cpp file incorrectly has it.""" + failings: list[str] = [] + + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + if file_path.endswith(".h"): + # For header files, check if #pragma once is missing + if "#pragma once" not in content: + failings.append(f"Missing #pragma once in {file_path}") + elif file_path.endswith(".cpp"): + # For cpp files, check if #pragma once is incorrectly present + if "#pragma once" in content: + failings.append(f"Incorrect #pragma once in cpp file: {file_path}") + + return failings + + def test_pragma_once_usage(self) -> None: + """ + Searches through files to: + 1. Check for missing #pragma once in header files + 2. Check for incorrect #pragma once in cpp files + """ + files_to_check: List[str] = [] + current_dir = None + + # Collect files to check + for root, dirs, files in os.walk(SRC_ROOT): + # Log when we enter a new directory + rel_path = os.path.relpath(root, SRC_ROOT) + if current_dir != rel_path: + current_dir = rel_path + if rel_path in EXCLUDED_DIRS: + dirs[:] = [] # Skip this directory and its subdirectories + continue + + # Check if this directory should be excluded + # if any(os.path.normpath(root).startswith(os.path.normpath(excluded_dir)) + # for excluded_dir in EXCLUDED_DIRS): + # print(f" Skipping excluded directory: {rel_path}") + # continue + for excluded_dir in EXCLUDED_DIRS: + npath = os.path.normpath(root) + npath_excluded = os.path.normpath(excluded_dir) + if npath.startswith(npath_excluded): + break + + for file in files: + if file.endswith((".h", ".cpp")): # Check both header and cpp files + file_path = os.path.join(root, file) + + # Check if file is excluded + # if any(file_path.endswith(excluded) for excluded in EXCLUDED_FILES): + # print(f" Skipping excluded file: {file}") + # continue + for excluded in EXCLUDED_FILES: + # print(f"Checking {file_path} against excluded {excluded}") + if file_path.endswith(excluded): + print(f" Skipping excluded file: {file}") + break + + files_to_check.append(file_path) + + print(f"Found {len(files_to_check)} files to check") + + # Process files in parallel + all_failings: List[str] = [] + with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor: + futures = [ + executor.submit(self.check_file, file_path) + for file_path in files_to_check + ] + for future in futures: + all_failings.extend(future.result()) + + # Report results + if all_failings: + msg = f"Found {len(all_failings)} pragma once issues: \n" + "\n".join( + all_failings + ) + for failing in all_failings: + print(failing) + self.fail(msg) + else: + print("All files have proper pragma once usage.") + + print(f"Pragma once check completed. Processed {len(files_to_check)} files.") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_no_banned_headers.py b/v1.5/lib/FastLED/ci/tests/test_no_banned_headers.py new file mode 100644 index 0000000..a0d32aa --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_no_banned_headers.py @@ -0,0 +1,207 @@ +# pyright: reportUnknownMemberType=false +import os +import unittest +from typing import Callable, List + +from ci.util.check_files import ( + EXCLUDED_FILES, + FileContent, + FileContentChecker, + MultiCheckerFileProcessor, + collect_files_to_check, +) +from ci.util.paths import PROJECT_ROOT + + +SRC_ROOT = PROJECT_ROOT / "src" + +ENABLE_PARANOID_GNU_HEADER_INSPECTION = False + +if ENABLE_PARANOID_GNU_HEADER_INSPECTION: + BANNED_HEADERS_ESP = ["esp32-hal.h"] +else: + BANNED_HEADERS_ESP = [] + +BANNED_HEADERS_COMMON = [ + "pthread.h", + "assert.h", + "iostream", + "stdio.h", + "cstdio", + "cstdlib", + "vector", + "list", + "map", + "set", + "queue", + "deque", + "algorithm", + "memory", + "thread", + "mutex", + "chrono", + "fstream", + "sstream", + "iomanip", + "exception", + "stdexcept", + "typeinfo", + "ctime", + "cmath", + "complex", + "valarray", + "cfloat", + "cassert", + "cerrno", + "cctype", + "cwctype", + "cstring", + "cwchar", + "cuchar", + "cstdint", + "stdint.h", + "stddef.h", + "cstddef", # this certainally fails + "type_traits", # this certainally fails +] + +BANNED_HEADERS_CORE = BANNED_HEADERS_COMMON + BANNED_HEADERS_ESP + ["Arduino.h"] + +# Banned headers for platforms directory - specifically checking for Arduino.h +BANNED_HEADERS_PLATFORMS = ["Arduino.h"] + + +class BannedHeadersChecker(FileContentChecker): + """Checker class for banned headers.""" + + def __init__(self, banned_headers_list: List[str]): + """Initialize with the list of banned headers to check for.""" + self.banned_headers_list = banned_headers_list + + def should_process_file(self, file_path: str) -> bool: + """Check if file should be processed for banned headers.""" + # Check file extension + if not file_path.endswith((".cpp", ".h", ".hpp", ".ino")): + return False + + # Check if file is in excluded list + if any(file_path.endswith(excluded) for excluded in EXCLUDED_FILES): + return False + + return True + + def check_file_content(self, file_content: FileContent) -> List[str]: + """Check file content for banned headers.""" + failings: List[str] = [] + + if len(self.banned_headers_list) == 0: + return failings + + # Check each line for banned headers + for line_number, line in enumerate(file_content.lines, 1): + if line.strip().startswith("//"): + continue + + for header in self.banned_headers_list: + if ( + f"#include <{header}>" in line or f'#include "{header}"' in line + ) and "// ok include" not in line: + failings.append( + f"Found banned header '{header}' in {file_content.path}:{line_number}" + ) + + return failings + + +def _test_no_banned_headers( + test_directories: List[str], + banned_headers_list: List[str], + on_fail: Callable[[str], None], +) -> None: + """Searches through the program files to check for banned headers.""" + # Collect files to check + files_to_check = collect_files_to_check(test_directories) + + # Create processor and checker + processor = MultiCheckerFileProcessor() + checker = BannedHeadersChecker(banned_headers_list) + + # Process files + results = processor.process_files_with_checkers(files_to_check, [checker]) + + # Get results for banned headers checker + all_failings = results.get("BannedHeadersChecker", []) or [] + + if all_failings: + msg = f"Found {len(all_failings)} banned header(s): \n" + "\n".join( + all_failings + ) + for failing in all_failings: + print(failing) + + on_fail(msg) + else: + print("No banned headers found.") + + +class TestNoBannedHeaders(unittest.TestCase): + def test_no_banned_headers_src(self) -> None: + """Searches through the program files to check for banned headers.""" + + def on_fail(msg: str) -> None: + self.fail( + msg + "\n" + "You can add '// ok include' at the end of the line to silence this error for specific inclusions." + ) + + # Test directories as requested + test_directories = [ + os.path.join(SRC_ROOT, "fl"), + os.path.join(SRC_ROOT, "fx"), + os.path.join(SRC_ROOT, "sensors"), + ] + _test_no_banned_headers( + test_directories=test_directories, + banned_headers_list=BANNED_HEADERS_CORE, + on_fail=on_fail, + ) + + def test_no_banned_headers_examples(self) -> None: + """Searches through the program files to check for banned headers.""" + + def on_fail(msg: str) -> None: + self.fail( + msg + "\n" + "You can add '// ok include' at the end of the line to silence this error for specific inclusions." + ) + + test_directories = ["examples"] + + _test_no_banned_headers( + test_directories=test_directories, + banned_headers_list=BANNED_HEADERS_COMMON, + on_fail=on_fail, + ) + + def test_no_banned_headers_platforms(self) -> None: + """Searches through the platforms directory to check for Arduino.h usage.""" + + def on_fail(msg: str) -> None: + self.fail( + msg + "\n" + "You can add '// ok include' at the end of the line to silence this error for specific inclusions." + ) + + # Test the platforms directory specifically for Arduino.h + test_directories = [ + os.path.join(SRC_ROOT, "platforms"), + ] + _test_no_banned_headers( + test_directories=test_directories, + banned_headers_list=BANNED_HEADERS_PLATFORMS, + on_fail=on_fail, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_no_include_after_namespace.py b/v1.5/lib/FastLED/ci/tests/test_no_include_after_namespace.py new file mode 100644 index 0000000..ea99e18 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_no_include_after_namespace.py @@ -0,0 +1,190 @@ +import os +import re +import unittest +from typing import Dict, List, Tuple + +from ci.util.paths import PROJECT_ROOT + + +SRC_ROOT = PROJECT_ROOT / "src" +EXAMPLES_ROOT = PROJECT_ROOT / "examples" +TESTS_ROOT = PROJECT_ROOT / "tests" + +# Skip patterns for directories that contain third-party code or build artifacts +SKIP_PATTERNS = [ + ".venv", + "node_modules", + "build", + ".build", + "third_party", + "ziglang", + "greenlet", + ".git", +] + +# Regex patterns as triple-quoted constants +NAMESPACE_PATTERN = re.compile( + r""" + ^\s* # Start of line with optional whitespace + ( # Capture group for namespace patterns + namespace\s+\w+ # namespace followed by identifier + | # OR + namespace\s*\{ # namespace followed by optional whitespace and { + ) +""", + re.VERBOSE, +) + +INCLUDE_PATTERN = re.compile( + r""" + ^\s* # Start of line with optional whitespace + \#\s* # Hash with optional whitespace + include\s* # include with optional whitespace + [<"] # Opening bracket or quote + .* # Anything in between + [>"] # Closing bracket or quote +""", + re.VERBOSE, +) + +ALLOW_DIRECTIVE_PATTERN = re.compile(r"//\s*allow-include-after-namespace") + + +def find_includes_after_namespace(file_path: str) -> List[Tuple[int, str]]: + """ + Check if a C++ file has #include directives after namespace declarations. + + Args: + file_path (str): Path to the C++ file to check + + Returns: + List[Tuple[int, str]]: List of tuples containing line numbers and line content + where includes appear after namespaces. Returns empty list + if no violations found or file cannot be decoded. + """ + try: + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + except UnicodeDecodeError: + # Skip files that can't be decoded as UTF-8 + return [] + + # Check if the file has the allow directive + for line in lines: + if ALLOW_DIRECTIVE_PATTERN.search(line): + return [] # Return empty list if directive is found + + namespace_started = False + violations: List[Tuple[int, str]] = [] + + for i, line in enumerate(lines, 1): + # Check if we're entering a namespace + if NAMESPACE_PATTERN.match(line): + namespace_started = True + continue + + # Check for includes after namespace started + if namespace_started and INCLUDE_PATTERN.match(line): + violations.append((i, line.rstrip("\n"))) + + return violations + + +def scan_cpp_files(directory: str = ".") -> Dict[str, List[Tuple[int, str]]]: + """ + Scan all C++ files in a directory for includes after namespace declarations. + + Args: + directory (str): Directory to scan for C++ files + + Returns: + Dict[str, List[Tuple[int, str]]]: Dictionary mapping file paths to list of tuples + (line numbers, line content) of violations + """ + violations: Dict[str, List[Tuple[int, str]]] = {} + + # Find all C++ files + cpp_extensions = [".cpp", ".h", ".hpp", ".cc", ".ino"] + + for root, dirs, files in os.walk(directory): + # Skip directories with third-party code + if any(pattern in root for pattern in SKIP_PATTERNS): + continue + + for file in files: + if any(file.endswith(ext) for ext in cpp_extensions): + file_path = os.path.join(root, file) + try: + line_info = find_includes_after_namespace(file_path) + if line_info: + violations[file_path] = line_info + except Exception as e: + print(f"Error processing {file_path}: {e}") + + return violations + + +class TestNoIncludeAfterNamespace(unittest.TestCase): + def test_no_includes_after_namespace_in_src(self) -> None: + """Check that src/ directory doesn't have includes after namespace declarations.""" + violations = scan_cpp_files(str(SRC_ROOT)) + + if violations: + msg = "Found includes after namespace declarations in src/:\n" + for file_path, line_info in violations.items(): + msg += f" {file_path}:\n" + for line_num, line_content in line_info: + msg += f" Line {line_num}: {line_content}\n" + self.fail( + msg + + "\nPlease fix these issues by moving includes to the top of the file.\n" + "See TEST_NAMESPACE_INCLUDES.md for more information." + ) + else: + print( + "No violations found in src/! All includes are properly placed before namespace declarations." + ) + + def test_no_includes_after_namespace_in_examples(self) -> None: + """Check that examples/ directory doesn't have includes after namespace declarations.""" + violations = scan_cpp_files(str(EXAMPLES_ROOT)) + + if violations: + msg = "Found includes after namespace declarations in examples/:\n" + for file_path, line_info in violations.items(): + msg += f" {file_path}:\n" + for line_num, line_content in line_info: + msg += f" Line {line_num}: {line_content}\n" + self.fail( + msg + + "\nPlease fix these issues by moving includes to the top of the file.\n" + "See TEST_NAMESPACE_INCLUDES.md for more information." + ) + else: + print( + "No violations found in examples/! All includes are properly placed before namespace declarations." + ) + + def test_no_includes_after_namespace_in_tests(self) -> None: + """Check that tests/ directory doesn't have includes after namespace declarations.""" + violations = scan_cpp_files(str(TESTS_ROOT)) + + if violations: + msg = "Found includes after namespace declarations in tests/:\n" + for file_path, line_info in violations.items(): + msg += f" {file_path}:\n" + for line_num, line_content in line_info: + msg += f" Line {line_num}: {line_content}\n" + self.fail( + msg + + "\nPlease fix these issues by moving includes to the top of the file.\n" + "See TEST_NAMESPACE_INCLUDES.md for more information." + ) + else: + print( + "No violations found in tests/! All includes are properly placed before namespace declarations." + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_package.py b/v1.5/lib/FastLED/ci/tests/test_package.py new file mode 100644 index 0000000..a1aa9f6 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_package.py @@ -0,0 +1,220 @@ +""" +Comprehensive tests for enhanced Arduino package index implementation with Pydantic + +Tests all Pydantic models, validation rules, parsing functionality, and error handling. +""" + +import json +from pathlib import Path +from typing import Any, Dict + +import pytest +from pydantic import ValidationError + +# Import the enhanced implementation +from ci.compiler.packages import ( + Board, + Help, + Package, + PackageIndex, + PackageIndexParser, + PackageManagerConfig, + PackageParsingError, + Platform, + SystemDownload, + Tool, + ToolDependency, + format_size, +) + + +class TestHelp: + """Test Help model validation""" + + def test_valid_help(self): + """Test valid help creation""" + help_data = {"online": "https://github.com/espressif/arduino-esp32"} + help_obj = Help(**help_data) # type: ignore + assert str(help_obj.online) == "https://github.com/espressif/arduino-esp32" + + def test_invalid_url(self): + """Test invalid URL validation""" + with pytest.raises(ValidationError): + Help(online="not-a-valid-url") # type: ignore + + +class TestBoard: + """Test Board model validation""" + + def test_valid_board(self): + """Test valid board creation""" + board_data = { + "name": "ESP32 Dev Module", + "properties": { + "upload.tool": "esptool_py", + "upload.maximum_size": "1310720", + }, + } + board = Board(**board_data) # type: ignore + assert board.name == "ESP32 Dev Module" + assert board.properties["upload.tool"] == "esptool_py" + + def test_empty_name_validation(self): + """Test empty name validation""" + with pytest.raises(ValidationError): + Board(name="", properties={}) + + def test_name_trimming(self): + """Test name trimming functionality""" + board = Board(name=" ESP32 Dev Module ", properties={}) + assert board.name == "ESP32 Dev Module" + + +class TestSystemDownload: + """Test SystemDownload model validation""" + + def test_size_conversion_from_string(self): + """Test size conversion from string bytes to MB""" + system = SystemDownload( + host="test-host", + url="https://example.com/file.tar.gz", # type: ignore + archiveFileName="file.tar.gz", + checksum="SHA-256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + size="52428800", # type: ignore - 50 MB in bytes, validator converts + ) + assert abs(system.size_mb - 50.0) < 0.1 # Should be ~50 MB + + def test_invalid_checksum_format(self): + """Test invalid checksum format validation""" + with pytest.raises(ValidationError): + SystemDownload( + host="test-host", + url="https://example.com/file.tar.gz", # type: ignore + archiveFileName="file.tar.gz", + checksum="invalid-checksum", + size=50.0, # type: ignore + ) + + +class TestPlatform: + """Test Platform model validation""" + + def test_valid_platform(self): + """Test valid platform creation""" + platform_data: Dict[str, Any] = { + "name": "ESP32 Arduino", + "architecture": "esp32", + "version": "2.0.5", + "category": "ESP32", + "url": "https://github.com/espressif/arduino-esp32/releases/download/2.0.5/esp32-2.0.5.zip", + "archiveFileName": "esp32-2.0.5.zip", + "checksum": "SHA-256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "size": "50000000", + "boards": [], + "toolsDependencies": [], + "help": {"online": "https://github.com/espressif/arduino-esp32"}, + } + platform = Platform(**platform_data) # type: ignore + assert platform.name == "ESP32 Arduino" + assert platform.architecture == "esp32" + assert platform.size_mb == 50000000 / (1024 * 1024) + + def test_invalid_archive_extension(self): + """Test invalid archive extension validation""" + with pytest.raises(ValidationError): + Platform( + name="Test Platform", + architecture="test", + version="1.0.0", + category="Test", + url="https://example.com/file.txt", # type: ignore + archiveFileName="file.txt", # Invalid extension + checksum="SHA-256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + size=50.0, # type: ignore + boards=[], + toolsDependencies=[], + help=Help(online="https://example.com"), # type: ignore + ) + + +class TestPackageIndexParser: + """Test PackageIndexParser functionality""" + + def test_parse_valid_json(self): + """Test parsing valid package index JSON""" + valid_json: Dict[str, Any] = { + "packages": [ + { + "name": "test", + "maintainer": "Test Maintainer", + "websiteURL": "https://example.com", + "email": "test@example.com", + "help": {"online": "https://example.com"}, + "platforms": [], + "tools": [], + } + ] + } + + parser = PackageIndexParser() + package_index = parser.parse_package_index(json.dumps(valid_json)) + assert len(package_index.packages) == 1 + assert package_index.packages[0].name == "test" + + def test_parse_invalid_json(self): + """Test parsing invalid JSON""" + parser = PackageIndexParser() + + with pytest.raises(PackageParsingError): + parser.parse_package_index("invalid json") + + +class TestUtilityFunctions: + """Test utility functions""" + + def test_format_size(self): + """Test size formatting function""" + # Test KB + assert format_size(0.5) == "512.0 KB" + + # Test MB + assert format_size(1.5) == "1.5 MB" + assert format_size(512.0) == "512.0 MB" + + # Test GB + assert format_size(1536.0) == "1.5 GB" + assert format_size(2048.0) == "2.0 GB" + + +class TestRealDataParsing: + """Test with real ESP32 package index data (if network available)""" + + def test_esp32_package_parsing(self): + """Test parsing real ESP32 package index""" + ESP32_URL = "https://espressif.github.io/arduino-esp32/package_esp32_index.json" + + try: + parser = PackageIndexParser(timeout=10) + package_index = parser.parse_from_url(ESP32_URL) + + # Basic validation + assert len(package_index.packages) > 0 + + esp32_package = package_index.packages[0] + assert esp32_package.name == "esp32" + assert len(esp32_package.platforms) > 0 + assert len(esp32_package.tools) > 0 + + print(f"✅ Successfully parsed ESP32 package index:") + print(f" 📦 Packages: {len(package_index.packages)}") + print(f" 🛠️ Platforms: {len(esp32_package.platforms)}") + print(f" 🔧 Tools: {len(esp32_package.tools)}") + + except Exception as e: + # Skip if network not available + pytest.skip(f"Network test skipped: {e}") + + +if __name__ == "__main__": + # Run tests + pytest.main([__file__, "-v"]) diff --git a/v1.5/lib/FastLED/ci/tests/test_platformio_ini_class.py b/v1.5/lib/FastLED/ci/tests/test_platformio_ini_class.py new file mode 100644 index 0000000..8d5caaa --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_platformio_ini_class.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +""" +Test the PlatformIOIni class functionality. +""" + +import tempfile +import unittest +from pathlib import Path + +from ci.compiler.platformio_ini import PlatformIOIni + + +# Test data constants +BASIC_INI_CONTENT = """[platformio] +src_dir = src + +[env:esp32dev] +board = esp32dev +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino + +[env:uno] +board = uno +platform = atmelavr +framework = arduino +""" + +INVALID_INI_CONTENT = """[platformio] +src_dir = src + +[env:missing_board] +platform = atmelavr +framework = arduino + +[env:missing_platform] +board = uno +framework = arduino +""" + +DICT_TEST_INI_CONTENT = """[platformio] +src_dir = src + +[env:test] +board = esp32dev +platform = espressif32 +framework = arduino +""" + +STRING_TEST_INI_CONTENT = """[platformio] +src_dir = src + +[env:test] +board = esp32dev +platform = espressif32 +""" + +PARSE_TEST_INI_CONTENT = """[platformio] +src_dir = src + +[env:test] +board = esp32dev +platform = espressif32 +framework = arduino +""" + +ESP32_PLATFORM_URL = "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip" +NEW_CACHED_URL = "file:///path/to/cached/platform" + + +class TestPlatformIOIni(unittest.TestCase): + """Test the PlatformIOIni class.""" + + def setUp(self): + """Set up test environment.""" + self.temp_dir = Path(tempfile.mkdtemp()) + self.test_ini = self.temp_dir / "platformio.ini" + + def tearDown(self): + """Clean up test environment.""" + import shutil + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir) + + def test_parse_and_dump(self): + """Test basic parse and dump functionality.""" + # Create a test platformio.ini file + self.test_ini.write_text(BASIC_INI_CONTENT) + + # Parse the file + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Test basic functionality + self.assertIn("env:esp32dev", pio_ini.get_sections()) + self.assertIn("env:uno", pio_ini.get_sections()) + self.assertEqual(len(pio_ini.get_env_sections()), 2) + + # Test option access + self.assertEqual(pio_ini.get_option("env:esp32dev", "board"), "esp32dev") + self.assertEqual(pio_ini.get_option("env:uno", "platform"), "atmelavr") + + # Test URL extraction + platform_urls = pio_ini.get_platform_urls() + self.assertEqual(len(platform_urls), 2) + + # Find the ESP32 platform URL + esp32_url = None + for section, option, url in platform_urls: + if section == "env:esp32dev": + esp32_url = url + break + + self.assertEqual(esp32_url, ESP32_PLATFORM_URL) + + # Test URL replacement + self.assertIsNotNone(esp32_url) # Ensure esp32_url is not None + result = pio_ini.replace_url( + "env:esp32dev", "platform", esp32_url or "", NEW_CACHED_URL + ) + self.assertTrue(result) + self.assertEqual(pio_ini.get_option("env:esp32dev", "platform"), NEW_CACHED_URL) + + # Test dump + output_file = self.temp_dir / "output.ini" + pio_ini.dump(output_file) + + # Verify the file was written + self.assertTrue(output_file.exists()) + + # Parse the output and verify the change + pio_ini2 = PlatformIOIni.parseFile(output_file) + self.assertEqual( + pio_ini2.get_option("env:esp32dev", "platform"), NEW_CACHED_URL + ) + self.assertEqual(pio_ini2.get_option("env:uno", "platform"), "atmelavr") + + def test_validation(self): + """Test validation functionality.""" + # Create an invalid platformio.ini + self.test_ini.write_text(INVALID_INI_CONTENT) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + issues = pio_ini.validate_structure() + self.assertEqual(len(issues), 2) + self.assertIn("missing 'board' option", issues[0]) + self.assertIn("missing required 'platform' option", issues[1]) + + def test_dict_conversion(self): + """Test to_dict and from_dict functionality.""" + self.test_ini.write_text(DICT_TEST_INI_CONTENT) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Convert to dict + config_dict = pio_ini.to_dict() + self.assertIn("env:test", config_dict) + self.assertEqual(config_dict["env:test"]["board"], "esp32dev") + + # Create new instance from dict + pio_ini2 = PlatformIOIni.create() + pio_ini2.from_dict(config_dict) + + self.assertEqual(pio_ini2.get_option("env:test", "board"), "esp32dev") + self.assertEqual(pio_ini2.get_option("env:test", "platform"), "espressif32") + + def test_string_representation(self): + """Test string representation.""" + self.test_ini.write_text(STRING_TEST_INI_CONTENT) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + str_repr = str(pio_ini) + self.assertIn("[platformio]", str_repr) + self.assertIn("src_dir = src", str_repr) + self.assertIn("[env:test]", str_repr) + + def test_static_parse_method(self): + """Test static parse factory method.""" + self.test_ini.write_text(PARSE_TEST_INI_CONTENT) + + # Test static parse method + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Verify it worked correctly + self.assertEqual(pio_ini.get_option("env:test", "board"), "esp32dev") + self.assertEqual(pio_ini.get_option("env:test", "platform"), "espressif32") + self.assertEqual(pio_ini.get_option("platformio", "src_dir"), "src") + + # Verify file_path is set correctly + self.assertEqual(pio_ini.file_path, self.test_ini) + + def test_parse_string_method(self): + """Test static parseString factory method.""" + # Test static parseString method + pio_ini = PlatformIOIni.parseString(PARSE_TEST_INI_CONTENT) + + # Verify it worked correctly + self.assertEqual(pio_ini.get_option("env:test", "board"), "esp32dev") + self.assertEqual(pio_ini.get_option("env:test", "platform"), "espressif32") + self.assertEqual(pio_ini.get_option("platformio", "src_dir"), "src") + + # Verify file_path is None since parsed from string + self.assertIsNone(pio_ini.file_path) + + def test_optimize_method(self): + """Test the optimize() method that downloads and caches packages.""" + # Create content with a real zip URL + test_content = """[platformio] +src_dir = src + +[env:esp32dev] +board = esp32dev +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino + +[env:esp32c3] +board = esp32-c3-devkitm-1 +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino +""" + + # Parse from string + pio_ini = PlatformIOIni.parseString(test_content) + + # Verify original URL is present + original_str = str(pio_ini) + self.assertIn("https://github.com/pioarduino", original_str) + + # Run optimize with test cache instance + from pathlib import Path + + from ci.compiler.platformio_cache import PlatformIOCache + + cache_dir = Path(".cache/tests/test_platformio_ini_optimize") + cache = PlatformIOCache(cache_dir) + pio_ini.optimize(cache) + + # Verify URLs were replaced + optimized_str = str(pio_ini) + self.assertNotIn("https://github.com/pioarduino", optimized_str) + # On Windows, paths are returned directly, not as file:// URLs + import platform + + if platform.system() == "Windows": + self.assertIn( + "extracted", optimized_str + ) # Check for the extracted directory + else: + self.assertIn("file:///", optimized_str) + + # Verify cache directory was created + self.assertTrue(cache_dir.exists()) + + # Verify both environments have the same local path + esp32dev_platform = pio_ini.get_option("env:esp32dev", "platform") + esp32c3_platform = pio_ini.get_option("env:esp32c3", "platform") + self.assertEqual(esp32dev_platform, esp32c3_platform) + # On Windows, paths are returned directly, not as file:// URLs + import platform + + if platform.system() == "Windows": + self.assertTrue(esp32dev_platform.endswith("extracted")) + else: + self.assertTrue(esp32dev_platform.startswith("file:///")) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_platformio_url_resolution.py b/v1.5/lib/FastLED/ci/tests/test_platformio_url_resolution.py new file mode 100644 index 0000000..fa6ab48 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_platformio_url_resolution.py @@ -0,0 +1,1617 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test the PlatformIO URL resolution functionality. + +This module uses UTF-8 encoding to support emoji output on all platforms, +including Windows. The debug output includes emoji symbols for visual clarity +but falls back gracefully on systems that don't support them. +""" + +import json +import os +import sys +import tempfile +import unittest +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, patch + + +# Ensure UTF-8 output on Windows and other platforms (only when running directly) +def _setup_utf8_output(): + """Setup UTF-8 output for Windows compatibility when running directly.""" + if sys.platform.startswith("win") and not hasattr(sys.stdout, "_pytest_wrapped"): + # Only apply UTF-8 wrapping when not running under pytest + try: + # Force UTF-8 encoding on Windows to prevent emoji crashes + import codecs + + if not isinstance(sys.stdout, codecs.StreamWriter): + sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) # type: ignore + if not isinstance(sys.stderr, codecs.StreamWriter): + sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach()) # type: ignore + + # Set console code page to UTF-8 if possible + os.system("chcp 65001 > nul 2>&1") + except: + pass # Ignore if we can't set up UTF-8 output + + +from ci.compiler.platformio_cache import PlatformIOCache +from ci.compiler.platformio_ini import PlatformIOIni + + +# Removed verbose emoji debug system - tests are now succinct + + +# Test data constants +BASIC_INI_WITH_SHORTHANDS = """[platformio] +src_dir = src + +[env:esp32dev] +board = esp32dev +platform = espressif32 +framework = arduino + +[env:uno] +board = uno +platform = atmelavr +framework = arduino + +[env:mixed] +board = esp32-c3-devkitm-1 +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino +""" + +# Mock PlatformIO CLI responses +MOCK_ESP32_PLATFORM_RESPONSE = { + "name": "espressif32", + "title": "Espressif 32", + "version": "55.3.30", + "repository": "https://github.com/pioarduino/platform-espressif32.git", + "frameworks": ["arduino", "espidf"], + "packages": [ + { + "name": "framework-arduinoespressif32", + "type": "framework", + "requirements": "https://github.com/espressif/arduino-esp32/releases/download/3.3.0/esp32-3.3.0.zip", + } + ], +} + +MOCK_ATMELAVR_PLATFORM_RESPONSE = { + "name": "atmelavr", + "title": "Atmel AVR", + "version": "4.2.0", + "repository": "https://github.com/platformio/platform-atmelavr.git", + "frameworks": ["arduino"], + "packages": [], +} + +MOCK_FRAMEWORKS_RESPONSE = [ + { + "name": "arduino", + "title": "Arduino", + "description": "Arduino framework", + "url": "http://arduino.cc", + "homepage": "https://platformio.org/frameworks/arduino", + "platforms": ["atmelavr", "espressif32", "espressif8266"], + }, + { + "name": "espidf", + "title": "ESP-IDF", + "description": "Espressif ESP-IDF framework", + "url": "https://github.com/espressif/esp-idf", + "homepage": "https://platformio.org/frameworks/espidf", + "platforms": ["espressif32"], + }, +] + +# Integration test configuration constants +TEST_CONTENT_SHORTHAND_AND_URL = """[platformio] +src_dir = src + +[env:shorthand] +board = esp32dev +platform = espressif32 +framework = arduino + +[env:direct_url] +board = esp32-c3-devkitm-1 +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino +""" + +TEST_CONTENT_DUPLICATE_PLATFORM = """[platformio] +src_dir = src + +[env:test1] +board = esp32dev +platform = espressif32 +framework = arduino + +[env:test2] +board = esp32-c3-devkitm-1 +platform = espressif32 +framework = arduino +""" + +TEST_CONTENT_MIXED_URL_TYPES = """[platformio] +src_dir = src + +[env:shorthand] +board = esp32dev +platform = espressif32 +framework = arduino + +[env:git_url] +board = uno +platform = https://github.com/platformio/platform-atmelavr.git +framework = arduino + +[env:zip_url] +board = esp32-c3-devkitm-1 +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino +""" + +# Additional mock responses for integration tests +MOCK_ESP32_PLATFORM_WITH_ZIP = { + "name": "espressif32", + "repository": "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip", + "version": "55.3.30", +} + +MOCK_ESP32_PLATFORM_WITH_GIT = { + "name": "espressif32", + "repository": "https://github.com/pioarduino/platform-espressif32.git", + "version": "55.3.30", +} + +MOCK_ARDUINO_FRAMEWORK = { + "name": "arduino", + "url": "http://arduino.cc", + "platforms": ["espressif32"], +} + + +# Mock responses for multi-value resolution tests +MOCK_ESP32_PLATFORM_FULL_RESPONSE = { + "name": "espressif32", + "title": "Espressif 32", + "version": "55.3.30", + "repository": "https://github.com/pioarduino/platform-espressif32.git", + "frameworks": ["arduino", "espidf"], + "packages": [ + { + "name": "framework-arduinoespressif32", + "type": "framework", + "requirements": "https://github.com/espressif/arduino-esp32/releases/download/3.3.0/esp32-3.3.0.zip", + } + ], + "homepage": "https://platformio.org/platforms/espressif32", +} + +MOCK_ESP32_PLATFORM_ZIP_RESPONSE = { + "name": "espressif32", + "title": "Espressif 32", + "version": "55.3.30", + "repository": "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip", + "frameworks": ["arduino", "espidf"], + "homepage": "https://platformio.org/platforms/espressif32", +} + +MOCK_ARDUINO_FRAMEWORK_FULL_RESPONSE = { + "name": "arduino", + "title": "Arduino", + "description": "Arduino framework", + "url": "http://arduino.cc", + "homepage": "https://platformio.org/frameworks/arduino", + "platforms": ["atmelavr", "espressif32", "espressif8266"], +} + +# Centralized Mock Data Organization (Design Implementation) +PLATFORM_MOCKS = { + "espressif32": MOCK_ESP32_PLATFORM_RESPONSE, + "espressif32_full": MOCK_ESP32_PLATFORM_FULL_RESPONSE, + "espressif32_zip": MOCK_ESP32_PLATFORM_ZIP_RESPONSE, + "espressif32_git": MOCK_ESP32_PLATFORM_WITH_GIT, + "atmelavr": MOCK_ATMELAVR_PLATFORM_RESPONSE, +} + +FRAMEWORK_MOCKS = { + "arduino": MOCK_ARDUINO_FRAMEWORK_FULL_RESPONSE, + "arduino_basic": MOCK_ARDUINO_FRAMEWORK, + "frameworks_list": MOCK_FRAMEWORKS_RESPONSE, +} + +# Expected Results for Verification (Design Implementation) +EXPECTED_PLATFORM_URLS = { + "espressif32": "https://github.com/pioarduino/platform-espressif32.git", + "atmelavr": "https://github.com/platformio/platform-atmelavr.git", +} + +EXPECTED_FRAMEWORK_URLS = { + "arduino": "http://arduino.cc", + "espidf": "https://github.com/espressif/esp-idf", +} + + +class TestPlatformIOUrlResolution(unittest.TestCase): + """ + Test the PlatformIO URL resolution functionality with Google-quality clean code. + + This test suite implements the design principles from DESIGN_PLATFORMIO_URL_TEST_IMPROVEMENT.md: + + 🎯 DESIGN GOALS ACHIEVED: + • Immediate Understanding: Any developer can understand tests within 30 seconds + • F5 Debug Friendly: Rich debug output for step-by-step execution tracking + • Maintainable: Minimal code duplication with reusable helper methods + • Reliable: Clear, actionable error messages for quick debugging + + 📋 TEST CATEGORIES: + + BASIC FUNCTIONALITY TESTS - Core URL resolution features: + • test_url_vs_shorthand_detection() - URL vs shorthand name detection + • test_basic_platform_shorthand_resolution() - Platform shorthand → repository URL + • test_url_passthrough_behavior() - URLs pass through unchanged + + CACHING TESTS - Resolution caching and TTL behavior: + • test_resolution_caching_works() - Platform resolution caching + • test_cache_expiration_behavior() - Cache TTL and expiration + + EDGE CASE TESTS - Error handling and boundary conditions: + • test_failed_resolution_handling() - Graceful failure handling + + INTEGRATION TESTS - End-to-end functionality (legacy): + • test_platform_url_resolution() - Complex multi-step resolution + • test_enhanced_platform_resolution() - Enhanced multi-value resolution + • [Additional legacy integration tests...] + + 🔧 HELPER METHODS: + • _create_simple_test_environment() - Standardized test setup + • _verify_url_resolution() - URL verification with debug output + • # debug_print() - Cross-platform emoji-safe printing + + 🎯 MOCK-FREE ARCHITECTURE: + These tests use REAL PlatformIO CLI commands instead of brittle mocks. + Tests now verify actual symbol resolution behavior, making them more reliable + and less prone to breaking when the implementation changes. + + 🚀 USAGE: + Run focused tests: python test_platformio_url_resolution.py + Run single test: Uncomment specific_test in main() + Run all tests: Comment out focused_tests in main() + + 💡 DEBUG OUTPUT LEGEND: + 🔍 = Test start/investigation 🏗️ = Setup phase 🚀 = Execution phase + 📋 = Mock data access 🎭 = Mock configuration 📞 = CLI calls + 📍 = Results/verification ✅ = Success ❌ = Failure + 🎉 = Test completion ⚠️ = Warnings 💡 = Tips/info + + This implementation transforms complex integration tests into maintainable, + immediately understandable, debug-friendly unit tests that follow Google-quality + clean code standards. + """ + + def setUp(self): + """Set up test environment.""" + self.temp_dir = Path(tempfile.mkdtemp()) + self.test_ini = self.temp_dir / "platformio.ini" + self.cache_dir = self.temp_dir / ".cache" + + def tearDown(self): + """Clean up test environment.""" + import shutil + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir) + + # REMOVED: _create_mock_pio_command() - No longer needed, using real PlatformIO CLI + + # REMOVED: _setup_basic_platform_mocks() - No longer needed, using real PlatformIO CLI + + # REMOVED: _setup_enhanced_platform_mocks() - No longer needed, using real PlatformIO CLI + + # REMOVED: _get_platform_mock() and _get_framework_mock() - No longer needed, using real PlatformIO CLI + + def _create_simple_test_environment( + self, ini_content: str | None = None, debug: bool = True + ) -> tuple[PlatformIOIni, Path]: + """ + Create a simple, standardized test environment. + + Args: + ini_content: Custom INI content, defaults to BASIC_INI_WITH_SHORTHANDS + debug: Whether to print debug information + + Returns: + Tuple of (pio_ini_instance, temp_file_path) + """ + content = ini_content or BASIC_INI_WITH_SHORTHANDS + if debug: + # debug_print(f"🏗️ Creating standardized test environment") + # debug_print(f" • INI content: {len(content.split())} lines") + # debug_print(f" • Temp directory: {self.temp_dir}") + pass + + self.test_ini.write_text(content) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + if debug: + sections = pio_ini.get_env_sections() + # debug_print(f" • Environment sections: {len(sections)} ({', '.join(sections)})") + # debug_print(f"✓ Test environment ready") + pass + + return pio_ini, self.test_ini + + def _verify_url_resolution(self, actual_url, expected_url, context="", debug=True): # type: ignore + """ + Verify URL resolution with clear debug output. + + Args: + actual_url: The URL that was actually resolved + expected_url: The URL that was expected + context: Additional context for the verification + debug: Whether to print debug information + """ + if debug: + # debug_print(f"🔍 Verifying URL resolution{f' ({context})' if context else ''}") + # debug_print(f" • Actual: {actual_url}") + # debug_print(f" • Expected: {expected_url}") + # debug_print(f" • Match: {'✅ Yes' if actual_url == expected_url else '❌ No'}") + pass + + self.assertEqual( + actual_url, + expected_url, + f"URL resolution mismatch{f' for {context}' if context else ''}", + ) + + if debug: + # debug_print(f"✓ URL verification successful{f' for {context}' if context else ''}") + pass + + # ============================================================================= + # BASIC FUNCTIONALITY TESTS - Core URL Resolution Features + # ============================================================================= + + def test_url_vs_shorthand_detection(self): + """Test basic URL detection vs shorthand name detection - NO MOCKS.""" + # debug_print(f"\n🔍 Testing URL vs shorthand name detection (REAL IMPLEMENTATION)") + + # SETUP - Create simple test environment (NO MOCKS NEEDED) + pio_ini, _ = self._create_simple_test_environment() + # debug_print(f"🎯 Using REAL PlatformIO implementation - no mocks!") + + # TEST CASES - Shorthand names (should return False) + shorthand_names = ["espressif32", "atmelavr", "arduino"] + # debug_print(f"\n🏷️ Testing shorthand names: {shorthand_names}") + + for shorthand in shorthand_names: + is_url_result = pio_ini._is_url(shorthand) + # debug_print(f" • '{shorthand}' → is_url: {is_url_result}") + self.assertFalse( + is_url_result, f"'{shorthand}' should be detected as shorthand, not URL" + ) + + # TEST CASES - URLs (should return True) + url_examples = [ + "https://github.com/example/platform.git", + "http://example.com/framework", + "file:///local/path", + ] + # debug_print(f"\n🔗 Testing URL examples: {len(url_examples)} cases") + + for url in url_examples: + is_url_result = pio_ini._is_url(url) + # debug_print(f" • '{url}' → is_url: {is_url_result}") + self.assertTrue(is_url_result, f"'{url}' should be detected as URL") + + # TEST CASES - Edge cases + edge_cases = [("", False), ("not-a-url", False)] + # debug_print(f"\n⚠️ Testing edge cases") + + for test_input, expected in edge_cases: + is_url_result = pio_ini._is_url(test_input) + # debug_print(f" • '{test_input}' → is_url: {is_url_result} (expected: {expected})") + self.assertEqual( + is_url_result, expected, f"Edge case '{test_input}' failed" + ) + + # debug_print(f"✅ URL detection works correctly for all test cases - REAL IMPLEMENTATION TESTED") + + def test_basic_platform_shorthand_resolution(self): + """Test converting platform shorthand names to repository URLs using REAL PlatformIO CLI.""" + # debug_print(f"\n🏗️ Testing basic platform shorthand to URL conversion (REAL PlatformIO CLI)") + + # SETUP - Create environment (NO MOCKS!) + pio_ini, _ = self._create_simple_test_environment() + # debug_print(f"🎯 Using REAL PlatformIO CLI - testing actual symbol resolution!") + + # TEST - Real platform resolution with actual PlatformIO CLI calls + test_platforms = [ + ("espressif32", "ESP32 platform"), + ("atmelavr", "ATMegaAVR platform"), + ] + + # debug_print(f"\n🚀 Testing REAL platform resolutions (no mocks)") + for platform_name, description in test_platforms: + # debug_print(f"\n 🔍 Resolving {description}: '{platform_name}'") + + # This will make REAL PlatformIO CLI calls! + resolved_url = pio_ini.resolve_platform_url(platform_name) + + # Verify we got a real URL back + # debug_print(f" 📍 Resolved URL: {resolved_url}") + + # Real assertions - we should get actual repository URLs + self.assertIsNotNone( + resolved_url, f"Platform '{platform_name}' should resolve to a URL" + ) + assert resolved_url is not None # Type guard for pyright + self.assertTrue( + resolved_url.startswith(("https://", "http://", "git@")), + f"Resolved URL should be a valid repository URL, got: {resolved_url}", + ) + self.assertIn( + "github.com", + resolved_url, + f"Expected GitHub repository URL for {platform_name}, got: {resolved_url}", + ) + + # debug_print(f" ✅ Real platform resolution successful: {platform_name} → {resolved_url}") + + # debug_print(f"🎉 All REAL platform shorthand resolutions successful - no mocks used!") + + def test_url_passthrough_behavior(self): + """Test that existing URLs pass through unchanged - NO MOCKS NEEDED.""" + # debug_print(f"\n🔄 Testing URL passthrough behavior (REAL IMPLEMENTATION)") + + # SETUP - No mocks needed for passthrough behavior + pio_ini, _ = self._create_simple_test_environment() + # debug_print(f"🎯 Testing real URL passthrough - no CLI calls needed!") + + # TEST CASES - URLs that should pass through unchanged + test_urls = [ + "https://github.com/example/platform.zip", + "https://github.com/custom/framework.git", + "file:///local/path/to/platform", + "https://releases.platformio.org/packages/framework-arduinoespressif32.zip", + ] + + # debug_print(f"\n🚀 Testing {len(test_urls)} real URL passthrough cases") + for test_url in test_urls: + # debug_print(f"\n 🔗 Testing URL passthrough: '{test_url}'") + + # This should NOT make any CLI calls - URLs should pass through directly + result = pio_ini.resolve_platform_url(test_url) + + # debug_print(f" 📍 Passthrough result: {result}") + # debug_print(f" 🎯 Should match input: {test_url}") + + # Verify exact passthrough + self.assertEqual(result, test_url, f"URL should pass through unchanged") + # debug_print(f" ✅ URL passthrough successful") + + # debug_print(f"🎉 All real URL passthrough tests successful - no mocks needed!") + + # ============================================================================= + # CACHING TESTS - Resolution Caching and TTL Behavior + # ============================================================================= + + def test_resolution_caching_works(self): + """Test that platform resolution results are properly cached using REAL PlatformIO CLI.""" + # debug_print(f"\n🗄️ Testing resolution caching mechanism (REAL CLI CALLS)") + + # SETUP - Environment with call counting (monitoring REAL CLI calls) + pio_ini, _ = self._create_simple_test_environment() + cli_call_count = 0 + + # debug_print(f"🎯 Testing caching with REAL PlatformIO CLI calls!") + + # Create counting wrapper to monitor REAL CLI calls + original_handler = pio_ini._run_pio_command + + def counting_wrapper( + args: list[str], + ) -> dict[str, Any] | list[dict[str, Any]] | None: + nonlocal cli_call_count + cli_call_count += 1 + # debug_print(f"📞 REAL CLI Call #{cli_call_count}: pio {' '.join(args)}") + return original_handler(args) # REAL PlatformIO CLI call! + + pio_ini._run_pio_command = counting_wrapper + + # TEST - First resolution should hit REAL CLI + target_platform = "espressif32" # Use a platform we know exists + + # debug_print(f"\n🚀 First resolution of '{target_platform}' (should hit REAL CLI)") + first_result = pio_ini.resolve_platform_url(target_platform) + first_call_count = cli_call_count + + # Verify first call made a real CLI call + self.assertEqual( + first_call_count, 1, "First resolution should make exactly 1 REAL CLI call" + ) + self.assertIsNotNone(first_result, "First resolution should return a real URL") + self.assertTrue( + first_result.startswith("https://"), + f"Should return real GitHub URL, got: {first_result}", + ) + + # debug_print(f"📍 First result from REAL CLI: {first_result}") + # debug_print(f"✅ First resolution: ✓ Real CLI called, ✓ got real result") + + # TEST - Second resolution should use cache (no additional CLI calls) + # debug_print(f"\n🚀 Second resolution of '{target_platform}' (should use cache, no CLI)") + second_result = pio_ini.resolve_platform_url(target_platform) + second_call_count = cli_call_count + + # Verify second call used cache + self.assertEqual( + second_call_count, + 1, + "Second resolution should NOT make additional CLI calls", + ) + self.assertEqual( + second_result, first_result, "Cached result should match first result" + ) + + # debug_print(f"📍 Second result from cache: {second_result}") + # debug_print(f"✅ Second resolution: ✓ No additional CLI calls, ✓ cache used correctly") + + # TEST - Verify cache contents + # debug_print(f"\n🔍 Verifying cache contents") + cache = pio_ini.get_resolved_urls_cache() + self.assertIn(target_platform, cache.platforms, f"Platform should be in cache") + + cached_url = cache.platforms[target_platform].repository_url + self.assertEqual( + cached_url, first_result, "Cached URL should match resolved URL" + ) + + # debug_print(f"📋 Cached URL: {cached_url}") + # debug_print(f"✅ Cache contents verified") + + # debug_print(f"🎉 Real caching mechanism works perfectly - tested with actual PlatformIO CLI!") + + def test_cache_expiration_behavior(self): + """Test that cache entries expire after TTL and get refreshed.""" + # debug_print(f"\n⏰ Testing cache expiration and refresh behavior") + + # SETUP - This test needs to be converted to mock-free + # TODO: Convert this test to use real PlatformIO CLI + self.skipTest("Test needs conversion to mock-free architecture") + + # Resolve and verify caching works + target_platform = "espressif32" + # debug_print(f"\n🚀 Initial resolution to populate cache") + + initial_result = pio_ini.resolve_platform_url(target_platform) + self.assertIsNotNone(initial_result, "Initial resolution should succeed") + # debug_print(f"✓ Platform resolved and cached") + + # Manually expire the cache entry (simulate TTL expiration) + # debug_print(f"\n⏰ Manually expiring cache entry") + if ( + hasattr(pio_ini, "_platform_cache") + and target_platform in pio_ini._platform_cache + ): + # Set resolved_at to 25 hours ago (past the 24h TTL) + pio_ini._platform_cache[target_platform].resolved_at = ( + datetime.now() - timedelta(hours=25) + ) + # debug_print(f"✓ Cache entry backdated to 25 hours ago") + + # Verify cache is now considered expired + is_cached = pio_ini._is_platform_cached(target_platform) + # debug_print(f"📊 Platform cached status: {is_cached}") + self.assertFalse( + is_cached, "Expired cache entry should not be considered cached" + ) + # debug_print(f"✅ Cache expiration logic works correctly") + else: + # debug_print(f"⚠️ No cache entry found to expire - test skipped") + pass + + # debug_print(f"🎉 Cache expiration behavior verified") + + # ============================================================================= + # EDGE CASE TESTS - Error Handling and Boundary Conditions + # ============================================================================= + + def test_failed_resolution_handling(self): + """Test graceful handling of failed resolution attempts.""" + # debug_print(f"\n🚨 Testing failed resolution error handling") + + # SETUP + pio_ini, _ = self._create_simple_test_environment() + + # Mock CLI to always return None (simulating failures) + def failing_mock(args: list[str]) -> None: + # debug_print(f"📞 Mock CLI (will fail): {' '.join(args)}") + return None + + pio_ini._run_pio_command = failing_mock + # debug_print(f"🎭 Mock configured to simulate CLI failures") + + # TEST - Platform resolution failure + # debug_print(f"\n🚀 Testing platform resolution failure") + failed_platform_result = pio_ini.resolve_platform_url("nonexistent_platform") + # debug_print(f"📍 Result for nonexistent platform: {failed_platform_result}") + self.assertIsNone( + failed_platform_result, "Failed platform resolution should return None" + ) + # debug_print(f"✅ Platform resolution failure handled gracefully") + + # TEST - Framework resolution failure + # debug_print(f"\n🚀 Testing framework resolution failure") + failed_framework_result = pio_ini.resolve_framework_url("nonexistent_framework") + # debug_print(f"📍 Result for nonexistent framework: {failed_framework_result}") + self.assertIsNone( + failed_framework_result, "Failed framework resolution should return None" + ) + # debug_print(f"✅ Framework resolution failure handled gracefully") + + # debug_print(f"🎉 All failure cases handled correctly") + + def test_platform_url_resolution(self): + """Test resolving platform shorthand names to URLs.""" + # debug_print(f"\n🔍 Testing platform shorthand to URL resolution") + + # SETUP - Create test environment with platformio.ini + # debug_print(f"📁 Setting up test platformio.ini file") + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + # debug_print(f"✓ Created PlatformIOIni instance from {self.test_ini}") + + # NOTE: Using REAL PlatformIO CLI instead of mocks + # mock_command_handler = self._create_mock_pio_command({}) # REMOVED + # pio_ini._run_pio_command = mock_command_handler # REMOVED + # debug_print(f"🎯 Using REAL PlatformIO CLI - testing actual symbol resolution!") + + # TEST 1: ESP32 platform resolution + platform_shorthand = "espressif32" + expected_esp32_url = "https://github.com/pioarduino/platform-espressif32.git" + # debug_print(f"\n🚀 Test 1: Resolving platform shorthand '{platform_shorthand}'") + + resolved_esp32_url = pio_ini.resolve_platform_url(platform_shorthand) + # debug_print(f"📍 Resolved URL: {resolved_esp32_url}") + # debug_print(f"🎯 Expected URL: {expected_esp32_url}") + + self.assertEqual(resolved_esp32_url, expected_esp32_url) + # debug_print(f"✅ ESP32 platform resolution successful") + + # TEST 2: ATMega AVR platform resolution + atmelavr_shorthand = "atmelavr" + expected_atmelavr_url = "https://github.com/platformio/platform-atmelavr.git" + # debug_print(f"\n🚀 Test 2: Resolving platform shorthand '{atmelavr_shorthand}'") + + resolved_atmelavr_url = pio_ini.resolve_platform_url(atmelavr_shorthand) + # debug_print(f"📍 Resolved URL: {resolved_atmelavr_url}") + # debug_print(f"🎯 Expected URL: {expected_atmelavr_url}") + + self.assertEqual(resolved_atmelavr_url, expected_atmelavr_url) + # debug_print(f"✅ ATMegaAVR platform resolution successful") + + # TEST 3: URL passthrough (URLs should remain unchanged) + existing_url = "https://github.com/example/platform.zip" + # debug_print(f"\n🚀 Test 3: URL passthrough for existing URL '{existing_url}'") + + passthrough_result = pio_ini.resolve_platform_url(existing_url) + # debug_print(f"📍 Passthrough result: {passthrough_result}") + # debug_print(f"🎯 Should match input: {existing_url}") + + self.assertEqual(passthrough_result, existing_url) + # debug_print(f"✅ URL passthrough works correctly") + + # debug_print(f"🎉 All platform URL resolution tests passed!") + + @unittest.skip("Skipped: depends on removed _create_mock_pio_command method") + def test_framework_url_resolution(self): + """Test resolving framework shorthand names to URLs.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + # Using real PlatformIO CLI instead of mocks + + # Test resolving arduino framework + resolved_url = pio_ini.resolve_framework_url("arduino") + self.assertEqual(resolved_url, "http://arduino.cc") + + # Test resolving espidf framework + resolved_url = pio_ini.resolve_framework_url("espidf") + self.assertEqual(resolved_url, "https://github.com/espressif/esp-idf") + + # Test that URLs are returned as-is + test_url = "https://github.com/example/framework.zip" + resolved_url = pio_ini.resolve_framework_url(test_url) + self.assertEqual(resolved_url, test_url) + + @unittest.skip("Skipped: depends on removed _create_mock_pio_command method") + def test_caching_functionality(self): + """Test that resolutions are cached and TTL works correctly.""" + # debug_print(f"\n🗄️ Testing platform resolution caching behavior") + + # SETUP - Create test environment + # debug_print(f"📁 Setting up test environment") + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + # debug_print(f"✓ PlatformIOIni instance created") + + # Using real PlatformIO CLI instead of mocks + # debug_print(f"🎭 Mock CLI handler with call counting configured") + + # TEST 1: First resolution should hit CLI + target_platform = "espressif32" + expected_url = "https://github.com/pioarduino/platform-espressif32.git" + # debug_print(f"\n🚀 Test 1: First resolution of '{target_platform}' (should hit CLI)") + + first_resolution_result = pio_ini.resolve_platform_url(target_platform) + + # debug_print(f"📍 First result: {first_resolution_result}") + + # Verify first resolution works (can't verify CLI call count with real CLI) + self.assertIsNotNone(first_resolution_result) + # debug_print(f"✅ First resolution successful") + + # TEST 2: Second resolution should use cache + # debug_print(f"\n🚀 Test 2: Second resolution of '{target_platform}' (should use cache)") + + second_resolution_result = pio_ini.resolve_platform_url(target_platform) + + # debug_print(f"📍 Second result: {second_resolution_result}") + + # Verify second resolution matches first (caching behavior) + self.assertEqual( + second_resolution_result, + first_resolution_result, + "Cached result should match first result", + ) + # debug_print(f"✅ Second resolution used cache successfully") + + # TEST 3: Verify cache contents structure + # debug_print(f"\n🚀 Test 3: Verifying cache structure and contents") + + cache_contents = pio_ini.get_resolved_urls_cache() + # debug_print(f"📋 Cache contains platforms: {list(cache_contents.platforms.keys())}") + + self.assertIn( + target_platform, + cache_contents.platforms, + f"Platform '{target_platform}' should be in cache", + ) + + cached_platform_entry = cache_contents.platforms[target_platform] + cached_repository_url = cached_platform_entry.repository_url + + # debug_print(f"📍 Cached repository URL: {cached_repository_url}") + # debug_print(f"📅 Cache entry created: {cached_platform_entry.resolved_at}") + # debug_print(f"⏰ Cache entry expires: {cached_platform_entry.expires_at}") + + self.assertEqual( + cached_repository_url, expected_url, "Cached URL should match expected URL" + ) + # debug_print(f"✅ Cache contents verified successfully") + + # debug_print(f"🎉 Platform resolution caching works correctly!") + + @unittest.skip("Skipped: depends on removed _create_mock_pio_command method") + def test_cache_expiration(self): + """Test that cache entries expire after TTL.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + # Using real PlatformIO CLI instead of mocks + + # Resolve a platform + resolved_url = pio_ini.resolve_platform_url("espressif32") + self.assertIsNotNone(resolved_url) + + # Manually expire the cache entry + if hasattr(pio_ini, "_platform_cache"): + pio_ini._platform_cache["espressif32"].resolved_at = ( + datetime.now() - timedelta(hours=25) + ) + + # Should not be cached anymore + self.assertFalse(pio_ini._is_platform_cached("espressif32")) + + @unittest.skip("Skipped: depends on removed _create_mock_pio_command method") + def test_resolve_all_platform_urls(self): + """Test resolving all platform URLs in the configuration.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + # Using real PlatformIO CLI instead of mocks + + # Resolve all platforms + resolutions = pio_ini.resolve_platform_urls() + + # Should have resolved the shorthand names but not the URL + expected_resolutions = { + "espressif32": "https://github.com/pioarduino/platform-espressif32.git", + "atmelavr": "https://github.com/platformio/platform-atmelavr.git", + } + + for platform, expected_url in expected_resolutions.items(): + self.assertIn(platform, resolutions) + self.assertEqual(resolutions[platform], expected_url) + + # The URL should not be in resolutions since it's already a URL + self.assertNotIn( + "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip", + resolutions, + ) + + @unittest.skip("Skipped: depends on removed _create_mock_pio_command method") + def test_resolve_all_framework_urls(self): + """Test resolving all framework URLs in the configuration.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + # Using real PlatformIO CLI instead of mocks + + # Resolve all frameworks + resolutions = pio_ini.resolve_framework_urls() + + # Should have resolved the arduino framework (appears in multiple environments) + self.assertIn("arduino", resolutions) + self.assertEqual(resolutions["arduino"], "http://arduino.cc") + + # REMOVED: Duplicate test_failed_resolution_handling method that used legacy mocks + + @unittest.skip("Skipped: depends on removed _create_mock_pio_command method") + def test_cache_invalidation(self): + """Test cache invalidation functionality.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + # Using real PlatformIO CLI instead of mocks + + # Populate cache + pio_ini.resolve_platform_url("espressif32") + pio_ini.resolve_framework_url("arduino") + + # Verify cache has entries + cache = pio_ini.get_resolved_urls_cache() + self.assertIn("espressif32", cache.platforms) + self.assertIn("arduino", cache.frameworks) + + # Invalidate cache + pio_ini.invalidate_resolution_cache() + + # Verify cache is empty + cache = pio_ini.get_resolved_urls_cache() + self.assertEqual(len(cache.platforms), 0) + self.assertEqual(len(cache.frameworks), 0) + + @patch("ci.compiler.platformio_ini.subprocess.run") + def test_integration_with_optimize_method(self, mock_subprocess): # type: ignore + """Test integration with the optimize() method for end-to-end functionality.""" + # Create test configuration with shorthand names + test_content = """[platformio] +src_dir = src + +[env:esp32dev] +board = esp32dev +platform = espressif32 +framework = arduino +""" + self.test_ini.write_text(test_content) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock successful PlatformIO CLI responses + def mock_run_side_effect(*args, **kwargs): # type: ignore + cmd = args[0] # type: ignore + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "" + mock_result.stderr = "" + + if "platform" in cmd and "show" in cmd and "espressif32" in cmd: + mock_result.stdout = json.dumps(MOCK_ESP32_PLATFORM_RESPONSE) + elif "platform" in cmd and "frameworks" in cmd: + mock_result.stdout = json.dumps(MOCK_FRAMEWORKS_RESPONSE) + + return mock_result + + mock_subprocess.side_effect = mock_run_side_effect + + # Mock cache manager + mock_cache = MagicMock() + + # Mock the _is_zip_web_url function to return False for git URLs + with patch("ci.compiler.platformio_cache._is_zip_web_url", return_value=False): + # Call optimize - should resolve shorthand names but not process as zip URLs + pio_ini.optimize(mock_cache) + + # Verify the shorthand names were resolved and replaced + esp32_platform = pio_ini.get_option("env:esp32dev", "platform") + self.assertEqual( + esp32_platform, "https://github.com/pioarduino/platform-espressif32.git" + ) + + @unittest.skip("Skipped: depends on removed _create_mock_pio_command method") + def test_get_resolved_urls_cache_structure(self): + """Test the structure and contents of the resolved URLs cache.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + # Using real PlatformIO CLI instead of mocks + + # Populate cache + pio_ini.resolve_platform_url("espressif32") + pio_ini.resolve_framework_url("arduino") + + # Get cache contents + cache = pio_ini.get_resolved_urls_cache() + + # Verify structure - cache is now strongly typed + self.assertIsInstance(cache.platforms, dict) + self.assertIsInstance(cache.frameworks, dict) + + # Verify platform cache contents + esp32_cache = cache.platforms["espressif32"] + self.assertEqual( + esp32_cache.repository_url, + "https://github.com/pioarduino/platform-espressif32.git", + ) + self.assertEqual(esp32_cache.version, "55.3.30") + self.assertEqual(esp32_cache.frameworks, ["arduino", "espidf"]) + self.assertIsNotNone(esp32_cache.resolved_at) + self.assertIsNotNone(esp32_cache.expires_at) + + # Verify framework cache contents + arduino_cache = cache.frameworks["arduino"] + self.assertEqual(arduino_cache.url, "http://arduino.cc") + self.assertEqual( + arduino_cache.homepage, "https://platformio.org/frameworks/arduino" + ) + # Arduino framework is available on many platforms, check that it includes the expected ones + expected_platforms = ["atmelavr", "espressif32", "espressif8266"] + for platform in expected_platforms: + self.assertIn(platform, arduino_cache.platforms) + self.assertIsNotNone(arduino_cache.resolved_at) + self.assertIsNotNone(arduino_cache.expires_at) + + # Integration tests + @patch("ci.compiler.platformio_ini.subprocess.run") + def test_shorthand_resolution_with_existing_cache_system(self, mock_subprocess): # type: ignore + """Test that shorthand resolution works seamlessly with existing cache optimization.""" + + self.test_ini.write_text(TEST_CONTENT_SHORTHAND_AND_URL) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock PlatformIO CLI to return a zip URL for espressif32 + def mock_run_side_effect(*args, **kwargs): # type: ignore + cmd = args[0] # type: ignore + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "" + mock_result.stderr = "" + + if "platform" in cmd and "show" in cmd and "espressif32" in cmd: + mock_result.stdout = json.dumps(MOCK_ESP32_PLATFORM_WITH_ZIP) + + return mock_result + + mock_subprocess.side_effect = mock_run_side_effect + + # Create cache manager + cache = PlatformIOCache(self.cache_dir) + + # Mock cache operations to avoid actual downloads + with ( + patch.object(cache, "download_artifact") as mock_download, + patch("ci.compiler.platformio_cache.handle_zip_artifact") as mock_handle, + ): + mock_download.return_value = str(self.cache_dir / "test.zip") + mock_handle.return_value = "file:///cached/path/extracted" + + # Call optimize - should resolve shorthand name then cache the resolved URL + pio_ini.optimize(cache) + + # Verify the shorthand was resolved first + shorthand_platform = pio_ini.get_option("env:shorthand", "platform") + direct_url_platform = pio_ini.get_option("env:direct_url", "platform") + + # Both should now point to cached paths since the resolved URL was a zip file + self.assertEqual(shorthand_platform, "file:///cached/path/extracted") + self.assertEqual(direct_url_platform, "file:///cached/path/extracted") + + # Verify cache operations were called for the unique URL + mock_handle.assert_called() + + def test_url_resolution_caching_behavior_integration(self): + """Test that URL resolution results are cached correctly in integration.""" + + self.test_ini.write_text(TEST_CONTENT_DUPLICATE_PLATFORM) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock PlatformIO CLI + call_count = 0 + + def mock_run_command(args): # type: ignore + nonlocal call_count + call_count += 1 + + if args == ["platform", "show", "espressif32", "--json-output"]: + return MOCK_ESP32_PLATFORM_WITH_GIT + elif args == ["platform", "frameworks", "--json-output"]: + return [MOCK_ARDUINO_FRAMEWORK] + return None + + pio_ini._run_pio_command = mock_run_command + + # Resolve URLs multiple times + platform_resolutions1 = pio_ini.resolve_platform_urls() + platform_resolutions2 = pio_ini.resolve_platform_urls() + framework_resolutions1 = pio_ini.resolve_framework_urls() + framework_resolutions2 = pio_ini.resolve_framework_urls() + + # Should have made only one call per unique platform due to caching + # 1 call for platform show (framework is built-in so no resolution needed) + self.assertEqual(call_count, 1) + + # Results should be identical + self.assertEqual(platform_resolutions1, platform_resolutions2) + self.assertEqual(framework_resolutions1, framework_resolutions2) + + # Verify cache contents + cache = pio_ini.get_resolved_urls_cache() + self.assertIn("espressif32", cache.platforms) + # Note: arduino is a built-in framework so it won't be in the cache + + def test_mixed_url_types_handling(self): + """Test handling of mixed URL types (shorthand, git URLs, zip URLs).""" + + self.test_ini.write_text(TEST_CONTENT_MIXED_URL_TYPES) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock PlatformIO CLI to return different URL types + def mock_run_command(args): # type: ignore + if args == ["platform", "show", "espressif32", "--json-output"]: + return MOCK_ESP32_PLATFORM_WITH_GIT + elif args == ["platform", "frameworks", "--json-output"]: + return [MOCK_ARDUINO_FRAMEWORK] + return None + + pio_ini._run_pio_command = mock_run_command + + # Create cache manager + cache = PlatformIOCache(self.cache_dir) + + # Mock cache operations + with ( + patch.object(cache, "download_artifact") as mock_download, + patch("ci.compiler.platformio_cache.handle_zip_artifact") as mock_handle, + ): + mock_download.return_value = str(self.cache_dir / "test.zip") + mock_handle.return_value = "file:///cached/path/extracted" + + # Call optimize + pio_ini.optimize(cache) + + # Check final platform values + shorthand_platform = pio_ini.get_option("env:shorthand", "platform") + git_url_platform = pio_ini.get_option("env:git_url", "platform") + zip_url_platform = pio_ini.get_option("env:zip_url", "platform") + + # Shorthand should be resolved to git URL (not cached since it's not a zip) + self.assertEqual( + shorthand_platform, "https://github.com/pioarduino/platform-espressif32.git" + ) + + # Git URL should remain unchanged (not a zip) + self.assertEqual( + git_url_platform, "https://github.com/platformio/platform-atmelavr.git" + ) + + # Zip URL should be cached + self.assertEqual(zip_url_platform, "file:///cached/path/extracted") + + # Multi-value resolution tests + def test_url_type_detection_methods(self): + """Test URL type detection utility methods.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Test git URL detection + self.assertTrue(pio_ini._is_git_url("https://github.com/example/platform.git")) + self.assertTrue(pio_ini._is_git_url("git://github.com/example/platform.git")) + self.assertFalse(pio_ini._is_git_url("https://github.com/example/platform.zip")) + self.assertFalse(pio_ini._is_git_url("espressif32")) + + # Test zip URL detection + self.assertTrue(pio_ini._is_zip_url("https://github.com/example/platform.zip")) + self.assertTrue(pio_ini._is_zip_url("http://example.com/archive.zip")) + self.assertFalse(pio_ini._is_zip_url("https://github.com/example/platform.git")) + self.assertFalse(pio_ini._is_zip_url("espressif32")) + + # Test URL type classification + self.assertEqual( + pio_ini._classify_url_type("https://github.com/example/platform.git"), "git" + ) + self.assertEqual( + pio_ini._classify_url_type("https://github.com/example/platform.zip"), "zip" + ) + self.assertEqual(pio_ini._classify_url_type("/local/path/to/platform"), "file") + self.assertEqual(pio_ini._classify_url_type("espressif32"), "unknown") + + def test_enhanced_platform_resolution(self): + """Test enhanced platform resolution returning multi-value dataclass.""" + # debug_print(f"\n🔬 Testing enhanced multi-value platform resolution") + + # SETUP - Create test environment + # debug_print(f"📁 Setting up test environment") + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + # debug_print(f"✓ PlatformIOIni instance created") + + # MOCK - Configure CLI to return comprehensive platform data + def enhanced_mock_cli_handler( + args: list[str], + ) -> dict[str, Any] | list[dict[str, Any]] | None: + # debug_print(f"📞 Mock CLI called with: {' '.join(args)}") + if args == ["platform", "show", "espressif32", "--json-output"]: + response = MOCK_ESP32_PLATFORM_FULL_RESPONSE + # debug_print(f"📋 Returning full ESP32 platform data (packages: {len(response['packages'])})") + return response # type: ignore + elif args == ["platform", "show", "atmelavr", "--json-output"]: + response = MOCK_ATMELAVR_PLATFORM_RESPONSE + # debug_print(f"📋 Returning ATMegaAVR platform data") + return response # type: ignore + # debug_print(f"⚠️ No mock response for command") + pass + return None + + pio_ini._run_pio_command = enhanced_mock_cli_handler + # debug_print(f"🎭 Enhanced mock CLI handler configured") + + # EXECUTE - Test enhanced resolution for ESP32 platform + target_platform = "espressif32" + # debug_print(f"\n🚀 Testing enhanced resolution for platform: '{target_platform}'") + + platform_resolution = pio_ini.resolve_platform_url_enhanced(target_platform) + # debug_print(f"📦 Resolution result type: {type(platform_resolution).__name__}") + + # VERIFY - Check that resolution was successful + self.assertIsNotNone( + platform_resolution, "Enhanced resolution should return a result" + ) + # debug_print(f"✓ Enhanced resolution returned valid result") + + # VERIFY - Check basic platform information + resolved_name = platform_resolution.name + resolved_version = platform_resolution.version + resolved_frameworks = platform_resolution.frameworks + + # debug_print(f"\n📋 Platform Basic Info:") + # debug_print(f" • Name: {resolved_name}") + # debug_print(f" • Version: {resolved_version}") + # debug_print(f" • Frameworks: {resolved_frameworks}") + + self.assertEqual(resolved_name, target_platform) + self.assertEqual(resolved_version, "55.3.30") + self.assertEqual(resolved_frameworks, ["arduino", "espidf"]) + # debug_print(f"✅ Basic platform information verified") + + # VERIFY - Check URL resolution (git and zip URLs) + git_url = platform_resolution.git_url + zip_url = platform_resolution.zip_url + local_path = platform_resolution.local_path + + expected_git_url = "https://github.com/pioarduino/platform-espressif32.git" + expected_zip_url = "https://github.com/espressif/arduino-esp32/releases/download/3.3.0/esp32-3.3.0.zip" + + # debug_print(f"\n🔗 URL Resolution Results:") + # debug_print(f" • Git URL: {git_url}") + # debug_print(f" • ZIP URL: {zip_url}") + # debug_print(f" • Local Path: {local_path}") + + self.assertEqual(git_url, expected_git_url) + self.assertEqual( + zip_url, expected_zip_url + ) # Enhanced resolution now finds ZIP URLs in packages + self.assertIsNone(local_path) + # debug_print(f"✅ URL resolution verified (both git and zip URLs detected)") + + # VERIFY - Check package information + platform_packages = platform_resolution.packages + packages_count = len(platform_packages) + + # debug_print(f"\n📦 Package Information:") + # debug_print(f" • Package count: {packages_count}") + + self.assertEqual(packages_count, 1) + + first_package = platform_packages[0] + package_name = first_package.name + package_requirements = first_package.requirements + + # debug_print(f" • First package name: {package_name}") + # debug_print(f" • First package requirements: {package_requirements}") + + self.assertEqual(package_name, "framework-arduinoespressif32") + # debug_print(f"✅ Package information verified") + + # VERIFY - Check homepage + homepage_url = platform_resolution.homepage + expected_homepage = "https://platformio.org/platforms/espressif32" + + # debug_print(f"\n🏠 Homepage: {homepage_url}") + self.assertEqual(homepage_url, expected_homepage) + # debug_print(f"✅ Homepage URL verified") + + # VERIFY - Check preferred URL logic (should prefer zip over git) + preferred_url = platform_resolution.preferred_url + # debug_print(f"\n🎯 Preferred URL: {preferred_url}") + # debug_print(f"🎯 Expected (ZIP): {expected_zip_url}") + + self.assertEqual( + preferred_url, + expected_zip_url, + "Preferred URL should be ZIP when available", + ) + # debug_print(f"✅ Preferred URL logic verified (ZIP preferred over Git)") + + # debug_print(f"🎉 Enhanced platform resolution test completed successfully!") + + def test_enhanced_platform_resolution_with_zip_url(self): + """Test enhanced platform resolution with zip URL.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls to return zip URL + def mock_run_command(args): # type: ignore + if args == ["platform", "show", "espressif32", "--json-output"]: + return MOCK_ESP32_PLATFORM_ZIP_RESPONSE + return None + + pio_ini._run_pio_command = mock_run_command + + # Test resolving platform with zip URL + resolution = pio_ini.resolve_platform_url_enhanced("espressif32") + self.assertIsNotNone(resolution) + self.assertEqual(resolution.name, "espressif32") + self.assertIsNone(resolution.git_url) + self.assertEqual( + resolution.zip_url, + "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip", + ) + self.assertIsNone(resolution.local_path) + + # Test preferred URL property prefers zip + self.assertEqual( + resolution.preferred_url, + "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip", + ) + + def test_enhanced_framework_resolution(self): + """Test enhanced framework resolution returning multi-value dataclass.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + def mock_run_command(args): # type: ignore + if args == ["platform", "frameworks", "--json-output"]: + return [MOCK_ARDUINO_FRAMEWORK_FULL_RESPONSE] + return None + + pio_ini._run_pio_command = mock_run_command + + # Test resolving arduino framework + resolution = pio_ini.resolve_framework_url_enhanced("arduino") + self.assertIsNotNone(resolution) + self.assertEqual(resolution.name, "arduino") + self.assertEqual(resolution.url, "http://arduino.cc") + self.assertEqual( + resolution.homepage, "https://platformio.org/frameworks/arduino" + ) + self.assertEqual( + resolution.platforms, ["atmelavr", "espressif32", "espressif8266"] + ) + self.assertEqual(resolution.title, "Arduino") + self.assertEqual(resolution.description, "Arduino framework") + + def test_enhanced_resolution_with_already_resolved_urls(self): + """Test enhanced resolution handles already resolved URLs correctly.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Test with git URL + git_url = "https://github.com/example/platform.git" + resolution = pio_ini.resolve_platform_url_enhanced(git_url) + self.assertIsNotNone(resolution) + self.assertEqual(resolution.name, git_url) + self.assertEqual(resolution.git_url, git_url) + self.assertIsNone(resolution.zip_url) + self.assertIsNone(resolution.local_path) + + # Test with zip URL + zip_url = "https://github.com/example/platform.zip" + resolution = pio_ini.resolve_platform_url_enhanced(zip_url) + self.assertIsNotNone(resolution) + self.assertEqual(resolution.name, zip_url) + self.assertIsNone(resolution.git_url) + self.assertEqual(resolution.zip_url, zip_url) + self.assertIsNone(resolution.local_path) + + # Test with local path + local_path = "/path/to/local/platform" + resolution = pio_ini.resolve_platform_url_enhanced(local_path) + self.assertIsNotNone(resolution) + self.assertEqual(resolution.name, local_path) + self.assertIsNone(resolution.git_url) + self.assertIsNone(resolution.zip_url) + self.assertEqual(resolution.local_path, local_path) + + def test_enhanced_resolution_failure_handling(self): + """Test enhanced resolution failure handling.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock CLI to return None (failure) + pio_ini._run_pio_command = MagicMock(return_value=None) + + # Should return None for failed resolutions + resolution = pio_ini.resolve_platform_url_enhanced("nonexistent_platform") + self.assertIsNone(resolution) + + resolution = pio_ini.resolve_framework_url_enhanced("nonexistent_framework") + self.assertIsNone(resolution) + + def test_enhanced_resolution_caching(self): + """Test that enhanced resolution results are cached correctly.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls with counter + call_count = 0 + + def mock_run_command(args): # type: ignore + nonlocal call_count + call_count += 1 + if args == ["platform", "show", "espressif32", "--json-output"]: + return MOCK_ESP32_PLATFORM_FULL_RESPONSE + return None + + pio_ini._run_pio_command = mock_run_command + + # First call should hit CLI + resolution1 = pio_ini.resolve_platform_url_enhanced("espressif32") + self.assertEqual(call_count, 1) + self.assertIsNotNone(resolution1) + + # Second call should use cache + resolution2 = pio_ini.resolve_platform_url_enhanced("espressif32") + self.assertEqual(call_count, 1) # Should not increment + self.assertIsNotNone(resolution2) + + # Results should be equivalent + self.assertEqual(resolution1.name, resolution2.name) + self.assertEqual(resolution1.git_url, resolution2.git_url) + self.assertEqual(resolution1.version, resolution2.version) + + def test_package_info_dataclass(self): + """Test PackageInfo dataclass functionality.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + def mock_run_command(args): # type: ignore + if args == ["platform", "show", "espressif32", "--json-output"]: + return MOCK_ESP32_PLATFORM_FULL_RESPONSE + return None + + pio_ini._run_pio_command = mock_run_command + + # Resolve platform to get packages + resolution = pio_ini.resolve_platform_url_enhanced("espressif32") + self.assertIsNotNone(resolution) + self.assertEqual(len(resolution.packages), 1) + + package = resolution.packages[0] + self.assertEqual(package.name, "framework-arduinoespressif32") + self.assertEqual(package.type, "framework") + self.assertEqual( + package.requirements, + "https://github.com/espressif/arduino-esp32/releases/download/3.3.0/esp32-3.3.0.zip", + ) + + @patch("ci.compiler.platformio_ini.subprocess.run") + def test_optimize_enhanced_method(self, mock_subprocess): # type: ignore + """Test the optimize_enhanced() method with multi-value resolution.""" + # Create test configuration with shorthand names + test_content = """[platformio] +src_dir = src + +[env:esp32dev] +board = esp32dev +platform = espressif32 +framework = arduino +""" + self.test_ini.write_text(test_content) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock successful PlatformIO CLI responses + def mock_run_side_effect(*args, **kwargs): # type: ignore + cmd = args[0] # type: ignore + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "" + mock_result.stderr = "" + + if "platform" in cmd and "show" in cmd and "espressif32" in cmd: + mock_result.stdout = json.dumps(MOCK_ESP32_PLATFORM_ZIP_RESPONSE) + elif "platform" in cmd and "frameworks" in cmd: + mock_result.stdout = json.dumps([MOCK_ARDUINO_FRAMEWORK_FULL_RESPONSE]) + + return mock_result + + mock_subprocess.side_effect = mock_run_side_effect + + # Mock cache manager + mock_cache = MagicMock() + + # Mock the optimization process + with patch("ci.compiler.platformio_cache._is_zip_web_url", return_value=True): + # Mock cache operations + with ( + patch.object(mock_cache, "download_artifact") as mock_download, + patch( + "ci.compiler.platformio_cache.handle_zip_artifact" + ) as mock_handle, + ): + mock_download.return_value = str(self.cache_dir / "test.zip") + mock_handle.return_value = "file:///cached/path/extracted" + + # Call optimize_enhanced + pio_ini.optimize_enhanced(mock_cache) + + # Verify the platform was resolved and optimized + esp32_platform = pio_ini.get_option("env:esp32dev", "platform") + self.assertEqual(esp32_platform, "file:///cached/path/extracted") + + def test_backward_compatibility_with_enhanced_methods(self): + """Test that enhanced methods maintain backward compatibility.""" + self.test_ini.write_text(BASIC_INI_WITH_SHORTHANDS) + pio_ini = PlatformIOIni.parseFile(self.test_ini) + + # Mock the PlatformIO CLI calls + def mock_run_command(args): # type: ignore + if args == ["platform", "show", "espressif32", "--json-output"]: + return MOCK_ESP32_PLATFORM_FULL_RESPONSE + elif args == ["platform", "frameworks", "--json-output"]: + return [MOCK_ARDUINO_FRAMEWORK_FULL_RESPONSE] + return None + + pio_ini._run_pio_command = mock_run_command + + # Test that old single-value methods still work + platform_url = pio_ini.resolve_platform_url("espressif32") + self.assertEqual( + platform_url, "https://github.com/pioarduino/platform-espressif32.git" + ) + + framework_url = pio_ini.resolve_framework_url("arduino") + self.assertEqual(framework_url, "http://arduino.cc") + + # Test that enhanced methods return the new dataclasses + platform_resolution = pio_ini.resolve_platform_url_enhanced("espressif32") + self.assertIsNotNone(platform_resolution) + self.assertEqual(platform_resolution.git_url, platform_url) + + framework_resolution = pio_ini.resolve_framework_url_enhanced("arduino") + self.assertIsNotNone(framework_resolution) + self.assertEqual(framework_resolution.url, framework_url) + + +def main(): + """ + Run tests directly with F5 debugging support. + + This function provides a debug-friendly way to run specific tests + or all tests with detailed output for troubleshooting. + """ + # Setup UTF-8 output for Windows emoji compatibility + _setup_utf8_output() + + # debug_print("🧪 PlatformIO URL Resolution Tests - Design Implementation") + print("=" * 65) + print("Debug Mode: Detailed output enabled for F5 debugging") + print() + + # ========================================================================= + # TEST EXECUTION OPTIONS - Uncomment one of these modes for focused debugging: + # ========================================================================= + + # Option 1: Run just the new MOCK-FREE tests (recommended for debugging) + focused_tests = [ + "test_url_vs_shorthand_detection", # NO MOCKS - real URL detection + "test_basic_platform_shorthand_resolution", # REAL PlatformIO CLI calls + "test_url_passthrough_behavior", # NO MOCKS - direct passthrough + "test_resolution_caching_works", # REAL CLI + caching verification + # 'test_cache_expiration_behavior', # TODO: Convert to mock-free + # 'test_failed_resolution_handling' # TODO: Convert to mock-free + ] + + # Option 2: Run a single specific test for deep debugging + # specific_test = 'test_basic_platform_shorthand_resolution' + # suite = unittest.TestLoader().loadTestsFromName(specific_test, TestPlatformIOUrlResolution) + + # Option 3: Run all tests (default) + # suite = unittest.TestLoader().loadTestsFromTestCase(TestPlatformIOUrlResolution) + + # ACTIVE CONFIGURATION: Run focused simplified tests + # debug_print("🎯 Running FOCUSED SIMPLIFIED TESTS (Design Implementation)") + # debug_print(f" Selected tests: {len(focused_tests)}") + for test_name in focused_tests: + # debug_print(f" • {test_name}") + pass + print() + + suite = unittest.TestSuite() + for test_name in focused_tests: + try: + suite.addTest(TestPlatformIOUrlResolution(test_name)) + except Exception as e: + # debug_print(f"⚠️ Could not load test '{test_name}': {e}") + pass + + runner = unittest.TextTestRunner(verbosity=2, stream=sys.stdout, buffer=False) + + # debug_print("🚀 Starting focused test execution...") + print() + result = runner.run(suite) + + print("\n" + "=" * 65) + if result.wasSuccessful(): + # debug_print(f"✅ SUCCESS: All {result.testsRun} focused tests passed!") + # debug_print("🎉 Design implementation working perfectly!") + pass + else: + failures = len(result.failures) + errors = len(result.errors) + # debug_print(f"❌ FAILURE: {failures} failures, {errors} errors out of {result.testsRun} tests") + pass + + if result.failures: + # debug_print("\n📋 Test Failures:") + for test, traceback in result.failures: + # debug_print(f" • {test}") + # debug_print(f" Error: {traceback.split()[-1] if traceback else 'Unknown failure'}") + pass + + if result.errors: + # debug_print("\n🚨 Test Errors:") + for test, traceback in result.errors: + # debug_print(f" • {test}") + # debug_print(f" Error: {traceback.split()[-1] if traceback else 'Unknown error'}") + pass + + # debug_print(f"\n💡 DEBUGGING TIPS:") + # debug_print(f" • To run all tests: Comment out 'focused_tests' section") + # debug_print(f" • To run one test: Uncomment 'specific_test' line") + # debug_print(f" • Each test shows step-by-step execution with emojis") + # debug_print(f" • Look for ❌ symbols to spot failures quickly") + + return result.wasSuccessful() + + +if __name__ == "__main__": + import sys + + main() diff --git a/v1.5/lib/FastLED/ci/tests/test_real_platformio_cache.py b/v1.5/lib/FastLED/ci/tests/test_real_platformio_cache.py new file mode 100644 index 0000000..4fe7491 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_real_platformio_cache.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Real test for PlatformIO cache functionality - no mocks. +Downloads actual ESP32 platform, caches it, and verifies transformation. +""" + +import shutil +import unittest +from pathlib import Path + +import pytest + +from ci.compiler.platformio_cache import PlatformIOCache +from ci.compiler.platformio_ini import PlatformIOIni + + +# Test constants +ESP32_PLATFORM_URL = "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip" + +PLATFORMIO_INI_TEMPLATE = """[platformio] +src_dir = src + +[env:esp32dev] +board = esp32dev +platform = {platform_url} +framework = arduino +build_flags = -DCORE_DEBUG_LEVEL=4 + +[env:esp32c3] +board = esp32-c3-devkitm-1 +platform = {platform_url} +framework = arduino +""" + + +class TestRealPlatformIOCache(unittest.TestCase): + """Real test for PlatformIO cache functionality without mocks.""" + + def setUp(self): + """Set up test environment.""" + self.cache_dir = Path(".cache/tests/test_real_platformio_cache") + self.cache_dir.mkdir(parents=True, exist_ok=True) + self.platformio_ini = self.cache_dir / "platformio.ini" + + def tearDown(self): + """Clean up test environment.""" + # Only clean up the platformio.ini file, keep cache for reuse + if self.platformio_ini.exists(): + self.platformio_ini.unlink() + + def test_real_esp32_platform_download_and_transform(self): + """Test with real ESP32 platform download - no mocks.""" + # Create simple platformio.ini with real ESP32 platform URL + platformio_ini_content = PLATFORMIO_INI_TEMPLATE.format( + platform_url=ESP32_PLATFORM_URL + ) + + # Write the platformio.ini file + self.platformio_ini.write_text(platformio_ini_content) + + # Verify original content has the URL + original_content = self.platformio_ini.read_text() + self.assertIn(ESP32_PLATFORM_URL, original_content) + url_count_before = original_content.count(ESP32_PLATFORM_URL) + self.assertEqual(url_count_before, 2) # Should appear in both environments + + print(f"🌐 Downloading real ESP32 platform from: {ESP32_PLATFORM_URL}") + + # Process the platformio.ini file through the cache system + # This will make real HTTP requests and download the actual platform + try: + pio_ini = PlatformIOIni.parseFile(self.platformio_ini) + cache = PlatformIOCache(self.cache_dir) + pio_ini.optimize(cache) + pio_ini.dump(self.platformio_ini) + except ValueError as e: + if "checksum verification" in str(e): + pytest.skip(f"Skipping test due to upstream file checksum change: {e}") + else: + raise + + print("✅ Download and processing completed!") + + # Read the transformed platformio.ini + final_content = self.platformio_ini.read_text() + print(final_content) + + # Verify original URL was replaced + self.assertNotIn(ESP32_PLATFORM_URL, final_content) + print(f"✅ Original URL removed from platformio.ini") + + # Verify file URLs or paths are present (should be 2 occurrences) + import platform + + if platform.system() == "Windows": + # On Windows, paths are returned directly + path_count = final_content.count("extracted") + self.assertEqual(path_count, 2) + print(f"✅ Found {path_count} local paths as expected") + else: + # On Unix systems, file:// URLs are used + file_url_count = final_content.count("file:///") + self.assertEqual(file_url_count, 2) + print(f"✅ Found {file_url_count} file:// URLs as expected") + + # Verify the file URLs point to the cache + self.assertIn("extracted", final_content) + print("✅ URLs point to extracted cache directory") + + # Verify other content is preserved + self.assertIn("board = esp32dev", final_content) + self.assertIn("board = esp32-c3-devkitm-1", final_content) + # Framework might be resolved from "arduino" to its URL, which is expected behavior + self.assertTrue( + "framework = arduino" in final_content + or "framework = http://arduino.cc" in final_content, + f"Expected framework to be 'arduino' or 'http://arduino.cc', but got: {final_content}", + ) + self.assertIn("build_flags = -DCORE_DEBUG_LEVEL=4", final_content) + self.assertIn("[env:esp32dev]", final_content) + self.assertIn("[env:esp32c3]", final_content) + print("✅ All other platformio.ini content preserved") + + # Verify cache directories and files were created + self.assertTrue(self.cache_dir.exists()) + + # Verify actual files were cached in the new structure + # Each artifact has its own directory under cache_dir/ + artifact_dirs = [d for d in self.cache_dir.iterdir() if d.is_dir()] + self.assertGreater(len(artifact_dirs), 0) + + # Check the artifact directory contains both zip and extracted folder + for artifact_dir in artifact_dirs: + # Look for the specific artifact.zip file + artifact_zip = artifact_dir / "artifact.zip" + self.assertTrue(artifact_zip.exists()) + print(f"✅ Platform zip cached: artifact.zip") + + # Check extracted directory exists + extracted_dir = artifact_dir / "extracted" + self.assertTrue(extracted_dir.exists()) + print(f"✅ Platform extracted alongside zip in: {artifact_dir.name}") + + # Verify platform.json exists in extracted content + platform_json_files = list(extracted_dir.rglob("platform.json")) + self.assertGreater(len(platform_json_files), 0) + print(f"✅ Platform extracted with valid platform.json") + + # Checksums were removed from the system + print("✅ No checksum verification (removed by design)") + + # Verify lock files were created alongside the zip + lock_files = list(artifact_dir.glob("*.lock")) + self.assertGreaterEqual( + len(lock_files), 0 + ) # Lock files may not exist after completion + print( + f"✅ Lock mechanism used (lock files may be cleaned up after completion)" + ) + + print("\n📋 Final transformed platformio.ini:") + for line in final_content.split("\n"): + if "platform =" in line: + print(f" {line}") + + print(f"\n📊 Cache statistics:") + print(f" Cache directory: {self.cache_dir}") + # Get the first artifact directory + first_artifact_dir = artifact_dirs[0] + zip_file = list(first_artifact_dir.glob("*.zip"))[0] + print(f" Cached zip size: {zip_file.stat().st_size // 1024} KB") + print(f" Extracted platform: {first_artifact_dir.name}") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_running_process.py b/v1.5/lib/FastLED/ci/tests/test_running_process.py new file mode 100644 index 0000000..9513ba0 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_running_process.py @@ -0,0 +1,206 @@ +"""Basic unittest for ci.util.running_process.RunningProcess. + +This test executes a trivial Python command via `uv run python -c` and verifies: +- The process exits successfully (return code 0) +- The streamed output contains the expected line +""" + +from __future__ import annotations + +import time +import unittest +from pathlib import Path +from typing import List, Union + +from ci.util.running_process import EndOfStream, RunningProcess + + +class TestRunningProcess(unittest.TestCase): + def test_sanity(self: "TestRunningProcess") -> None: + """Run a trivial command and validate output streaming and exit code. + + Uses `uv run python -c "print('hello')"` to ensure we respect the + repository rule that all Python execution goes through `uv run python`. + """ + + command: list[str] = [ + "uv", + "run", + "python", + "-c", + "print('hello')", + ] + + rp: RunningProcess = RunningProcess( + command=command, + cwd=Path(".").absolute(), + check=False, + auto_run=True, + timeout=30, + enable_stack_trace=True, + on_complete=None, + output_formatter=None, + ) + + captured_lines: List[str] = [] + + while True: + out: Union[str, EndOfStream, None] = rp.get_next_line_non_blocking() + if isinstance(out, EndOfStream): + break + if isinstance(out, str): + captured_lines.append(out) + else: + time.sleep(0.01) + + rc: int = rp.wait() + self.assertEqual(rc, 0) + + combined: str = "\n".join(captured_lines).strip() + self.assertIn("hello", combined) + + def test_line_iter_basic(self: "TestRunningProcess") -> None: + """Validate context-managed line iteration yields only strings and completes.""" + + command: list[str] = [ + "uv", + "run", + "python", + "-c", + "print('a'); print('b'); print('c')", + ] + + rp: RunningProcess = RunningProcess( + command=command, + cwd=Path(".").absolute(), + check=False, + auto_run=True, + timeout=10, + enable_stack_trace=False, + on_complete=None, + output_formatter=None, + ) + + iter_lines: List[str] = [] + with rp.line_iter(timeout=5) as it: + for ln in it: + # Should always be a string, never None + self.assertIsInstance(ln, str) + iter_lines.append(ln) + + # Process should have finished; ensure exit success + rc: int = rp.wait() + self.assertEqual(rc, 0) + self.assertEqual(iter_lines, ["a", "b", "c"]) + + +class _UpperFormatter: + """Simple OutputFormatter that records begin/end calls and uppercases lines.""" + + def __init__(self) -> None: + self.begin_called: bool = False + self.end_called: bool = False + + def begin(self) -> None: + self.begin_called = True + + def transform(self, line: str) -> str: + return line.upper() + + def end(self) -> None: + self.end_called = True + + +class TestRunningProcessAdditional(unittest.TestCase): + def test_timeout_and_kill(self: "TestRunningProcessAdditional") -> None: + """Process exceeding timeout should be killed and raise TimeoutError.""" + + command: list[str] = [ + "uv", + "run", + "python", + "-c", + "import time; time.sleep(999)", + ] + + rp: RunningProcess = RunningProcess( + command=command, + cwd=Path(".").absolute(), + check=False, + auto_run=True, + timeout=1, + enable_stack_trace=True, + on_complete=None, + output_formatter=None, + ) + + # Do not block on output; wait should time out quickly + with self.assertRaises(TimeoutError): + _ = rp.wait() + + # After timeout, process should be finished + self.assertTrue(rp.finished) + + # EndOfStream should be delivered shortly after + end_seen: bool = False + deadline: float = time.time() + 2.0 + while time.time() < deadline: + nxt: Union[str, EndOfStream, None] = rp.get_next_line_non_blocking() + if isinstance(nxt, EndOfStream): + end_seen = True + break + time.sleep(0.01) + self.assertTrue(end_seen) + + def test_output_formatter(self: "TestRunningProcessAdditional") -> None: + """Output formatter hooks are invoked and transform is applied; blanks ignored.""" + + formatter = _UpperFormatter() + + command: list[str] = [ + "uv", + "run", + "python", + "-c", + "print(); print('hello'); print('world')", + ] + + rp: RunningProcess = RunningProcess( + command=command, + cwd=Path(".").absolute(), + check=False, + auto_run=True, + timeout=10, + enable_stack_trace=False, + on_complete=None, + output_formatter=formatter, + ) + + # Drain output (optional; accumulated_output records lines regardless) + while True: + line: Union[str, EndOfStream, None] = rp.get_next_line_non_blocking() + if isinstance(line, EndOfStream): + break + if line is None: + time.sleep(0.005) + continue + + rc: int = rp.wait() + self.assertEqual(rc, 0) + + # Verify formatter begin/end were called + self.assertTrue(formatter.begin_called) + self.assertTrue(formatter.end_called) + + # Verify transformed, non-empty lines only + output_text: str = rp.stdout.strip() + # Should contain HELLO and WORLD, but not an empty line + self.assertIn("HELLO", output_text) + self.assertIn("WORLD", output_text) + # Ensure no blank-only lines exist in accumulated output + for ln in output_text.split("\n"): + self.assertTrue(len(ln.strip()) > 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_symbol_analysis.py b/v1.5/lib/FastLED/ci/tests/test_symbol_analysis.py new file mode 100644 index 0000000..4fa423a --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_symbol_analysis.py @@ -0,0 +1,426 @@ +import json +import subprocess +import threading +import time +import unittest +from pathlib import Path + +import pytest + + +# OPTIMIZED: Disabled by default to avoid expensive imports during test discovery +_ENABLED = True + +from ci.util.paths import PROJECT_ROOT +from ci.util.symbol_analysis import ( + SymbolInfo, + analyze_map_file, + analyze_symbols, + build_reverse_call_graph, + find_board_build_info, + generate_report, +) +from ci.util.tools import Tools, load_tools + + +HERE = Path(__file__).resolve().parent.absolute() +UNO = HERE / "uno" +OUTPUT = HERE / "output" +ELF_FILE = UNO / "firmware.elf" +BUILD_INFO_PATH = ( + PROJECT_ROOT / ".build" / "fled" / "examples" / "uno" / "build_info.json" +) + + +PLATFORMIO_PATH = Path.home() / ".platformio" +PLATFORMIO_PACKAGES_PATH = PLATFORMIO_PATH / "packages" +TOOLCHAIN_AVR = PLATFORMIO_PACKAGES_PATH / "toolchain-atmelavr" + +# Global lock to prevent multiple threads from running compilation simultaneously +_compilation_lock = threading.Lock() +_compilation_done = False + + +def init() -> None: + global _compilation_done + + # Use lock to ensure only one thread runs compilation + with _compilation_lock: + if _compilation_done: + print("Compilation already completed by another thread, skipping.") + return + + uno_build = PROJECT_ROOT / ".build" / "uno" + print( + f"Thread {threading.current_thread().ident}: Checking for Uno build in: {uno_build}" + ) + print(f"BUILD_INFO_PATH: {BUILD_INFO_PATH}") + print(f"TOOLCHAIN_AVR: {TOOLCHAIN_AVR}") + print(f"BUILD_INFO_PATH exists: {BUILD_INFO_PATH.exists()}") + print(f"TOOLCHAIN_AVR exists: {TOOLCHAIN_AVR.exists()}") + + if not BUILD_INFO_PATH.exists() or not TOOLCHAIN_AVR.exists(): + print("Uno build not found. Running compilation...") + print(f"Working directory: {PROJECT_ROOT}") + try: + print( + "Starting compilation command: uv run python-m ci.ci-compile uno --examples Blink" + ) + start_time = time.time() + result = subprocess.run( + "uv run python -m ci.ci-compile uno --examples Blink", + shell=True, + check=True, + cwd=str(PROJECT_ROOT), + capture_output=True, + text=True, + ) + end_time = time.time() + print( + f"Compilation completed successfully in {end_time - start_time:.2f} seconds." + ) + print(f"STDOUT: {result.stdout}") + if result.stderr: + print(f"STDERR: {result.stderr}") + _compilation_done = True + except subprocess.CalledProcessError as e: + print(f"Error during compilation (returncode: {e.returncode}): {e}") + if e.stdout: + print(f"STDOUT: {e.stdout}") + if e.stderr: + print(f"STDERR: {e.stderr}") + raise + else: + print("Uno build found, skipping compilation.") + _compilation_done = True + + +@pytest.mark.full +class TestSymbolAnalysis(unittest.TestCase): + @classmethod + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def setUpClass(cls): + """Set up test fixtures before running tests.""" + if not _ENABLED: + return + init() + # Import Tools dynamically to avoid import errors when disabled + from ci.util.tools import Tools, load_tools + + cls.tools: Tools = load_tools(BUILD_INFO_PATH) + + # Load build info for testing + with open(BUILD_INFO_PATH) as f: + cls.build_info = json.load(f) + + # Get the board key (should be 'uno' for UNO board) + cls.board_key = "uno" + if cls.board_key not in cls.build_info: + # Fallback to first available key + cls.board_key = list(cls.build_info.keys())[0] + + cls.board_info = cls.build_info[cls.board_key] + + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def test_analyze_symbols_basic(self) -> None: + """Test basic symbol analysis functionality.""" + print("Testing basic symbol analysis...") + + # Test with the test ELF file + symbols = analyze_symbols( + str(ELF_FILE), str(self.tools.nm_path), str(self.tools.cpp_filt_path) + ) + + # Verify we got some symbols + self.assertGreater(len(symbols), 0, "Should find some symbols in ELF file") + + # Verify symbol structure + symbols_with_size = 0 + for symbol in symbols: + self.assertIsInstance(symbol.address, str) + self.assertIsInstance(symbol.size, int) + self.assertIsInstance(symbol.type, str) + self.assertIsInstance(symbol.name, str) + self.assertIsInstance(symbol.demangled_name, str) + self.assertGreaterEqual( + symbol.size, 0, "Symbol size should be non-negative" + ) + if symbol.size > 0: + symbols_with_size += 1 + + # Ensure we have at least some symbols with actual size (not all zero-size) + self.assertGreater( + symbols_with_size, 0, "Should have at least some symbols with positive size" + ) + + print(f"Found {len(symbols)} symbols") + + # Check that we have some common symbols we'd expect in a compiled program + symbol_names = [s.demangled_name for s in symbols] + + # Should have main function + main_symbols = [name for name in symbol_names if "main" in name.lower()] + self.assertGreater(len(main_symbols), 0, "Should find main function") + + print(f"Sample symbols: {symbol_names[:5]}") + + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def test_analyze_symbols_from_build_info(self) -> None: + """Test symbol analysis using actual build info paths.""" + print("Testing symbol analysis with build_info.json paths...") + + # Get paths from build info + nm_path = self.board_info["aliases"]["nm"] + cppfilt_path = self.board_info["aliases"]["c++filt"] + elf_file = self.board_info["prog_path"] + + # If the ELF file from build_info doesn't exist, try the actual build location + if not Path(elf_file).exists(): + # Fallback to the actual ELF file location + actual_elf_file = ( + PROJECT_ROOT + / ".build" + / "fled" + / "examples" + / "uno" + / "Blink" + / ".pio" + / "build" + / "uno" + / "firmware.elf" + ) + if actual_elf_file.exists(): + elf_file = str(actual_elf_file) + print(f"Using actual ELF file location: {elf_file}") + + # Verify the ELF file exists + self.assertTrue(Path(elf_file).exists(), f"ELF file should exist: {elf_file}") + + # Run analysis + symbols = analyze_symbols(elf_file, nm_path, cppfilt_path) + + # Verify results + self.assertGreater(len(symbols), 0, "Should find symbols") + + # Check that we have symbols with reasonable sizes + sizes = [s.size for s in symbols] + symbols_with_size = sum(1 for size in sizes if size > 0) + self.assertGreater( + symbols_with_size, 0, "Should have at least some symbols with positive size" + ) + # All sizes should be non-negative (zero-size symbols are valid for undefined symbols, labels, etc.) + self.assertTrue( + all(size >= 0 for size in sizes), + "All symbols should have non-negative sizes", + ) + + print(f"Analyzed {len(symbols)} symbols from build ELF: {elf_file}") + + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def test_generate_report_basic(self) -> None: + """Test report generation functionality.""" + print("Testing basic report generation...") + + # Create some test symbols using SymbolInfo dataclass + test_symbols = [ + SymbolInfo( + address="0x1000", + size=1000, + type="T", + name="test_function_1", + demangled_name="test_function_1()", + source="test", + ), + SymbolInfo( + address="0x2000", + size=500, + type="T", + name="_Z12test_func_2v", + demangled_name="test_function_2()", + source="test", + ), + SymbolInfo( + address="0x3000", + size=200, + type="D", + name="test_data", + demangled_name="test_data", + source="test", + ), + ] + + test_dependencies = {"test_module.o": ["test_function_1", "test_data"]} + + # Generate report + report = generate_report("TEST_BOARD", test_symbols, test_dependencies) + + # Verify report structure - use dataclass field access + self.assertIsInstance(report.board, str) + self.assertIsInstance(report.total_symbols, int) + self.assertIsInstance(report.total_size, int) + self.assertIsInstance(report.largest_symbols, list) + self.assertIsInstance(report.type_breakdown, list) + self.assertIsInstance(report.dependencies, dict) + + # Verify values + self.assertEqual(report.board, "TEST_BOARD") + self.assertEqual(report.total_symbols, 3) + self.assertEqual(report.total_size, 1700) # 1000 + 500 + 200 + + # Verify type breakdown - it's now a list of TypeBreakdown dataclasses + type_breakdown_dict = {tb.type: tb for tb in report.type_breakdown} + self.assertIn("T", type_breakdown_dict) + self.assertIn("D", type_breakdown_dict) + self.assertEqual(type_breakdown_dict["T"].count, 2) + self.assertEqual(type_breakdown_dict["D"].count, 1) + + print("Report generation test passed") + + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def test_find_board_build_info(self) -> None: + """Test the board build info detection functionality.""" + print("Testing board build info detection...") + + # Test finding UNO board specifically + try: + build_info_path, board_name = find_board_build_info("uno") + self.assertEqual(board_name, "uno") + self.assertTrue(build_info_path.exists()) + self.assertEqual(build_info_path.name, "build_info.json") + print(f"Found UNO build info: {build_info_path}") + except SystemExit: + self.skipTest("UNO build not available for testing") + + # Test auto-detection (should find any available board) + try: + build_info_path, board_name = find_board_build_info(None) + self.assertTrue(build_info_path.exists()) + self.assertEqual(build_info_path.name, "build_info.json") + print(f"Auto-detected board: {board_name} at {build_info_path}") + except SystemExit: + self.skipTest("No builds available for auto-detection testing") + + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def test_analyze_map_file(self) -> None: + """Test map file analysis if available.""" + print("Testing map file analysis...") + + # Try to find a map file + elf_file_path = Path(self.board_info["prog_path"]) + map_file = elf_file_path.with_suffix(".map") + + if not map_file.exists(): + print(f"Map file not found at {map_file}, skipping map analysis test") + return + + # Analyze the map file + dependencies = analyze_map_file(map_file) + + # Verify result structure + self.assertIsInstance(dependencies, dict) + + if dependencies: + print(f"Found {len(dependencies)} modules in map file") + # Verify structure of dependencies + for module, symbols in dependencies.items(): + self.assertIsInstance(module, str) + self.assertIsInstance(symbols, list) + + # Print a sample for debugging + sample_modules = list(dependencies.keys())[:3] + for module in sample_modules: + print(f" {module}: {len(dependencies[module])} symbols") + else: + print("No dependencies found in map file (this may be normal)") + + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def test_build_reverse_call_graph(self) -> None: + """Test reverse call graph building.""" + print("Testing reverse call graph building...") + + # Create test call graph + test_call_graph = { + "function_a": ["function_b", "function_c"], + "function_b": ["function_c", "function_d"], + "function_c": ["function_d"], + } + + # Build reverse call graph + reverse_graph = build_reverse_call_graph(test_call_graph) + + # Verify structure + expected_reverse = { + "function_b": ["function_a"], + "function_c": ["function_a", "function_b"], + "function_d": ["function_b", "function_c"], + } + + self.assertEqual(reverse_graph, expected_reverse) + print("Reverse call graph building test passed") + + @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") + def test_full_symbol_analysis_workflow(self) -> None: + """Test the complete symbol analysis workflow.""" + print("Testing complete symbol analysis workflow...") + + # Run full analysis on actual build + elf_file = self.board_info["prog_path"] + + # If the ELF file from build_info doesn't exist, try the actual build location + if not Path(elf_file).exists(): + # Fallback to the actual ELF file location + actual_elf_file = ( + PROJECT_ROOT + / ".build" + / "fled" + / "examples" + / "uno" + / "Blink" + / ".pio" + / "build" + / "uno" + / "firmware.elf" + ) + if actual_elf_file.exists(): + elf_file = str(actual_elf_file) + print(f"Using actual ELF file location: {elf_file}") + + nm_path = self.board_info["aliases"]["nm"] + cppfilt_path = self.board_info["aliases"]["c++filt"] + + # Analyze symbols + symbols = analyze_symbols(elf_file, nm_path, cppfilt_path) + + # Analyze map file if available + map_file = Path(elf_file).with_suffix(".map") + dependencies = analyze_map_file(map_file) + + # Generate report + report = generate_report("UNO", symbols, dependencies) + + # Verify the complete workflow produced valid results - use dataclass field access + self.assertGreater(report.total_symbols, 0) + self.assertGreater(report.total_size, 0) + self.assertGreater(len(report.largest_symbols), 0) + self.assertGreater(len(report.type_breakdown), 0) + + # Print summary for verification + print("Complete analysis results:") + print(f" Board: {report.board}") + print(f" Total symbols: {report.total_symbols}") + print( + f" Total size: {report.total_size} bytes ({report.total_size / 1024:.1f} KB)" + ) + print( + f" Largest symbol: {report.largest_symbols[0].demangled_name} ({report.largest_symbols[0].size} bytes)" + ) + + # Verify we have expected symbol types for a typical embedded program + type_breakdown_dict = {tb.type: tb for tb in report.type_breakdown} + self.assertIn("T", type_breakdown_dict, "Should have text/code symbols") + + print("Full workflow test completed successfully") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_unity_generation.py b/v1.5/lib/FastLED/ci/tests/test_unity_generation.py new file mode 100644 index 0000000..88c556d --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_unity_generation.py @@ -0,0 +1,171 @@ +""" +Unit tests for unity file generation behavior in the Python compiler. + +Validates that: +- Unity content matches expected includes order +- Unity file writes are idempotent (no rewrite when content unchanged) +- Unity file is rewritten when source list changes +""" + +import os +import tempfile +import time +import unittest +from pathlib import Path +from typing import List +from unittest import TestCase + +from ci.compiler.clang_compiler import BuildFlags, Compiler, CompilerOptions + + +class TestUnityGeneration(TestCase): + def setUp(self) -> None: + self.temp_dir = Path(tempfile.mkdtemp(prefix="unity_test_")).resolve() + self.unity_dir = self.temp_dir / "unity" + self.unity_dir.mkdir(parents=True, exist_ok=True) + self.src_dir = self.temp_dir / "src" + self.src_dir.mkdir(parents=True, exist_ok=True) + + # Create minimal compilable .cpp files + self.cpp1 = self.src_dir / "a.cpp" + self.cpp2 = self.src_dir / "b.cpp" + + with open(self.cpp1, "w", encoding="utf-8") as f1: + f1.write( + """ +static int func_a() { return 1; } +int var_a = func_a(); +""".lstrip() + ) + + with open(self.cpp2, "w", encoding="utf-8") as f2: + f2.write( + """ +static int func_b() { return 2; } +int var_b = func_b(); +""".lstrip() + ) + + # Load build flags and create a compiler instance + project_root = Path.cwd() + toml_path = project_root / "ci" / "build_unit.toml" + build_flags: BuildFlags = BuildFlags.parse( + toml_path, quick_build=True, strict_mode=False + ) + + compiler_args: List[str] = [] + for item in build_flags.tools.cpp_compiler: + compiler_args.append(item) + for item in build_flags.compiler_flags: + compiler_args.append(item) + for item in build_flags.include_flags: + compiler_args.append(item) + + settings = CompilerOptions( + include_path=str(project_root / "src"), + defines=[], + compiler_args=compiler_args, + ) + + self.compiler = Compiler(settings, build_flags) + + def tearDown(self) -> None: + # Best effort cleanup of temp directory + try: + for root, dirs, files in os.walk(self.temp_dir, topdown=False): + for name in files: + try: + (Path(root) / name).unlink(missing_ok=True) + except Exception: + pass + for name in dirs: + try: + (Path(root) / name).rmdir() + except Exception: + pass + self.temp_dir.rmdir() + except Exception: + # Ignore cleanup issues on Windows + pass + + def test_unity_write_idempotent_and_updates_on_change(self) -> None: + # First run: generate and compile one chunk unity + options = CompilerOptions( + include_path=self.compiler.settings.include_path, + defines=[], + compiler_args=self.compiler.settings.compiler_args, + use_pch=False, + additional_flags=["-c"], + ) + + cpp_list: List[str] = [str(self.cpp1), str(self.cpp2)] + + result = self.compiler._compile_unity_chunks_sync( # Internal, synchronous for testing + options, + cpp_list, + chunks=1, + unity_dir=self.unity_dir, + no_parallel=True, + ) + + self.assertTrue(result.success, "Initial unity compilation should succeed") + + unity_cpp = self.unity_dir / "unity1.cpp" + self.assertTrue(unity_cpp.exists(), "unity1.cpp should be created") + + first_mtime = unity_cpp.stat().st_mtime_ns + + # Second run with identical inputs should not rewrite the file + time.sleep(0.01) # Ensure detectable time difference if rewritten + + result2 = self.compiler._compile_unity_chunks_sync( + options, + cpp_list, + chunks=1, + unity_dir=self.unity_dir, + no_parallel=True, + ) + self.assertTrue(result2.success, "Second unity compilation should succeed") + + second_mtime = unity_cpp.stat().st_mtime_ns + self.assertEqual( + first_mtime, + second_mtime, + "unity1.cpp should not be rewritten when content is unchanged", + ) + + # Change the input set (add a new file) to force unity content change and rewrite + cpp3 = self.src_dir / "c.cpp" + with open(cpp3, "w", encoding="utf-8") as f3: + f3.write( + """ +static int func_c() { return 3; } +int var_c = func_c(); +""".lstrip() + ) + + # Update list to include the new file + cpp_list.append(str(cpp3)) + + time.sleep(0.01) + result3 = self.compiler._compile_unity_chunks_sync( + options, + cpp_list, + chunks=1, + unity_dir=self.unity_dir, + no_parallel=True, + ) + self.assertTrue( + result3.success, "Unity compilation after input set change should succeed" + ) + + third_mtime = unity_cpp.stat().st_mtime_ns + self.assertGreater( + third_mtime, + second_mtime, + "unity1.cpp should be rewritten when the included file list changes", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_wasm_clang_format.py b/v1.5/lib/FastLED/ci/tests/test_wasm_clang_format.py new file mode 100644 index 0000000..b6645a6 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_wasm_clang_format.py @@ -0,0 +1,81 @@ +# pyright: reportUnknownMemberType=false +import os +import unittest +from concurrent.futures import ThreadPoolExecutor +from typing import List + +from ci.util.paths import PROJECT_ROOT + + +NUM_WORKERS = 1 if os.environ.get("NO_PARALLEL") else (os.cpu_count() or 1) * 4 + + +WASM_ROOT = PROJECT_ROOT / "src" / "platforms" / "wasm" + + +class TestMissingPragmaOnce(unittest.TestCase): + def check_file(self, file_path: str) -> list[str]: + """Check if a header file has #pragma once directive or if a cpp file incorrectly has it.""" + failings: list[str] = [] + + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + if file_path.endswith(".h") or file_path.endswith(".cpp"): + content = f.read() + # For header files, check if #pragma once is missing + if "EM_ASM_" in content and "// clang-format off\n" not in content: + if "clang-format off" not in content: + failings.append(f"Missing clang-format off in {file_path}") + else: + failings.append(f"clang-format off is malformed in {file_path}") + + return failings + + def test_esm_asm_and_clang_format(self) -> None: + files_to_check: List[str] = [] + current_dir = None + + # Collect files to check + for root, _, files in os.walk(WASM_ROOT): + # Log when we enter a new directory + rel_path = os.path.relpath(root, WASM_ROOT) + if current_dir != rel_path: + current_dir = rel_path + print(f"Traversing directory: {rel_path}") + + for file in files: + if file.endswith((".h", ".cpp")): # Check both header and cpp files + file_path = os.path.join(root, file) + files_to_check.append(file_path) + + print(f"Found {len(files_to_check)} files to check") + + # Process files in parallel + all_failings: List[str] = [] + with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor: + futures = [ + executor.submit(self.check_file, file_path) + for file_path in files_to_check + ] + for future in futures: + all_failings.extend(future.result()) + + # Report results + if all_failings: + msg = ( + f"Found {len(all_failings)} clang format issues in wasm: \n" + + "\n".join(all_failings) + ) + for failing in all_failings: + print(failing) + print( + "Please be aware you need // then one space then clang-format off then a new line exactly" + ) + self.fail(msg) + else: + print("All files passed the check.") + + print(f"Clange format check completed. Processed {len(files_to_check)} files.") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/test_wrong_defines.py b/v1.5/lib/FastLED/ci/tests/test_wrong_defines.py new file mode 100644 index 0000000..eb6b81a --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/test_wrong_defines.py @@ -0,0 +1,64 @@ +import os +import unittest +from concurrent.futures import ThreadPoolExecutor +from typing import List + +from ci.util.paths import PROJECT_ROOT + + +SRC_ROOT = PROJECT_ROOT / "src" +# PLATFORMS_DIR = os.path.join(SRC_ROOT, "platforms") + +NUM_WORKERS = 1 if os.environ.get("NO_PARALLEL") else (os.cpu_count() or 1) * 4 + +WRONG_DEFINES: dict[str, str] = { + "#if ESP32": "Use #ifdef ESP32 instead of #if ESP32", + "#if defined(FASTLED_RMT5)": "Use #ifdef FASTLED_RMT5 instead of #if defined(FASTLED_RMT5)", + "#if defined(FASTLED_ESP_HAS_CLOCKLESS_SPI)": "Use #ifdef FASTLED_ESP_HAS_CLOCKLESS_SPI instead of #if defined(FASTLED_ESP_HAS_CLOCKLESS_SPI)", +} + + +class TestWrongDefines(unittest.TestCase): + def check_file(self, file_path: str) -> List[str]: + failings: List[str] = [] + with open(file_path, "r", encoding="utf-8") as f: + for line_number, line in enumerate(f, 1): + line = line.strip() + if line.startswith("//"): + continue + for needle, message in WRONG_DEFINES.items(): + if needle in line: + failings.append(f"{file_path}:{line_number}: {message}") + return failings + + def test_no_bad_defines(self) -> None: + """Searches through the program files to check for banned headers, excluding src/platforms.""" + files_to_check: List[str] = [] + for root, _, files in os.walk(SRC_ROOT): + for file in files: + if file.endswith( + (".cpp", ".h", ".hpp") + ): # Add or remove file extensions as needed + file_path = os.path.join(root, file) + files_to_check.append(file_path) + + all_failings: List[str] = [] + with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor: + futures = [ + executor.submit(self.check_file, file_path) + for file_path in files_to_check + ] + for future in futures: + all_failings.extend(future.result()) + + if all_failings: + msg = f"Found {len(all_failings)} bad defines: \n" + "\n".join(all_failings) + for failing in all_failings: + print(failing) + self.fail("Please fix the defines: \n" + msg + "\n") + else: + print("No bad defines found.") + + +if __name__ == "__main__": + unittest.main() diff --git a/v1.5/lib/FastLED/ci/tests/uno/firmware.elf b/v1.5/lib/FastLED/ci/tests/uno/firmware.elf new file mode 100644 index 0000000..7d93df8 Binary files /dev/null and b/v1.5/lib/FastLED/ci/tests/uno/firmware.elf differ diff --git a/v1.5/lib/FastLED/ci/tests/uno/firmware.hex b/v1.5/lib/FastLED/ci/tests/uno/firmware.hex new file mode 100644 index 0000000..8f63143 --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/uno/firmware.hex @@ -0,0 +1,237 @@ +:100000000C9435000C945D000C945D000C945D0024 +:100010000C945D000C945D000C945D000C945D00EC +:100020000C945D000C945D000C945D000C945D00DC +:100030000C945D000C945D000C945D000C945D00CC +:100040000C948D050C945D000C945D000C945D0087 +:100050000C945D000C945D000C945D000C945D00AC +:100060000C945D000C945D00BB0611241FBECFEF05 +:10007000D8E0DEBFCDBF11E0A0E0B1E0EEE8FEE0E9 +:1000800002C005900D92AE32B107D9F721E0AEE281 +:10009000B1E001C01D92A637B207E1F710E0C5E359 +:1000A000D0E004C02197FE010E943A07C433D10773 +:1000B000C9F70E94D7050C9445070C9400003FB780 +:1000C000F8948091550190915601A0915701B091FB +:1000D000580126B5A89B05C02F3F19F00196A11D18 +:1000E000B11D3FBFBA2FA92F982F8827BC01CD0182 +:1000F000620F711D811D911D42E0660F771F881FE1 +:10010000991F4A95D1F708952F923F924F925F928F +:100110006F927F928F929F92AF92BF92CF92DF9217 +:10012000EF92FF920F931F93CF93DF93CDB7DEB77C +:10013000C358D1090FB6F894DEBF0FBECDBF782EDD +:100140008091660190916701A0916801B091690169 +:10015000892B8A2B8B2BD9F00E945F0000917201B2 +:10016000109173012091740130917501601B710B26 +:10017000820B930B00916601109167012091680139 +:10018000309169016017710782079307C8F20E94D6 +:100190005F00609372017093730180937401909378 +:1001A0007501E0916E01F0916F01309759F0409127 +:1001B0006A0150916B0160916C0170916D01872D06 +:1001C0000995782EFE01319680E8DF011D928A950F +:1001D000E9F7E0907001F09071016F015E01B1E804 +:1001E000AB0EB11C4F01472C512CE114F10409F462 +:1001F0005CC08A149B0409F458C0D701ED91FC91AE +:100200000484F585E02DC7010995F401819391934C +:100210004F0180916401909165018436910518F435 +:10022000D7011C961C92D701ED91FC91228033805E +:10023000F501108211821282772019F187010A5F7D +:100240001F4FF0E0E0E0D8012D918D012223A9F0AD +:1002500012966C90662089F030E02F5F3F4FD201FC +:100260000E9405079B01AC01A62DB0E011960E94EB +:100270001B07D501AE0FBF1F8C933196E330F105FC +:1002800011F7F501008111812281D7011D964D9151 +:100290005C911E9712966D917C91C701F1010995B1 +:1002A000D7011496ED90FC90A0CF009170011091B1 +:1002B00071010115110599F0CA14DB0481F0F601F2 +:1002C000619171916F01D801ED91FC910684F785E0 +:1002D000E02DC8010995F80104811581EACF8091CC +:1002E0006101909162019C012F5F3F4F3093620149 +:1002F0002093610149970CF44AC08FB7F89420917C +:10030000590130915A0140915B0150915C018FBFBE +:1003100080915D0190915E01A0915F01B0916001BB +:10032000281B390B4A0B5B0B21F421E030E040E045 +:1003300050E0E0916101F091620188EE93E0E89F66 +:10034000B001E99F700DF89F700D1124072E000C6D +:10035000880B990B0E94E30630936501209364019A +:1003600010926201109261012FB7F89480915901A7 +:1003700090915A01A0915B01B0915C012FBF8093D5 +:100380005D0190935E01A0935F01B0936001CD5732 +:10039000DF4F0FB6F894DEBF0FBECDBFDF91CF9118 +:1003A0001F910F91FF90EF90DF90CF90BF90AF9093 +:1003B0009F908F907F906F905F904F903F902F9085 +:1003C00008958F929F92AF92BF92CF92DF92EF9259 +:1003D000FF920E945F004B015C0184EFC82EDD2478 +:1003E000D394E12CF12C0E945F00681979098A09E5 +:1003F0009B09683E734081059105A8F321E0C21A6C +:10040000D108E108F10888EE880E83E0981EA11C4F +:10041000B11CC114D104E104F10429F7FF90EF905D +:10042000DF90CF90BF90AF909F908F90089580E91C +:1004300091E008950F931F93CF93DF9320912F01A5 +:100440002F5F322F377030932F0120FF2BC020E811 +:1004500031FD2064347009F02062205FFC01EC0162 +:10046000239600E011E06485662329F070E0C8015E +:100470000E94CF066F5F6187822F869F080E80E003 +:10048000811D1124811110C01682662311F0615064 +:1004900061873196EC17FD0731F7DF91CF911F91FE +:1004A0000F91089520E0D4CF81508683EECF4F92F4 +:1004B0005F927F928F929F92AF92BF92CF92DF9284 +:1004C000EF92FF920F931F93CF93DF932C01EB01D9 +:1004D0000E945F00F20125893689621B730B6A3026 +:1004E0007105B0F3F8948A819B81181619060CF0F7 +:1004F000CDC1E881F9816BB1862E689483F83BB158 +:10050000377F3BB9DA848F812D2D281B822F2F83D3 +:100510004F85042E000C550BAA81BB817D85FC8480 +:10052000EE847F5FF394E3949E819884B984AB84D6 +:100530001181C12C6C2D0C2D2C2D2181112788941B +:100540002111280F08F42FEF8195889470FD120F68 +:100550001795889471FD120F1795889472FD120FEC +:100560001795889473FD120F1795889474FD120FD8 +:100570001795889475FD120F1795889476FD120FC4 +:100580001795889477FD120F17958894622F711133 +:10059000612F8D0D162F002C8BB800C017FF3BB9B3 +:1005A00020816627889400C000C0002C3BB921112F +:1005B000290F00C0002C8BB800C016FF3BB908F40F +:1005C0002FEF9195889400C000C0002C3BB9F0FC3F +:1005D000620F00C0002C8BB800C015FF3BB96795B7 +:1005E0008894F1FC620F00C000C0002C3BB96795F5 +:1005F000889400C0002C8BB800C014FF3BB9F2FCFB +:10060000620F6795889400C000C0002C3BB9F3FCD2 +:10061000620F00C0002C8BB800C013FF3BB9679578 +:100620008894F4FC620F00C000C0002C3BB96795B1 +:10063000889400C0002C8BB800C012FF3BB9F5FCB9 +:10064000620F6795889400C000C0002C3BB9F6FC8F +:10065000620F00C0002C8BB800C011FF3BB967953A +:100660008894F7FC620F00C000C0002C3BB967956E +:10067000889400C0002C8BB800C010FF3BB9122F2B +:10068000F110162F9B0D00C000C0002C3BB900C01C +:1006900000C0002C8BB800C017FF3BB92281662731 +:1006A000889400C000C0002C3BB92111290D00C066 +:1006B000002C8BB800C016FF3BB908F42FEFE40FF5 +:1006C000F51F00C000C0002C3BB9E0FC620F00C069 +:1006D000002C8BB800C015FF3BB967958894E1FCEE +:1006E000620F00C000C0002C3BB96795889400C021 +:1006F000002C8BB800C014FF3BB9E2FC620F679579 +:10070000889400C000C0002C3BB9E3FC620F00C01D +:10071000002C8BB800C013FF3BB967958894E4FCAC +:10072000620F00C000C0002C3BB96795889400C0E0 +:10073000002C8BB800C012FF3BB9E5FC620F679537 +:10074000889400C000C0002C3BB9E6FC620F00C0DA +:10075000002C8BB800C011FF3BB967958894E7FC6B +:10076000620F00C000C0002C3BB96795889400C0A0 +:10077000002C8BB800C010FF3BB9122FE110162FD0 +:10078000919400C000C0002C3BB99A0C00C000C07E +:100790008BB800C017FF3BB921816627889400C041 +:1007A00000C0002C3BB92111280F00C0002C8BB8D1 +:1007B00000C016FF3BB908F42FEF8195889400C064 +:1007C00000C0002C3BB970FD620F00C0002C8BB83C +:1007D00000C015FF3BB96795889471FD620F00C09A +:1007E00000C0002C3BB96795889400C0002C8BB8E2 +:1007F00000C014FF3BB972FD620F6795889400C07A +:1008000000C0002C3BB973FD620F00C0002C8BB8F8 +:1008100000C013FF3BB96795889474FD620F00C058 +:1008200000C0002C3BB96795889400C0002C8BB8A1 +:1008300000C012FF3BB975FD620F6795889400C038 +:1008400000C0002C3BB976FD620F00C0002C8BB8B5 +:1008500000C011FF3BB96795889477FD620F00C017 +:1008600000C0002C3BB96795889400C0002C8BB861 +:1008700000C010FF3BB9122F7111162F8D0D00C053 +:1008800000C0002C3BB9119709F086CE4A815B81EC +:1008900020EE31E0DA010E941407DC01CB01F4E024 +:1008A000B695A79597958795FA95D1F730E020E012 +:1008B000B901EAE94E9F040E611D5E9F600D711D36 +:1008C0001124650F711D860F971FA11DB11D893E53 +:1008D00043E09407A105B10508F434C0885E934055 +:1008E000A109B10942E0B695A795979587954A95D4 +:1008F000D1F747E0849F080E211D949F200D311DE4 +:100900001124290F311D60912E0170E0860F971F71 +:10091000820F931F4091590150915A0160915B01E0 +:1009200070915C01292F3327420F531F611D711DE8 +:100930004093590150935A0160935B0170935C019D +:1009400080932E0178940E945F00F201768B658B74 +:10095000DF91CF911F910F91FF90EF90DF90CF909B +:10096000BF90AF909F908F907F905F904F90089531 +:1009700081E090E00895539A08956F927F928F924C +:10098000CF92DF92EF92FF920F931F93CF93DF935B +:10099000CDB7DEB762970FB6F894DEBF0FBECDBFFE +:1009A0006C017A013801822EDC011C962C91CA015F +:1009B00057FF04C088279927841B950B7A83698386 +:1009C0009C838B839E838D836D867E868F8621306C +:1009D00049F5CE0101960E941A0283E0888B1A8A9B +:1009E000198AF7FE02C08DEF888BD601ED91FC913C +:1009F0000288F389E02DBE016F5F7F4FC601099524 +:100A000062960FB6F894DEBF0FBECDBFDF91CF91D7 +:100A10001F910F91FF90EF90DF90CF908F907F907C +:100A20006F9008951C861B861A86198618861F8269 +:100A3000D4CFEF92FF920F931F93CF93DF93CDB755 +:100A4000DEB762970FB6F894DEBF0FBECDBF7C0154 +:100A5000DC011C968C917A8369835C834B835E8373 +:100A60004D830D871E872F878130F9F4CE010196C3 +:100A70000E941A02188A1A8A198AD701ED91FC91EC +:100A80000288F389E02DBE016F5F7F4FC701099592 +:100A900062960FB6F894DEBF0FBECDBFDF91CF9147 +:100AA0001F910F91FF90EF9008951C861B861A8668 +:100AB000198618861F82DECF90E080E00895FC0141 +:100AC00064870895FC01848590E00895FC01858584 +:100AD000968508950F931F93CF93DF9300D01F92B5 +:100AE000CDB7DEB7AB0119821A821B82DC01ED9112 +:100AF000FC910190F081E02D00E010E020E0BE01CB +:100B00006F5F7F4F09950F900F900F90DF91CF91FE +:100B10001F910F9108950E9440071F920F920FB6E8 +:100B20000F9211242F933F938F939F93AF93BF9373 +:100B30008091590190915A01A0915B01B0915C01A3 +:100B40003091540123E0230F2D3758F50196A11D54 +:100B5000B11D209354018093590190935A01A093A1 +:100B60005B01B0935C018091550190915601A09179 +:100B70005701B09158010196A11DB11D80935501F7 +:100B800090935601A0935701B0935801BF91AF9134 +:100B90009F918F913F912F910F900FBE0F901F90BB +:100BA000189526E8230F0296A11DB11DD2CF789487 +:100BB00084B5826084BD84B5816084BD85B5826062 +:100BC00085BD85B5816085BD80916E008160809313 +:100BD0006E0010928100809181008260809381007C +:100BE000809181008160809381008091800081608C +:100BF000809380008091B10084608093B1008091E7 +:100C0000B00081608093B00080917A00846080930E +:100C10007A0080917A00826080937A0080917A00D5 +:100C2000816080937A0080917A00806880937A0056 +:100C30001092C10080914901811155C01092350177 +:100C4000109234018FEF80933801809339018093A3 +:100C50003A0180933B0180933C0180933D0181E008 +:100C600080933E011092400110923F0180E797E18E +:100C7000909342018093410183E090E0909344017E +:100C80008093430110924601109245011092370162 +:100C9000109236018091700190917101892B31F48D +:100CA00082E391E09093710180937001E0913001B3 +:100CB000F0913101309721F082E391E095838483B4 +:100CC00082E391E0909331018093300110924801CA +:100CD000109247018AE191E09093330180933201B1 +:100CE00081E080934901539A81E591E09093350129 +:100CF0008093340181E090E09093400180933F0124 +:100D00008091660190916701A0916801B09169019D +:100D1000843C29E09207A105B10520F484EC99E018 +:100D2000A0E0B0E08093660190936701A093680112 +:100D3000B0936901CFEF00E010E0C0935101109231 +:100D4000520110925301809163010E9484000E941D +:100D5000E1011092510110925201109253018091C1 +:100D600063010E9484000E94E1010115110529F32D +:100D70000E940000E2CFE3E6F1E08FEF8083128271 +:100D80001182148613868FEF9FEFDC018783908793 +:100D9000A187B2871382148215821682089597FB69 +:100DA000072E16F4009407D077FD09D00E9426077D +:100DB00007FC05D03EF4909581959F4F089570955E +:100DC00061957F4F0895A1E21A2EAA1BBB1BFD015E +:100DD0000DC0AA1FBB1FEE1FFF1FA217B307E4071A +:100DE000F50720F0A21BB30BE40BF50B661F771F72 +:100DF000881F991F1A9469F7609570958095909552 +:100E00009B01AC01BD01CF010895A29FB001B39F2A +:100E1000C001A39F700D811D1124911DB29F700D03 +:100E2000811D1124911D08950E940507B7FF0895A3 +:100E3000821B930B08950E940507A59F900DB49FF8 +:100E4000900DA49F800D911D11240895AA1BBB1B1A +:100E500051E107C0AA1FBB1FA617B70710F0A61BBA +:100E6000B70B881F991F5A95A9F780959095BC01DB +:100E7000CD010895EE0FFF1F0590F491E02D099428 +:0E0E800081E090E0F8940C944507F894FFCFC1 +:100E8E00000000008B058B058B056A056605B8040E +:100E9E0062055F055C05000000001905BD04BB047A +:0E0EAE006A056605B80462055F051702570263 +:00000001FF diff --git a/v1.5/lib/FastLED/ci/tests/uno/firmware.map b/v1.5/lib/FastLED/ci/tests/uno/firmware.map new file mode 100644 index 0000000..3f887eb --- /dev/null +++ b/v1.5/lib/FastLED/ci/tests/uno/firmware.map @@ -0,0 +1,789 @@ +Archive member included to satisfy reference by file (symbol) + +.pio\build\uno\lib6ec\libsrc.a(FastLED.cpp.o) + .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) (_ZN14CLEDController7m_pHeadE) +.pio\build\uno\lib6ec\libsrc.a(crgb.cpp.o) + .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) (_ZN4CRGB17computeAdjustmentEhRKS_S1_) +.pio\build\uno\lib6ec\libsrc.a(lib8tion.cpp.o) + FastLED.cpp.o (symbol from plugin) (memset8) +.pio\build\uno\libFrameworkArduino.a(abi.cpp.o) + .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) (__cxa_pure_virtual) +.pio\build\uno\libFrameworkArduino.a(hooks.c.o) + FastLED.cpp.o (symbol from plugin) (yield) +.pio\build\uno\libFrameworkArduino.a(main.cpp.o) + c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o (main) +.pio\build\uno\libFrameworkArduino.a(wiring.c.o) + .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) (timer0_millis) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o (exit) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__divmodhi4) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__udivmodsi4) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__do_copy_data) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__do_clear_bss) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__do_global_ctors) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__umulhisi3) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__usmulhisi3) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (__muluhisi3) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) (__udivmodhi4) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) (__tablejump2__) +c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libc.a(abort.o) + C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o (abort) + +Discarded input sections + + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + .text 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController4sizeEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController5lanesEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController13beginShowLedsEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController11endShowLedsEPv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZNK14CLEDController17getMaxRefreshRateEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19CPixelLEDControllerIL6EOrder66ELi1ELm4294967295EE5lanesEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZNK19ClocklessControllerILh3ELi4ELi10ELi6EL6EOrder66ELi0ELb0ELi10EE17getMaxRefreshRateEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDControllerC5Ev + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZTV14CLEDController + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController13getAdjustmentEh + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController9showColorERK4CRGBih + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController9clearLedsEi + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN8CFastLED4showEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19CPixelLEDControllerIL6EOrder66ELi1ELm4294967295EEC5Ev + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZTV19CPixelLEDControllerIL6EOrder66ELi1ELm4294967295EE + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19ClocklessControllerILh3ELi4ELi10ELi6EL6EOrder66ELi0ELb0ELi10EEC5Ev + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZTV19ClocklessControllerILh3ELi4ELi10ELi6EL6EOrder66ELi0ELb0ELi10EE + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN22WS2812Controller800KhzILh3EL6EOrder66EEC5Ev + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZTV22WS2812Controller800KhzILh3EL6EOrder66EE + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN8NEOPIXELILh3EEC5Ev + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZTV8NEOPIXELILh3EE + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN8CFastLED7addLedsI8NEOPIXELLh3EEER14CLEDControllerP4CRGBii + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZGVZN8CFastLED7addLedsI8NEOPIXELLh3EEER14CLEDControllerP4CRGBiiE1c + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZZN8CFastLED7addLedsI8NEOPIXELLh3EEER14CLEDControllerP4CRGBiiE1c + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN7_AVRPINILh3ELh8E18__gen_struct_PORTD17__gen_struct_DDRD17__gen_struct_PINDE9setOutputEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19ClocklessControllerILh3ELi4ELi10ELi6EL6EOrder66ELi0ELb0ELi10EE4initEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN8CMinWaitILi10EE4waitEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19ClocklessControllerILh3ELi4ELi10ELi6EL6EOrder66ELi0ELb0ELi10EE15showRGBInternalER15PixelControllerILS0_66ELi1ELm4294967295EE + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN8CMinWaitILi10EE4markEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19ClocklessControllerILh3ELi4ELi10ELi6EL6EOrder66ELi0ELb0ELi10EE10showPixelsER15PixelControllerILS0_66ELi1ELm4294967295EE + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN15PixelControllerIL6EOrder66ELi1ELm4294967295EE11initOffsetsEi + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN15PixelControllerIL6EOrder66ELi1ELm4294967295EE21init_binary_ditheringEv + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZZN15PixelControllerIL6EOrder66ELi1ELm4294967295EE21init_binary_ditheringEvE1R + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN15PixelControllerIL6EOrder66ELi1ELm4294967295EE16enable_ditheringEh + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN15PixelControllerIL6EOrder66ELi1ELm4294967295EEC5EPK4CRGBiRS2_h + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19CPixelLEDControllerIL6EOrder66ELi1ELm4294967295EE4showEPK4CRGBiS2_ + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN15PixelControllerIL6EOrder66ELi1ELm4294967295EEC5ERK4CRGBiRS2_h + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN19CPixelLEDControllerIL6EOrder66ELi1ELm4294967295EE9showColorERK4CRGBiS2_ + 0x00000000 0x0 .pio\build\uno\src\Blink.ino.cpp.o (symbol from plugin) + .data 0x00000000 0x0 C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o + .text 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController12clearLedDataEv + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController13beginShowLedsEv + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController11endShowLedsEPv + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController13getAdjustmentEh + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController8showLedsEh + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN14CLEDController9showColorERK4CRGBh + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZNK14CLEDController17getMaxRefreshRateEv + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .gnu.linkonce.t._ZN8CFastLED4showEv + 0x00000000 0x0 FastLED.cpp.o (symbol from plugin) + .text 0x00000000 0x0 crgb.cpp.o (symbol from plugin) + .text 0x00000000 0x0 lib8tion.cpp.o (symbol from plugin) + .text 0x00000000 0x0 abi.cpp.o (symbol from plugin) + .text 0x00000000 0x0 hooks.c.o (symbol from plugin) + .text 0x00000000 0x0 main.cpp.o (symbol from plugin) + .text 0x00000000 0x0 wiring.c.o (symbol from plugin) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .text.libgcc 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .text.libgcc.mul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .text.libgcc.div + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .text.libgcc.prologue + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .text.libgcc.builtins + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .text.libgcc.fmul + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .text.libgcc.fixed + 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + .text 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libc.a(abort.o) + .data 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libc.a(abort.o) + .bss 0x00000000 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libc.a(abort.o) + +Memory Configuration + +Name Origin Length Attributes +text 0x00000000 0x00020000 xr +data 0x00800060 0x0000ffa0 rw !x +eeprom 0x00810000 0x00010000 rw !x +fuse 0x00820000 0x00000003 rw !x +lock 0x00830000 0x00000400 rw !x +signature 0x00840000 0x00000400 rw !x +user_signatures 0x00850000 0x00000400 rw !x +*default* 0x00000000 0xffffffff + +Linker script and memory map + +Address of section .data set to 0x800100 +LOAD c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o +LOAD .pio\build\uno\src\Blink.ino.cpp.o +LOAD C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o +START GROUP +LOAD .pio\build\uno\liba19\libSoftwareSerial.a +LOAD .pio\build\uno\lib8b0\libSPI.a +LOAD .pio\build\uno\lib6ec\libsrc.a +LOAD .pio\build\uno\libFrameworkArduinoVariant.a +LOAD .pio\build\uno\libFrameworkArduino.a +END GROUP +LOAD c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a +LOAD c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libm.a +START GROUP +LOAD c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a +LOAD c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libm.a +LOAD c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libc.a +LOAD c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libatmega328p.a +END GROUP + 0x00020000 __TEXT_REGION_LENGTH__ = DEFINED (__TEXT_REGION_LENGTH__)?__TEXT_REGION_LENGTH__:0x20000 + 0x0000ffa0 __DATA_REGION_LENGTH__ = DEFINED (__DATA_REGION_LENGTH__)?__DATA_REGION_LENGTH__:0xffa0 + 0x00010000 __EEPROM_REGION_LENGTH__ = DEFINED (__EEPROM_REGION_LENGTH__)?__EEPROM_REGION_LENGTH__:0x10000 + [0x00000003] __FUSE_REGION_LENGTH__ = DEFINED (__FUSE_REGION_LENGTH__)?__FUSE_REGION_LENGTH__:0x400 + 0x00000400 __LOCK_REGION_LENGTH__ = DEFINED (__LOCK_REGION_LENGTH__)?__LOCK_REGION_LENGTH__:0x400 + 0x00000400 __SIGNATURE_REGION_LENGTH__ = DEFINED (__SIGNATURE_REGION_LENGTH__)?__SIGNATURE_REGION_LENGTH__:0x400 + 0x00000400 __USER_SIGNATURE_REGION_LENGTH__ = DEFINED (__USER_SIGNATURE_REGION_LENGTH__)?__USER_SIGNATURE_REGION_LENGTH__:0x400 + +.hash + *(.hash) + +.dynsym + *(.dynsym) + +.dynstr + *(.dynstr) + +.gnu.version + *(.gnu.version) + +.gnu.version_d + *(.gnu.version_d) + +.gnu.version_r + *(.gnu.version_r) + +.rel.init + *(.rel.init) + +.rela.init + *(.rela.init) + +.rel.text + *(.rel.text) + *(.rel.text.*) + *(.rel.gnu.linkonce.t*) + +.rela.text + *(.rela.text) + *(.rela.text.*) + *(.rela.gnu.linkonce.t*) + +.rel.fini + *(.rel.fini) + +.rela.fini + *(.rela.fini) + +.rel.rodata + *(.rel.rodata) + *(.rel.rodata.*) + *(.rel.gnu.linkonce.r*) + +.rela.rodata + *(.rela.rodata) + *(.rela.rodata.*) + *(.rela.gnu.linkonce.r*) + +.rel.data + *(.rel.data) + *(.rel.data.*) + *(.rel.gnu.linkonce.d*) + +.rela.data + *(.rela.data) + *(.rela.data.*) + *(.rela.gnu.linkonce.d*) + +.rel.ctors + *(.rel.ctors) + +.rela.ctors + *(.rela.ctors) + +.rel.dtors + *(.rel.dtors) + +.rela.dtors + *(.rela.dtors) + +.rel.got + *(.rel.got) + +.rela.got + *(.rela.got) + +.rel.bss + *(.rel.bss) + +.rela.bss + *(.rela.bss) + +.rel.plt + *(.rel.plt) + +.rela.plt + *(.rela.plt) + +.text 0x00000000 0xe8e + *(.vectors) + .vectors 0x00000000 0x68 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + 0x00000000 __vector_default + 0x00000000 __vectors + *(.vectors) + *(.progmem.gcc*) + 0x00000068 . = ALIGN (0x2) + 0x00000068 __trampolines_start = . + *(.trampolines) + .trampolines 0x00000068 0x0 linker stubs + *(.trampolines*) + 0x00000068 __trampolines_end = . + *libprintf_flt.a:*(.progmem.data) + *libc.a:*(.progmem.data) + *(.progmem*) + 0x00000068 . = ALIGN (0x2) + *(.jumptables) + *(.jumptables*) + *(.lowtext) + *(.lowtext*) + 0x00000068 __ctors_start = . + *(.ctors) + .ctors 0x00000068 0x2 C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o + 0x0000006a __ctors_end = . + 0x0000006a __dtors_start = . + *(.dtors) + 0x0000006a __dtors_end = . + SORT(*)(.ctors) + SORT(*)(.dtors) + *(.init0) + .init0 0x0000006a 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + 0x0000006a __init + *(.init0) + *(.init1) + *(.init1) + *(.init2) + .init2 0x0000006a 0xc c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + *(.init2) + *(.init3) + *(.init3) + *(.init4) + .init4 0x00000076 0x16 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + 0x00000076 __do_copy_data + .init4 0x0000008c 0x10 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + 0x0000008c __do_clear_bss + *(.init4) + *(.init5) + *(.init5) + *(.init6) + .init6 0x0000009c 0x16 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + 0x0000009c __do_global_ctors + *(.init6) + *(.init7) + *(.init7) + *(.init8) + *(.init8) + *(.init9) + .init9 0x000000b2 0x8 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + *(.init9) + *(.text) + .text 0x000000ba 0x4 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + 0x000000ba __vector_22 + 0x000000ba __vector_1 + 0x000000ba __vector_24 + 0x000000ba __vector_12 + 0x000000ba __bad_interrupt + 0x000000ba __vector_6 + 0x000000ba __vector_3 + 0x000000ba __vector_23 + 0x000000ba __vector_25 + 0x000000ba __vector_11 + 0x000000ba __vector_13 + 0x000000ba __vector_17 + 0x000000ba __vector_19 + 0x000000ba __vector_7 + 0x000000ba __vector_5 + 0x000000ba __vector_4 + 0x000000ba __vector_9 + 0x000000ba __vector_2 + 0x000000ba __vector_21 + 0x000000ba __vector_15 + 0x000000ba __vector_8 + 0x000000ba __vector_14 + 0x000000ba __vector_10 + 0x000000ba __vector_18 + 0x000000ba __vector_20 + .text 0x000000be 0xaf0 C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o + 0x00000b1a __vector_16 + 0x00000bae . = ALIGN (0x2) + *(.text.*) + .text.startup 0x00000bae 0x1f0 C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o + 0x00000bae main + .text.libgcc.div + 0x00000d9e 0x28 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + 0x00000d9e _div + 0x00000d9e __divmodhi4 + .text.libgcc.div + 0x00000dc6 0x44 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + 0x00000dc6 __udivmodsi4 + .text.libgcc.mul + 0x00000e0a 0x1e c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + 0x00000e0a __umulhisi3 + .text.libgcc.mul + 0x00000e28 0xe c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + 0x00000e28 __usmulhisi3 + 0x00000e2c __usmulhisi3_tail + .text.libgcc.mul + 0x00000e36 0x16 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + 0x00000e36 __muluhisi3 + .text.libgcc.div + 0x00000e4c 0x28 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + 0x00000e4c __udivmodhi4 + .text.libgcc 0x00000e74 0xc c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + 0x00000e74 __tablejump2__ + .text.avr-libc + 0x00000e80 0xa c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5\libc.a(abort.o) + 0x00000e80 abort + 0x00000e8a . = ALIGN (0x2) + *(.fini9) + .fini9 0x00000e8a 0x0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + 0x00000e8a _exit + 0x00000e8a exit + *(.fini9) + *(.fini8) + *(.fini8) + *(.fini7) + *(.fini7) + *(.fini6) + *(.fini6) + *(.fini5) + *(.fini5) + *(.fini4) + *(.fini4) + *(.fini3) + *(.fini3) + *(.fini2) + *(.fini2) + *(.fini1) + *(.fini1) + *(.fini0) + .fini0 0x00000e8a 0x4 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + *(.fini0) + 0x00000e8e _etext = . + +.data 0x00800100 0x2e load address 0x00000e8e + 0x00800100 PROVIDE (__data_start, .) + *(.data) + *(.data*) + *(.gnu.linkonce.d*) + *(.rodata) + .rodata 0x00800100 0x2e C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o + *(.rodata*) + *(.gnu.linkonce.r*) + 0x0080012e . = ALIGN (0x2) + 0x0080012e _edata = . + 0x0080012e PROVIDE (__data_end, .) + +.bss 0x0080012e 0x48 + 0x0080012e PROVIDE (__bss_start, .) + *(.bss) + .bss 0x0080012e 0x48 C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o + *(.bss*) + *(COMMON) + 0x00800176 PROVIDE (__bss_end, .) + 0x00000e8e __data_load_start = LOADADDR (.data) + 0x00000ebc __data_load_end = (__data_load_start + SIZEOF (.data)) + +.noinit 0x00800176 0x0 + [!provide] PROVIDE (__noinit_start, .) + *(.noinit*) + [!provide] PROVIDE (__noinit_end, .) + 0x00800176 _end = . + [!provide] PROVIDE (__heap_start, .) + +.eeprom 0x00810000 0x0 + *(.eeprom*) + 0x00810000 __eeprom_end = . + +.fuse + *(.fuse) + *(.lfuse) + *(.hfuse) + *(.efuse) + +.lock + *(.lock*) + +.signature + *(.signature*) + +.user_signatures + *(.user_signatures*) + +.stab + *(.stab) + +.stabstr + *(.stabstr) + +.stab.excl + *(.stab.excl) + +.stab.exclstr + *(.stab.exclstr) + +.stab.index + *(.stab.index) + +.stab.indexstr + *(.stab.indexstr) + +.comment 0x00000000 0x11 + *(.comment) + .comment 0x00000000 0x11 C:\Users\niteris\AppData\Local\Temp\ccAA6ajC.ltrans0.ltrans.o + 0x12 (size before relaxing) + +.note.gnu.avr.deviceinfo + 0x00000000 0x40 + .note.gnu.avr.deviceinfo + 0x00000000 0x40 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + +.note.gnu.build-id + *(.note.gnu.build-id) + +.debug + *(.debug) + +.line + *(.line) + +.debug_srcinfo + *(.debug_srcinfo) + +.debug_sfnames + *(.debug_sfnames) + +.debug_aranges 0x00000000 0x160 + *(.debug_aranges) + .debug_aranges + 0x00000000 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .debug_aranges + 0x00000020 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .debug_aranges + 0x00000040 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .debug_aranges + 0x00000060 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .debug_aranges + 0x00000080 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .debug_aranges + 0x000000a0 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .debug_aranges + 0x000000c0 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .debug_aranges + 0x000000e0 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .debug_aranges + 0x00000100 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .debug_aranges + 0x00000120 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .debug_aranges + 0x00000140 0x20 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + +.debug_pubnames + *(.debug_pubnames) + +.debug_info 0x00000000 0xdfd + *(.debug_info .gnu.linkonce.wi.*) + .debug_info 0x00000000 0x5f4 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + .debug_info 0x000005f4 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .debug_info 0x000006af 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .debug_info 0x0000076a 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .debug_info 0x00000825 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .debug_info 0x000008e0 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .debug_info 0x0000099b 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .debug_info 0x00000a56 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .debug_info 0x00000b11 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .debug_info 0x00000bcc 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .debug_info 0x00000c87 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .debug_info 0x00000d42 0xbb c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + +.debug_abbrev 0x00000000 0x67e + *(.debug_abbrev) + .debug_abbrev 0x00000000 0x5a2 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + .debug_abbrev 0x000005a2 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .debug_abbrev 0x000005b6 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .debug_abbrev 0x000005ca 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .debug_abbrev 0x000005de 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .debug_abbrev 0x000005f2 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .debug_abbrev 0x00000606 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .debug_abbrev 0x0000061a 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .debug_abbrev 0x0000062e 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .debug_abbrev 0x00000642 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .debug_abbrev 0x00000656 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .debug_abbrev 0x0000066a 0x14 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + +.debug_line 0x00000000 0x71a + *(.debug_line .debug_line.* .debug_line_end) + .debug_line 0x00000000 0x1a c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + .debug_line 0x0000001a 0x62 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_exit.o) + .debug_line 0x0000007c 0xc8 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_divmodhi4.o) + .debug_line 0x00000144 0x122 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodsi4.o) + .debug_line 0x00000266 0x98 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_copy_data.o) + .debug_line 0x000002fe 0x86 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_clear_bss.o) + .debug_line 0x00000384 0x92 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_ctors.o) + .debug_line 0x00000416 0xb0 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_umulhisi3.o) + .debug_line 0x000004c6 0x7a c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_usmulhisi3.o) + .debug_line 0x00000540 0x92 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_muluhisi3.o) + .debug_line 0x000005d2 0xce c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_udivmodhi4.o) + .debug_line 0x000006a0 0x7a c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/avr5\libgcc.a(_tablejump2.o) + +.debug_frame + *(.debug_frame) + +.debug_str 0x00000000 0x208 + *(.debug_str) + .debug_str 0x00000000 0x208 c:/users/niteris/.platformio/packages/toolchain-atmelavr/bin/../lib/gcc/avr/7.3.0/../../../../avr/lib/avr5/crtatmega328p.o + +.debug_loc + *(.debug_loc) + +.debug_macinfo + *(.debug_macinfo) + +.debug_weaknames + *(.debug_weaknames) + +.debug_funcnames + *(.debug_funcnames) + +.debug_typenames + *(.debug_typenames) + +.debug_varnames + *(.debug_varnames) + +.debug_pubtypes + *(.debug_pubtypes) + +.debug_ranges + *(.debug_ranges) + +.debug_macro + *(.debug_macro) +OUTPUT(.pio\build\uno\firmware.elf elf32-avr) +LOAD linker stubs diff --git a/v1.5/lib/FastLED/ci/util/__init__.py b/v1.5/lib/FastLED/ci/util/__init__.py new file mode 100644 index 0000000..5ea5b75 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/__init__.py @@ -0,0 +1 @@ +"""FastLED CI tools and utilities - internal package.""" diff --git a/v1.5/lib/FastLED/ci/util/bin_2_elf.py b/v1.5/lib/FastLED/ci/util/bin_2_elf.py new file mode 100644 index 0000000..ff2b312 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/bin_2_elf.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 +""" +Improved Binary to ELF Converter +Handles platform-specific binary formats for ESP32, Uno, and other platforms. +""" + +import json +import subprocess +from pathlib import Path + + +def _run_command(command: list[str] | str, show_output: bool = False) -> str: + """ + Run a command using subprocess and capture the output. + + Args: + command (list or str): Command to run. + show_output (bool): Print command and its output if True. + + Returns: + str: Standard output of the command. + + Raises: + RuntimeError: If the command fails. + """ + if isinstance(command, str): + command = [command] + + if show_output: + print(f"Running command: {' '.join(command)}") + + result = subprocess.run(command, capture_output=True, text=True) + if result.returncode != 0: + if show_output: + print(f"Command failed: {' '.join(command)}") + print(f"Error: {result.stderr}") + raise RuntimeError(f"Command failed: {' '.join(command)}\n{result.stderr}") + + if show_output and result.stdout: + print(f"Command output: {result.stdout}") + return result.stdout + + +def _detect_platform_from_paths(as_path: Path, ld_path: Path): + """ + Detect the target platform from toolchain paths. + + Returns: + str: Platform name (esp32, avr, arm, etc.) + """ + path_str = str(as_path).lower() + + if "xtensa-esp32" in path_str or "esp32" in path_str: + return "esp32" + elif "avr" in path_str: + return "avr" + elif "arm" in path_str: + return "arm" + else: + return "unknown" + + +def _analyze_binary_structure(bin_file: Path, platform: str): + """ + Analyze binary structure based on platform. + + Args: + bin_file (Path): Path to binary file + platform (str): Target platform + + Returns: + dict: Binary analysis information + """ + print(f"Analyzing {platform} binary: {bin_file}") + + if not bin_file.exists(): + raise RuntimeError(f"Binary file not found: {bin_file}") + + with open(bin_file, "rb") as f: + data = f.read() + + analysis: dict[str, str | int | bytes | dict[str, str | int]] = { + "platform": platform, + "size": len(data), + "header": data[:32] if len(data) >= 32 else data, + } + + if platform == "esp32": + # ESP32 binary format analysis + print(f"ESP32 binary size: {len(data)} bytes") + if len(data) >= 32: + # ESP32 image header + magic = data[0] + segments = data[1] + flash_mode = data[2] + flash_size_freq = data[3] + analysis["esp32"] = { + "magic": hex(magic), + "segments": segments, + "flash_mode": flash_mode, + "flash_size_freq": flash_size_freq, + } + print(f"ESP32 image - Magic: {hex(magic)}, Segments: {segments}") + + elif platform == "avr": + # AVR/Arduino binary format (Intel HEX) + print(f"AVR binary size: {len(data)} bytes") + if bin_file.suffix.lower() == ".hex": + print("Intel HEX format detected") + analysis["format"] = "intel_hex" + else: + analysis["format"] = "binary" + + # Print hex dump of first 64 bytes for analysis + print(f"\nFirst 64 bytes of {platform} binary (hex):") + for i in range(0, min(64, len(data)), 16): + hex_bytes = " ".join(f"{b:02x}" for b in data[i : i + 16]) + ascii_chars = "".join( + chr(b) if 32 <= b < 127 else "." for b in data[i : i + 16] + ) + print(f"{i:08x}: {hex_bytes:<48} {ascii_chars}") + + return analysis + + +def _generate_platform_linker_script(map_file: Path, platform: str) -> Path: + """ + Generate a platform-specific linker script. + + Args: + map_file (Path): Path to the map file. + platform (str): Target platform. + + Returns: + Path: Path to the generated linker script. + """ + print(f"Generating {platform} linker script...") + + if platform == "esp32": + # ESP32 memory layout + linker_script_content = """ + MEMORY + { + iram0_0_seg : org = 0x40080000, len = 0x20000 + dram0_0_seg : org = 0x3FFB0000, len = 0x50000 + flash_seg : org = 0x400D0020, len = 0x330000 + } + + SECTIONS + { + .iram0.text : { + *(.iram0.literal .iram0.text) + } > iram0_0_seg + + .flash.text : { + *(.literal .text .literal.* .text.*) + } > flash_seg + + .flash.rodata : { + *(.rodata .rodata.*) + } > flash_seg + + .dram0.data : { + *(.data .data.*) + } > dram0_0_seg + + .dram0.bss : { + *(.bss .bss.*) + } > dram0_0_seg + } + """ + elif platform == "avr": + # AVR memory layout + linker_script_content = """ + MEMORY + { + text (rx) : ORIGIN = 0x0000, LENGTH = 0x8000 + data (rw!x) : ORIGIN = 0x800100, LENGTH = 0x800 + eeprom (rw!x) : ORIGIN = 0x810000, LENGTH = 0x400 + } + + SECTIONS + { + .text : { + *(.text) + *(.text.*) + } > text + + .data : { + *(.data) + *(.data.*) + } > data + + .bss : { + *(.bss) + *(.bss.*) + } > data + } + """ + else: + # Generic linker script + linker_script_content = """ + SECTIONS + { + .text 0x00000000 : { + *(.text) + *(.text.*) + } + .data : { + *(.data) + *(.data.*) + } + .bss : { + *(.bss) + *(.bss.*) + } + } + """ + + linker_script_path = map_file.with_suffix(f".{platform}.ld") + linker_script_path.write_text(linker_script_content) + print(f"Generated {platform} linker script at: {linker_script_path}") + return linker_script_path + + +def _create_platform_object_file(as_path: Path, dummy_obj_path: Path, platform: str): + """ + Create a platform-specific dummy object file. + + Args: + as_path (Path): Path to the assembler executable. + dummy_obj_path (Path): Path to the dummy object file to be created. + platform (str): Target platform. + """ + print(f"Creating {platform} dummy object file...") + + if platform == "esp32": + # ESP32/Xtensa assembly + assembly_code = """ + .section .text + .global _start + .type _start, @function + _start: + nop + j _start + """ + elif platform == "avr": + # AVR assembly + assembly_code = """ + .section .text + .global _start + _start: + nop + rjmp _start + """ + else: + # Generic assembly + assembly_code = """ + .section .text + .global _start + _start: + nop + """ + + asm_file = dummy_obj_path.with_suffix(".s") + asm_file.write_text(assembly_code) + + command = [str(as_path), "-o", str(dummy_obj_path), str(asm_file)] + print(f"Creating {platform} dummy object file: {dummy_obj_path}") + _run_command(command, show_output=True) + + # Clean up assembly file + if asm_file.exists(): + asm_file.unlink() + + +def _create_dummy_elf( + ld_path: Path, linker_script: Path, dummy_obj: Path, output_elf: Path, platform: str +): + """ + Create a dummy ELF file using the specified linker script and dummy object file. + + Args: + ld_path (Path): Path to the ld executable. + linker_script (Path): Path to the linker script. + dummy_obj (Path): Path to the dummy object file. + output_elf (Path): Path to the output ELF file. + platform (str): Target platform. + """ + print(f"Creating {platform} dummy ELF file...") + + command = [ + str(ld_path), + str(dummy_obj), + "-T", + str(linker_script), + "-o", + str(output_elf), + ] + + print(f"Creating {platform} dummy ELF file: {output_elf}") + _run_command(command, show_output=True) + + +def _update_elf_sections( + objcopy_path: Path, bin_file: Path, elf_file: Path, platform: str +): + """ + Update the ELF file sections with binary data (platform-specific). + + Args: + objcopy_path (Path): Path to the objcopy executable. + bin_file (Path): Path to the binary file. + elf_file (Path): Path to the ELF file. + platform (str): Target platform. + """ + print(f"Updating {platform} ELF sections...") + + # Platform-specific section names + section_mapping = {"esp32": ".flash.text", "avr": ".text", "arm": ".text"} + + section_name = section_mapping.get(platform, ".text") + + try: + command = [ + str(objcopy_path), + "--update-section", + f"{section_name}={bin_file}", + str(elf_file), + ] + print( + f"Updating {platform} ELF file '{elf_file}' section '{section_name}' with binary file '{bin_file}'" + ) + _run_command(command, show_output=True) + print(f"Successfully updated {section_name} section") + except RuntimeError as e: + print(f"Warning: Could not update {section_name} section: {e}") + # Try alternative section names + alternative_sections = [".text", ".data", ".rodata"] + for alt_section in alternative_sections: + if alt_section != section_name: + try: + command = [ + str(objcopy_path), + "--update-section", + f"{alt_section}={bin_file}", + str(elf_file), + ] + print(f"Trying alternative section: {alt_section}") + _run_command(command, show_output=True) + print(f"Successfully updated {alt_section} section") + break + except RuntimeError: + continue + + +def bin_to_elf( + bin_file: Path, + map_file: Path, + as_path: Path, + ld_path: Path, + objcopy_path: Path, + output_elf: Path, +): + """ + Convert a binary file to ELF format with platform detection and analysis. + + Args: + bin_file (Path): Path to the input binary file. + map_file (Path): Path to the map file. + as_path (Path): Path to the assembler executable. + ld_path (Path): Path to the linker executable. + objcopy_path (Path): Path to the objcopy executable. + output_elf (Path): Path to the output ELF file. + + Returns: + Path: Path to the generated ELF file. + """ + print("=" * 80) + print("IMPROVED BINARY TO ELF CONVERTER") + print("=" * 80) + + # Detect platform from toolchain + platform = _detect_platform_from_paths(as_path, ld_path) + print(f"Detected platform: {platform}") + + # Analyze binary structure + binary_analysis = _analyze_binary_structure(bin_file, platform) + + # Generate platform-specific linker script + linker_script = _generate_platform_linker_script(map_file, platform) + + # Create platform-specific dummy object file + dummy_obj_path = bin_file.with_name(f"dummy_{platform}.o") + _create_platform_object_file(as_path, dummy_obj_path, platform) + + # Create a dummy ELF file using the generated linker script + _create_dummy_elf(ld_path, linker_script, dummy_obj_path, output_elf, platform) + + # Update the ELF sections with binary data + _update_elf_sections(objcopy_path, bin_file, output_elf, platform) + + # Clean up temporary files + if dummy_obj_path.exists(): + dummy_obj_path.unlink() + print(f"Cleaned up dummy object file: {dummy_obj_path}") + + if linker_script.exists(): + linker_script.unlink() + print(f"Cleaned up linker script: {linker_script}") + + print(f"\n✅ Successfully created {platform} ELF file: {output_elf}") + + # Save analysis results + analysis_file = output_elf.with_suffix(".analysis.json") + with open(analysis_file, "w") as f: + json.dump(binary_analysis, f, indent=2, default=str) + print(f"📊 Binary analysis saved to: {analysis_file}") + + return output_elf + + +# For backward compatibility, keep the old function name +def _generate_linker_script(map_file: Path) -> Path: + """Legacy function for backward compatibility.""" + return _generate_platform_linker_script(map_file, "generic") diff --git a/v1.5/lib/FastLED/ci/util/build_info_analyzer.py b/v1.5/lib/FastLED/ci/util/build_info_analyzer.py new file mode 100644 index 0000000..8a96ce5 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/build_info_analyzer.py @@ -0,0 +1,480 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false +""" +Build Info Analyzer - Tool for extracting platform information from build_info.json files. + +This tool provides easy access to platform-specific defines, compiler flags, toolchain paths, +and other build configuration information generated by PlatformIO. + +Usage: + python build_info_analyzer.py --board uno --show-defines + python build_info_analyzer.py --board esp32dev --show-all + python build_info_analyzer.py --list-boards +""" + +import argparse +import json +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +@dataclass +class CompilerInfo: + """Information about compilers and toolchain""" + + cc_path: str = "" + cxx_path: str = "" + cc_flags: List[str] = field(default_factory=lambda: list()) + cxx_flags: List[str] = field(default_factory=lambda: list()) + compiler_type: str = "" + build_type: str = "" + + +@dataclass +class BuildInfo: + """Complete build information for a platform""" + + board_name: str + defines: Dict[str, str] = field(default_factory=lambda: dict()) + compiler_info: CompilerInfo = field(default_factory=CompilerInfo) + aliases: Dict[str, Optional[str]] = field(default_factory=lambda: dict()) + + +class BuildInfoAnalyzer: + """Analyzer for build_info.json files generated by PlatformIO builds.""" + + def __init__(self, build_dir: str): + """ + Initialize the analyzer. + + Args: + build_dir: Directory containing platform build directories + """ + self.build_dir = Path(build_dir) + + def list_available_boards(self) -> List[str]: + """ + List all boards that have build_info.json files. + + Returns: + List of board names that have been built + """ + boards: List[str] = [] + if not self.build_dir.exists(): + return boards + + for item in self.build_dir.iterdir(): + if item.is_dir() and (item / "build_info.json").exists(): + boards.append(item.name) + + return sorted(boards) + + def get_build_info_path(self, board_name: str) -> Optional[Path]: + """ + Get the path to build_info.json for a specific board. + + Args: + board_name: Name of the board (e.g., 'uno', 'esp32dev') + + Returns: + Path to build_info.json file or None if not found + """ + build_info_path = self.build_dir / board_name / "build_info.json" + if build_info_path.exists(): + return build_info_path + return None + + def load_build_info(self, board_name: str) -> Optional[Dict[str, Any]]: + """ + Load and parse build_info.json for a board. + + Args: + board_name: Name of the board + + Returns: + Parsed JSON data or None if file not found + """ + build_info_path = self.get_build_info_path(board_name) + if not build_info_path: + return None + + try: + with open(build_info_path, "r") as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + print(f"Error loading build_info.json for {board_name}: {e}") + return None + + def create_board_key_from_build_info( + self, data: Dict[str, Any], board_name: str + ) -> Optional[str]: + """ + Get the actual board key used in build_info.json. + + Sometimes the board name differs from the directory name. + + Args: + data: Parsed build_info.json data + board_name: Directory name of the board + + Returns: + Actual board key used in JSON or None if not found + """ + # Try exact match first + if board_name in data: + return board_name + + # If only one key, use it + keys = list(data.keys()) + if len(keys) == 1: + return keys[0] + + # Try to find a close match + for key in keys: + if board_name.lower() in key.lower() or key.lower() in board_name.lower(): + return key + + return None + + def get_platform_defines(self, board_name: str) -> Tuple[bool, List[str], str]: + """ + Get platform-specific preprocessor defines for a board. + + Args: + board_name: Name of the board + + Returns: + Tuple of (success, defines_list, error_message) + """ + data = self.load_build_info(board_name) + if not data: + return False, [], f"Build info not found for {board_name}" + + board_key = self.create_board_key_from_build_info(data, board_name) + if not board_key: + available_keys = list(data.keys()) + return False, [], f"Board key not found. Available keys: {available_keys}" + + board_data = data[board_key] + defines = board_data.get("defines", []) + + return True, defines, "" + + def get_compiler_info(self, board_name: str) -> Tuple[bool, CompilerInfo, str]: + """ + Get compiler information for a board. + + Args: + board_name: Name of the board + + Returns: + Tuple of (success, compiler_info, error_message) + """ + data = self.load_build_info(board_name) + if not data: + return False, CompilerInfo(), f"Build info not found for {board_name}" + + board_key = self.create_board_key_from_build_info(data, board_name) + if not board_key: + return False, CompilerInfo(), "Board key not found in build_info.json" + + board_data = data[board_key] + + compiler_info = CompilerInfo( + cc_path=board_data.get("cc_path", ""), + cxx_path=board_data.get("cxx_path", ""), + cc_flags=board_data.get("cc_flags", []), + cxx_flags=board_data.get("cxx_flags", []), + compiler_type=board_data.get("compiler_type", ""), + build_type=board_data.get("build_type", ""), + ) + + return True, compiler_info, "" + + def get_toolchain_aliases( + self, board_name: str + ) -> Tuple[bool, Dict[str, str], str]: + """ + Get toolchain tool aliases for a board. + + Args: + board_name: Name of the board + + Returns: + Tuple of (success, aliases_dict, error_message) + """ + data = self.load_build_info(board_name) + if not data: + return False, {}, f"Build info not found for {board_name}" + + board_key = self.create_board_key_from_build_info(data, board_name) + if not board_key: + return False, {}, "Board key not found in build_info.json" + + board_data = data[board_key] + aliases = board_data.get("aliases", {}) + + return True, aliases, "" + + def get_all_info(self, board_name: str) -> Tuple[bool, Dict[str, Any], str]: + """ + Get all available information for a board. + + Args: + board_name: Name of the board + + Returns: + Tuple of (success, all_info_dict, error_message) + """ + data = self.load_build_info(board_name) + if not data: + return False, {}, f"Build info not found for {board_name}" + + board_key = self.create_board_key_from_build_info(data, board_name) + if not board_key: + return False, {}, "Board key not found in build_info.json" + + return True, data[board_key], "" + + def compare_defines( + self, board1: str, board2: str + ) -> Tuple[bool, Dict[str, Any], str]: + """ + Compare platform defines between two boards. + + Args: + board1: First board name + board2: Second board name + + Returns: + Tuple of (success, comparison_dict, error_message) + """ + success1, defines1, err1 = self.get_platform_defines(board1) + success2, defines2, err2 = self.get_platform_defines(board2) + + if not success1: + return False, {}, f"Error getting defines for {board1}: {err1}" + if not success2: + return False, {}, f"Error getting defines for {board2}: {err2}" + + set1 = set(defines1) + set2 = set(defines2) + + comparison = { + "board1": board1, + "board2": board2, + "board1_only": sorted(list(set1 - set2)), + "board2_only": sorted(list(set2 - set1)), + "common": sorted(list(set1 & set2)), + "board1_total": len(defines1), + "board2_total": len(defines2), + "common_count": len(set1 & set2), + } + + return True, comparison, "" + + +def print_defines(defines: List[str], board_name: str): + """Print platform defines in a formatted way.""" + print(f"\n📋 Platform Defines for {board_name.upper()}:") + print("=" * 50) + for define in defines: + print(f" {define}") + print(f"\nTotal: {len(defines)} defines") + + +def print_compiler_info(compiler_info: CompilerInfo, board_name: str): + """Print compiler information in a formatted way.""" + print(f"\n🔧 Compiler Information for {board_name.upper()}:") + print("=" * 50) + print(f"Compiler Type: {compiler_info.compiler_type or 'Unknown'}") + print(f"Build Type: {compiler_info.build_type or 'Unknown'}") + print(f"C Compiler: {compiler_info.cc_path or 'Unknown'}") + print(f"C++ Compiler: {compiler_info.cxx_path or 'Unknown'}") + + if compiler_info.cc_flags: + print(f"\nC Flags ({len(compiler_info.cc_flags)}):") + for flag in compiler_info.cc_flags: + print(f" {flag}") + + if compiler_info.cxx_flags: + print(f"\nC++ Flags ({len(compiler_info.cxx_flags)}):") + for flag in compiler_info.cxx_flags: + print(f" {flag}") + + +def print_toolchain_aliases(aliases: Dict[str, str], board_name: str): + """Print toolchain aliases in a formatted way.""" + print(f"\n⚙️ Toolchain Aliases for {board_name.upper()}:") + print("=" * 50) + for tool, path in aliases.items(): + if path: + # Show just the tool name from the path for readability + tool_name = Path(path).name if path else "Not available" + print(f" {tool:10}: {tool_name}") + else: + print(f" {tool:10}: Not available") + + +def print_comparison(comparison: Dict[str, Any]): + """Print a comparison between two boards.""" + board1 = comparison["board1"] + board2 = comparison["board2"] + + print("\n🔍 Platform Defines Comparison:") + print("=" * 60) + print(f"📊 {board1.upper()} vs {board2.upper()}") + print(f" {board1}: {comparison['board1_total']} defines") + print(f" {board2}: {comparison['board2_total']} defines") + print(f" Common: {comparison['common_count']} defines") + + if comparison["board1_only"]: + print(f"\n🔴 Only in {board1.upper()} ({len(comparison['board1_only'])}):") + for define in comparison["board1_only"]: + print(f" {define}") + + if comparison["board2_only"]: + print(f"\n🔵 Only in {board2.upper()} ({len(comparison['board2_only'])}):") + for define in comparison["board2_only"]: + print(f" {define}") + + if comparison["common"]: + print(f"\n🟢 Common Defines ({len(comparison['common'])}):") + for define in comparison["common"]: + print(f" {define}") + + +def main(): + """Main function for command line usage.""" + parser = argparse.ArgumentParser( + description="Analyze build_info.json files to extract platform information", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s --list-boards # List all available boards + %(prog)s --board uno --show-defines # Show UNO platform defines + %(prog)s --board esp32dev --show-compiler # Show ESP32 compiler info + %(prog)s --board teensy31 --show-all # Show all info for Teensy 3.1 + %(prog)s --compare uno esp32dev # Compare defines between boards + """, + ) + + parser.add_argument( + "--build-dir", + default=".build", + help="Build directory containing platform subdirectories (default: .build)", + ) + + parser.add_argument( + "--list-boards", + action="store_true", + help="List all boards with build_info.json files", + ) + + parser.add_argument( + "--board", help="Board name to analyze (e.g., uno, esp32dev, teensy31)" + ) + + parser.add_argument( + "--show-defines", action="store_true", help="Show platform preprocessor defines" + ) + + parser.add_argument( + "--show-compiler", action="store_true", help="Show compiler information" + ) + + parser.add_argument( + "--show-toolchain", action="store_true", help="Show toolchain aliases" + ) + + parser.add_argument( + "--show-all", action="store_true", help="Show all available information" + ) + + parser.add_argument( + "--compare", + nargs=2, + metavar=("BOARD1", "BOARD2"), + help="Compare platform defines between two boards", + ) + + parser.add_argument( + "--json", action="store_true", help="Output results in JSON format" + ) + + args = parser.parse_args() + + analyzer = BuildInfoAnalyzer(args.build_dir) + + if args.list_boards: + boards = analyzer.list_available_boards() + if not boards: + print("❌ No boards with build_info.json found in build directory") + print(f" Directory: {analyzer.build_dir}") + print( + " Try running a compilation first: uv run python -m ci.ci-compile uno --examples Blink" + ) + return 1 + + print(f"📋 Available boards ({len(boards)}):") + for board in boards: + print(f" ✅ {board}") + return 0 + + if args.compare: + board1, board2 = args.compare + success, comparison, error = analyzer.compare_defines(board1, board2) + if not success: + print(f"❌ Error: {error}") + return 1 + + if args.json: + print(json.dumps(comparison, indent=2)) + else: + print_comparison(comparison) + return 0 + + if not args.board: + print("❌ Error: --board is required (or use --list-boards or --compare)") + return 1 + + # Handle single board analysis + if args.show_defines or args.show_all: + success, defines, error = analyzer.get_platform_defines(args.board) + if not success: + print(f"❌ Error getting defines: {error}") + return 1 + + if args.json: + print(json.dumps({"defines": defines}, indent=2)) + else: + print_defines(defines, args.board) + + if args.show_compiler or args.show_all: + success, compiler_info, error = analyzer.get_compiler_info(args.board) + if not success: + print(f"❌ Error getting compiler info: {error}") + return 1 + + if args.json: + print(json.dumps({"compiler": asdict(compiler_info)}, indent=2)) + else: + print_compiler_info(compiler_info, args.board) + + if args.show_toolchain or args.show_all: + success, aliases, error = analyzer.get_toolchain_aliases(args.board) + if not success: + print(f"❌ Error getting toolchain aliases: {error}") + return 1 + + if args.json: + print(json.dumps({"toolchain": aliases}, indent=2)) + else: + print_toolchain_aliases(aliases, args.board) + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/v1.5/lib/FastLED/ci/util/cached_compiler.py b/v1.5/lib/FastLED/ci/util/cached_compiler.py new file mode 100644 index 0000000..6fd98ff --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/cached_compiler.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +""" +Python-based cached compiler system for sccache integration. + +This module provides Python scripts that act as cached compilers, wrapping the real +toolchain with cache support. Unlike batch scripts, Python wrappers can: +- Properly resolve toolchain paths from PlatformIO platform packages +- Handle response files and complex argument parsing +- Provide better error handling and debugging +- Work consistently across platforms (Windows/Unix) +""" + +import os +import platform +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Dict, List, Optional + + +def find_toolchain_compiler( + compiler_name: str, platform_packages_paths: List[str] +) -> Optional[str]: + """ + Find the real compiler binary in PlatformIO platform packages. + + Args: + compiler_name: Name of compiler (e.g., 'arm-none-eabi-gcc', 'gcc', 'clang') + platform_packages_paths: List of paths to search for toolchain + + Returns: + Absolute path to compiler binary if found, None otherwise + """ + # First check if compiler is already an absolute path + if Path(compiler_name).is_absolute() and Path(compiler_name).exists(): + return str(Path(compiler_name).resolve()) + + # Check if compiler is in PATH + path_compiler = shutil.which(compiler_name) + if path_compiler: + return str(Path(path_compiler).resolve()) + + # Search in platform packages directories + for package_path in platform_packages_paths: + package_dir = Path(package_path) + if not package_dir.exists(): + continue + + # Search for compiler in common toolchain subdirectories + search_patterns = [ + f"bin/{compiler_name}", + f"bin/{compiler_name}.exe", + f"**/bin/{compiler_name}", + f"**/bin/{compiler_name}.exe", + ] + + for pattern in search_patterns: + compiler_candidates = list(package_dir.glob(pattern)) + for candidate in compiler_candidates: + if candidate.is_file(): + return str(candidate.resolve()) + + return None + + +def create_cached_compiler_script( + compiler_name: str, + cache_executable: str, + real_compiler_path: str, + output_dir: Path, + debug: bool = False, +) -> Path: + """ + Create a Python script that acts as a cached compiler. + + Args: + compiler_name: Name for the cached compiler (e.g., 'gcc', 'g++') + cache_executable: Path to cache tool (sccache, ccache, etc.) + real_compiler_path: Path to the real compiler to wrap + output_dir: Directory to create the cached compiler script + debug: Enable debug output + + Returns: + Path to the created cached compiler script + """ + script_base = f"cached_{compiler_name.replace('-', '_')}" + script_py = output_dir / f"{script_base}.py" + + script_content = f'''#!/usr/bin/env python3 +""" +Cached compiler wrapper for {compiler_name} +Generated automatically for sccache integration. + +This script acts as a cached compiler that wraps the real {compiler_name} +with cache support. It resolves the real compiler path and forwards +all arguments to the cache tool. +""" + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +# Configuration (set at generation time) +CACHE_EXECUTABLE = r"{cache_executable}" +REAL_COMPILER_PATH = r"{real_compiler_path}" +DEBUG = {debug} + +def debug_print(msg: str) -> None: + """Print debug message if debug mode is enabled.""" + if DEBUG: + print(f"CACHED_COMPILER[{compiler_name}]: {{msg}}", file=sys.stderr) + +def main() -> int: + """Main entry point for cached compiler.""" + try: + # Verify real compiler exists + if not Path(REAL_COMPILER_PATH).exists(): + print(f"ERROR: Real compiler not found: {{REAL_COMPILER_PATH}}", file=sys.stderr) + return 1 + + # Verify cache executable exists + cache_path = CACHE_EXECUTABLE + if not Path(cache_path).exists() and not shutil.which(cache_path.split()[0]): + print(f"ERROR: Cache executable not found: {{cache_path}}", file=sys.stderr) + return 1 + + # Build command: cache_tool real_compiler args... + if " " in cache_path: + # Handle "python xcache.py" style commands + cache_parts = cache_path.split() + command = cache_parts + [REAL_COMPILER_PATH] + sys.argv[1:] + else: + # Handle simple "sccache" style commands + command = [cache_path, REAL_COMPILER_PATH] + sys.argv[1:] + + debug_print(f"Executing: {{' '.join(command)}}") + + # Execute the cache tool with real compiler and arguments + result = subprocess.run(command, cwd=os.getcwd()) + return result.returncode + except KeyboardInterrupt: + print(f"Keyboard interrupt in cached compiler {{compiler_name}}", file=sys.stderr) + return 1 + except Exception as e: + print(f"ERROR in cached compiler {{compiler_name}}: {{e}}", file=sys.stderr) + return 1 + +if __name__ == "__main__": + sys.exit(main()) +''' + + # Write the script + script_py.write_text(script_content, encoding="utf-8") + + # Detect Windows reliably, including MSYS/Cygwin Git-Bash environments where + # os.name may report 'posix'. In those cases we still need a .cmd shim so + # that response files ("@file.tmp") are not misinterpreted by 'python'. + is_windows = ( + os.name == "nt" + or sys.platform.startswith("win") + or sys.platform.startswith("cygwin") + or sys.platform.startswith("msys") + or platform.system().lower().startswith(("windows", "msys", "cygwin")) + ) + + # Make executable script on Unix-like systems + if not is_windows: + script_py.chmod(script_py.stat().st_mode | 0o755) + return script_py + + # On Windows, create a .cmd shim so our wrapper is the program token + script_cmd = output_dir / f"{script_base}.cmd" + # Use system python; PlatformIO environment provides the right interpreter + cmd_content = f'@echo off\r\npython "{script_py}" %*\r\nexit /b %ERRORLEVEL%\r\n' + script_cmd.write_text(cmd_content, encoding="utf-8") + return script_cmd + + +def create_cached_toolchain( + toolchain_info: Dict[str, str], + cache_config: Dict[str, str], + platform_packages_paths: List[str], + output_dir: Path, + debug: bool = False, +) -> Dict[str, str]: + """ + Create a complete set of cached compiler scripts for a toolchain. + + Args: + toolchain_info: Mapping of tool names to real paths/names + cache_config: Cache configuration (CACHE_EXECUTABLE, etc.) + platform_packages_paths: Paths to search for real compilers + output_dir: Directory to create cached scripts + debug: Enable debug output + + Returns: + Mapping of tool names to cached script paths + """ + output_dir.mkdir(parents=True, exist_ok=True) + + cache_executable = cache_config.get("CACHE_EXECUTABLE", "sccache") + cached_tools: Dict[str, str] = {} + + # Standard compiler tools that should be wrapped with cache + cacheable_tools = {"CC", "CXX", "gcc", "g++", "clang", "clang++"} + + for tool_name, tool_path_or_name in toolchain_info.items(): + # Only wrap cacheable tools + tool_base = Path(tool_path_or_name).name if tool_path_or_name else tool_name + if not any(cacheable in tool_base for cacheable in cacheable_tools): + if debug: + print(f"Skipping non-cacheable tool: {tool_name} = {tool_path_or_name}") + continue + + # Find the real compiler + real_compiler_path = find_toolchain_compiler( + tool_path_or_name, platform_packages_paths + ) + if not real_compiler_path: + if debug: + print( + f"WARNING: Could not find real compiler for {tool_name} = {tool_path_or_name}" + ) + continue + + # Create cached compiler script + cached_script = create_cached_compiler_script( + compiler_name=tool_name, + cache_executable=cache_executable, + real_compiler_path=real_compiler_path, + output_dir=output_dir, + debug=debug, + ) + + # Use returned script path directly (Windows: .cmd shim; Unix: executable .py) + cached_tools[tool_name] = str(cached_script) + + if debug: + print( + f"Created cached {tool_name}: {cached_tools[tool_name]} -> {real_compiler_path}" + ) + + return cached_tools + + +def get_platform_packages_paths() -> List[str]: + """ + Get list of platform package paths from PlatformIO. + + Returns: + List of paths where platform packages are installed + """ + paths: List[str] = [] + + # Try to get PlatformIO home directory + try: + result = subprocess.run( + ["pio", "system", "info", "--json"], + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode == 0: + import json + + info = json.loads(result.stdout) + platformio_home = info.get("platformio_home_dir") + if platformio_home: + packages_dir = Path(platformio_home) / "packages" + if packages_dir.exists(): + # Add all package directories + for package_dir in packages_dir.iterdir(): + if package_dir.is_dir(): + paths.append(str(package_dir)) + except KeyboardInterrupt: + print(f"Keyboard interrupt in get_platform_packages_paths") + import sys + + sys.exit(1) + except Exception: + pass + + # Fallback: common PlatformIO package locations + common_locations = [ + Path.home() / ".platformio" / "packages", + Path.home() / ".fastled" / "packages", + Path("C:/Users") / os.environ.get("USERNAME", "") / ".platformio" / "packages", + Path("C:/Users") / os.environ.get("USERNAME", "") / ".fastled" / "packages", + ] + + for location in common_locations: + if location.exists(): + for package_dir in location.iterdir(): + if package_dir.is_dir(): + paths.append(str(package_dir)) + + return paths + + +if __name__ == "__main__": + # Test the cached compiler system + print("Testing cached compiler system...") + + # Get platform packages + packages = get_platform_packages_paths() + print(f"Found {len(packages)} platform packages") + + # Test finding a common compiler + test_compiler = find_toolchain_compiler("gcc", packages) + print(f"Found gcc at: {test_compiler}") + + # Create test cached compiler + test_dir = Path(tempfile.mkdtemp()) + try: + cached_tools = create_cached_toolchain( + toolchain_info={"CC": "gcc", "CXX": "g++"}, + cache_config={"CACHE_EXECUTABLE": "sccache"}, + platform_packages_paths=packages, + output_dir=test_dir, + debug=True, + ) + print(f"Created cached tools: {cached_tools}") + finally: + shutil.rmtree(test_dir, ignore_errors=True) diff --git a/v1.5/lib/FastLED/ci/util/ccache_config.py b/v1.5/lib/FastLED/ci/util/ccache_config.py new file mode 100644 index 0000000..0effc99 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/ccache_config.py @@ -0,0 +1,124 @@ +"""Configure CCACHE for PlatformIO builds.""" + +import os +import platform +import subprocess +from pathlib import Path +from typing import Any, Protocol + + +# ruff: noqa: F821 +# pyright: reportUndefinedVariable=false +Import("env") # type: ignore # Import is provided by PlatformIO + + +class PlatformIOEnv(Protocol): + """Type information for PlatformIO environment.""" + + def get(self, key: str, default: str | None = None) -> str | None: + """Get a value from the environment.""" + ... + + def Replace(self, **kwargs: Any) -> None: + """Replace environment variables.""" + ... + + +def is_ccache_available() -> bool: + """Check if ccache is available in the system.""" + try: + subprocess.run(["ccache", "--version"], capture_output=True, check=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + + +def get_ccache_path() -> str | None: + """Get the full path to ccache executable.""" + if platform.system() == "Windows": + # On Windows, look in chocolatey's bin directory + ccache_paths = [ + "C:\\ProgramData\\chocolatey\\bin\\ccache.exe", + os.path.expanduser("~\\scoop\\shims\\ccache.exe"), + ] + for path in ccache_paths: + if os.path.exists(path): + return path + else: + # On Unix-like systems, use which to find ccache + try: + return subprocess.check_output(["which", "ccache"]).decode().strip() + except subprocess.CalledProcessError: + pass + return None + + +def configure_ccache(env: PlatformIOEnv) -> None: # type: ignore # env is provided by PlatformIO + """Configure CCACHE for the build environment.""" + if not is_ccache_available(): + print("CCACHE is not available. Skipping CCACHE configuration.") + return + + ccache_path = get_ccache_path() + if not ccache_path: + print("Could not find CCACHE executable. Skipping CCACHE configuration.") + return + + print(f"Found CCACHE at: {ccache_path}") + + # Set up CCACHE environment variables if not already set + if "CCACHE_DIR" not in os.environ: + project_dir = env.get("PROJECT_DIR") + if project_dir is None: + project_dir = os.getcwd() + + # Use board-specific ccache directory if PIOENV (board environment) is available + board_name = env.get("PIOENV") + if board_name: + ccache_dir = os.path.join(project_dir, ".ccache", board_name) + else: + ccache_dir = os.path.join(project_dir, ".ccache", "default") + + os.environ["CCACHE_DIR"] = ccache_dir + Path(ccache_dir).mkdir(parents=True, exist_ok=True) + print(f"Using board-specific CCACHE directory: {ccache_dir}") + + # Configure CCACHE for this build + project_dir = env.get("PROJECT_DIR") + if project_dir is None: + project_dir = os.getcwd() + os.environ["CCACHE_BASEDIR"] = project_dir + os.environ["CCACHE_COMPRESS"] = "true" + os.environ["CCACHE_COMPRESSLEVEL"] = "6" + os.environ["CCACHE_MAXSIZE"] = "400M" + + # Wrap compiler commands with ccache + # STRICT: CC and CXX must be explicitly set - NO fallbacks allowed + original_cc = env.get("CC") + if not original_cc: + raise RuntimeError( + "CRITICAL: CC environment variable is required but not set. " + "Please set CC to the C compiler path (e.g., gcc, clang)." + ) + original_cxx = env.get("CXX") + if not original_cxx: + raise RuntimeError( + "CRITICAL: CXX environment variable is required but not set. " + "Please set CXX to the C++ compiler path (e.g., g++, clang++)." + ) + + # Don't wrap if already wrapped + if original_cc is not None and "ccache" not in original_cc: + env.Replace( + CC=f"{ccache_path} {original_cc}", + CXX=f"{ccache_path} {original_cxx}", + ) + print(f"Wrapped CC: {env.get('CC')}") + print(f"Wrapped CXX: {env.get('CXX')}") + + # Show CCACHE stats + subprocess.run([ccache_path, "--show-stats"], check=False) + + +# ruff: noqa: F821 +configure_ccache(env) # type: ignore # env is provided by PlatformIO diff --git a/v1.5/lib/FastLED/ci/util/check_files.py b/v1.5/lib/FastLED/ci/util/check_files.py new file mode 100644 index 0000000..fa5963c --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/check_files.py @@ -0,0 +1,217 @@ +# pyright: reportUnknownMemberType=false +import os +from abc import ABC, abstractmethod +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from typing import Dict, List, Optional + +from ci.util.paths import PROJECT_ROOT + + +SRC_ROOT = PROJECT_ROOT / "src" + +NUM_WORKERS = 1 if os.environ.get("NO_PARALLEL") else (os.cpu_count() or 1) * 4 + +EXCLUDED_FILES = [ + "stub_main.cpp", +] + + +@dataclass +class FileContent: + """Container for file content and metadata.""" + + path: str + content: str + lines: List[str] + + def __post_init__(self): + if not self.lines: + self.lines = self.content.splitlines() + + +class FileContentChecker(ABC): + """Abstract base class for checking file content.""" + + @abstractmethod + def should_process_file(self, file_path: str) -> bool: + """Predicate to determine if a file should be processed. + + Args: + file_path: Path to the file to check + + Returns: + True if the file should be processed, False otherwise + """ + pass + + @abstractmethod + def check_file_content(self, file_content: FileContent) -> List[str]: + """Check the file content and return any issues found. + + Args: + file_content: FileContent object containing path, content, and lines + + Returns: + List of error messages, empty if no issues found + """ + pass + + +class MultiCheckerFileProcessor: + """Processor that can run multiple checkers on files.""" + + def __init__(self): + pass + + def process_files_with_checkers( + self, file_paths: List[str], checkers: List[FileContentChecker] + ) -> Dict[str, List[str]]: + """Process files with multiple checkers. + + Args: + file_paths: List of file paths to process + checkers: List of checker instances to run on the files + + Returns: + Dictionary mapping checker class name to list of issues found + """ + # Initialize results dictionary for each checker + results: Dict[str, List[str]] = {} + for checker in checkers: + checker_name = checker.__class__.__name__ + results[checker_name] = [] + + # Process each file + for file_path in file_paths: + # Check if any checker wants to process this file + interested_checkers = [ + checker + for checker in checkers + if checker.should_process_file(file_path) + ] + + # If any checker is interested, read the file once + if interested_checkers: + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + # Create FileContent object with lines split + file_content = FileContent( + path=file_path, content=content, lines=content.splitlines() + ) + + # Pass the file content to all interested checkers + for checker in interested_checkers: + checker_name = checker.__class__.__name__ + issues = checker.check_file_content(file_content) + results[checker_name].extend(issues) + + except Exception as e: + # Add error to all interested checkers + error_msg = f"Error reading file {file_path}: {str(e)}" + for checker in interested_checkers: + checker_name = checker.__class__.__name__ + results[checker_name].append(error_msg) + + return results + + +# Legacy compatibility classes +class FileProcessorCallback(FileContentChecker): + """Legacy compatibility wrapper - delegates to FileContentChecker methods.""" + + def check_file_content_legacy(self, file_path: str, content: str) -> List[str]: + """Legacy method signature for backward compatibility.""" + file_content = FileContent(path=file_path, content=content, lines=[]) + return self.check_file_content(file_content) + + +class GenericFileSearcher: + """Generic file searcher that processes files using a callback pattern.""" + + def __init__(self, max_workers: Optional[int] = None): + self.max_workers = max_workers or NUM_WORKERS + + def search_directory( + self, start_dir: str, callback: FileProcessorCallback + ) -> List[str]: + """Search a directory and process files using the provided callback. + + Args: + start_dir: Directory to start searching from + callback: Callback class to handle file processing + + Returns: + List of all issues found across all files + """ + files_to_check: List[str] = [] + + # Collect all files that should be processed + for root, _, files in os.walk(start_dir): + for file in files: + file_path = os.path.join(root, file) + if callback.should_process_file(file_path): + files_to_check.append(file_path) + + # Process files in parallel + all_issues: List[str] = [] + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + futures = [ + executor.submit(self._process_single_file, file_path, callback) + for file_path in files_to_check + ] + for future in futures: + all_issues.extend(future.result()) + + return all_issues + + def _process_single_file( + self, file_path: str, callback: FileProcessorCallback + ) -> List[str]: + """Process a single file using the callback. + + Args: + file_path: Path to the file to process + callback: Callback to use for processing + + Returns: + List of issues found in this file + """ + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + file_content = FileContent(path=file_path, content=content, lines=[]) + return callback.check_file_content(file_content) + except Exception as e: + return [f"Error processing file {file_path}: {str(e)}"] + + +def collect_files_to_check( + test_directories: List[str], extensions: Optional[List[str]] = None +) -> List[str]: + """Collect all files to check from the given directories.""" + if extensions is None: + extensions = [".cpp", ".h", ".hpp"] + + files_to_check: List[str] = [] + + # Search each directory + for directory in test_directories: + if os.path.exists(directory): + for root, _, files in os.walk(directory): + for file in files: + if any(file.endswith(ext) for ext in extensions): + file_path = os.path.join(root, file) + files_to_check.append(file_path) + + # Also check the main src directory files (not subdirectories) + for file in os.listdir(SRC_ROOT): + file_path = os.path.join(SRC_ROOT, file) + if os.path.isfile(file_path) and any( + file_path.endswith(ext) for ext in extensions + ): + files_to_check.append(file_path) + + return files_to_check diff --git a/v1.5/lib/FastLED/ci/util/check_implementation_files.py.disabled b/v1.5/lib/FastLED/ci/util/check_implementation_files.py.disabled new file mode 100644 index 0000000..622c5bc --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/check_implementation_files.py.disabled @@ -0,0 +1,477 @@ +#!/usr/bin/env python3 +""" +Implementation Files Checker +Scans src/fl/ and src/fx/ directories for *.hpp and *.cpp.hpp files. +Provides statistics and can verify inclusion in the all-source build. +""" + +import argparse +import json +import os +from pathlib import Path +from typing import Dict, List, Set + + +# Get project root directory +PROJECT_ROOT = Path(__file__).parent.parent.parent +SRC_ROOT = PROJECT_ROOT / "src" +FL_DIR = SRC_ROOT / "fl" +FX_DIR = SRC_ROOT / "fx" +ALL_SOURCE_BUILD_FILE = SRC_ROOT / "fastled_compile.hpp.cpp" + +# Hierarchical compile files +HIERARCHICAL_FILES = [ + SRC_ROOT / "fl" / "fl_compile.hpp", + SRC_ROOT / "fx" / "fx_compile.hpp", + SRC_ROOT / "sensors" / "sensors_compile.hpp", + SRC_ROOT / "platforms" / "platforms_compile.hpp", + SRC_ROOT / "third_party" / "third_party_compile.hpp", + SRC_ROOT / "src_compile.hpp", +] + +# Detect if running in CI/test environment for ASCII-only output +USE_ASCII_ONLY = ( + os.environ.get("FASTLED_CI_NO_INTERACTIVE") == "true" + or os.environ.get("GITHUB_ACTIONS") == "true" + or os.environ.get("CI") == "true" +) + + +def collect_files_by_type(directory: Path) -> Dict[str, List[Path]]: + """Collect files by type (.hpp vs .cpp.hpp) from a directory. + + Args: + directory: Directory to scan + + Returns: + Dictionary with 'hpp' and 'cpp_hpp' keys containing lists of files + """ + files = {"hpp": [], "cpp_hpp": []} + + if not directory.exists(): + print(f"Warning: Directory {directory} does not exist") + return files + + # Recursively find all .hpp and .cpp.hpp files + for file_path in directory.rglob("*.hpp"): + if file_path.name.endswith(".cpp.hpp"): + files["cpp_hpp"].append(file_path) + else: + files["hpp"].append(file_path) + + return files + + +def get_all_source_build_includes() -> Set[str]: + """Extract the list of #include statements from the all-source build files. + + This function handles the hierarchical structure by checking: + 1. The main all-source build file (fastled_compile.hpp.cpp) + 2. All hierarchical module compile files (fl_compile.hpp, fx_compile.hpp, etc.) + + Returns: + Set of included file paths (relative to src/) + """ + includes = set() + + # Check main all-source build file + if not ALL_SOURCE_BUILD_FILE.exists(): + print(f"Warning: All-source build file {ALL_SOURCE_BUILD_FILE} does not exist") + return includes + + # Function to extract includes from a file + def extract_includes_from_file(file_path: Path) -> Set[str]: + file_includes = set() + if not file_path.exists(): + return file_includes + + try: + with open(file_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + # Look for #include statements + if line.startswith('#include "') and line.endswith('"'): + # Extract the include path + include_path = line[10:-1] # Remove '#include "' and '"' + file_includes.add(include_path) + except Exception as e: + print(f"Error reading file {file_path}: {e}") + + return file_includes + + # Extract includes from main file + includes.update(extract_includes_from_file(ALL_SOURCE_BUILD_FILE)) + + # Extract includes from all hierarchical files + for hierarchical_file in HIERARCHICAL_FILES: + if hierarchical_file.exists(): + hierarchical_includes = extract_includes_from_file(hierarchical_file) + includes.update(hierarchical_includes) + + return includes + + +def check_inclusion_in_all_source_build( + files: Dict[str, List[Path]], base_dir: Path +) -> Dict[str, Dict[str, bool]]: + """Check which implementation files are included in the all-source build. + + Args: + files: Dictionary of files by type + base_dir: Base directory (fl or fx) for relative path calculation + + Returns: + Dictionary mapping file types to dictionaries of file -> included status + """ + all_source_includes = get_all_source_build_includes() + + inclusion_status = {"hpp": {}, "cpp_hpp": {}} + + for file_type, file_list in files.items(): + if file_type == "cpp_hpp": # Only check .cpp.hpp files for inclusion + for file_path in file_list: + # Calculate relative path from src/ + relative_path = file_path.relative_to(SRC_ROOT) + relative_path_str = str(relative_path).replace( + "\\", "/" + ) # Normalize path separators + + # Check if this file is included + is_included = relative_path_str in all_source_includes + inclusion_status[file_type][str(file_path)] = is_included + + return inclusion_status + + +def print_file_list( + files: List[Path], title: str, base_dir: Path, show_relative: bool = True +): + """Print a formatted list of files. + + Args: + files: List of file paths + title: Title for the section + base_dir: Base directory for relative path calculation + show_relative: Whether to show relative paths + """ + print(f"\n{title} ({len(files)} files):") + print("-" * (len(title) + 20)) + + if not files: + print(" (none)") + return + + # Sort files for consistent output + sorted_files = sorted(files, key=lambda p: str(p)) + + for i, file_path in enumerate(sorted_files, 1): + if show_relative: + rel_path = file_path.relative_to(base_dir) + print(f" {i:2d}. {rel_path}") + else: + print(f" {i:2d}. {file_path.name}") + + +def print_inclusion_report( + inclusion_status: Dict[str, Dict[str, bool]], base_dir: Path +): + """Print a report of which implementation files are included in all-source build. + + Args: + inclusion_status: Dictionary of inclusion status by file type + base_dir: Base directory for relative path calculation + """ + cpp_hpp_status = inclusion_status.get("cpp_hpp", {}) + + if not cpp_hpp_status: + print("\nNo .cpp.hpp files found to check for inclusion") + return + + included_files = [path for path, included in cpp_hpp_status.items() if included] + missing_files = [path for path, included in cpp_hpp_status.items() if not included] + + print("\nALL-SOURCE BUILD INCLUSION STATUS:") + print("=" * 50) + + # Use ASCII or Unicode symbols based on environment + check_symbol = "[+]" if USE_ASCII_ONLY else "✅" + cross_symbol = "[-]" if USE_ASCII_ONLY else "❌" + + print(f"{check_symbol} Included in all-source build: {len(included_files)}") + if included_files: + for file_path in sorted(included_files): + rel_path = Path(file_path).relative_to(SRC_ROOT) + print(f" {check_symbol} {rel_path}") + + print(f"\n{cross_symbol} Missing from all-source build: {len(missing_files)}") + if missing_files: + for file_path in sorted(missing_files): + rel_path = Path(file_path).relative_to(SRC_ROOT) + print(f" {cross_symbol} {rel_path}") + + +def generate_summary_report( + fl_files: Dict[str, List[Path]], fx_files: Dict[str, List[Path]] +) -> Dict: + """Generate a summary report with statistics. + + Args: + fl_files: Files found in fl/ directory + fx_files: Files found in fx/ directory + + Returns: + Dictionary containing summary statistics + """ + summary = { + "fl_directory": { + "hpp_files": len(fl_files["hpp"]), + "cpp_hpp_files": len(fl_files["cpp_hpp"]), + "total_files": len(fl_files["hpp"]) + len(fl_files["cpp_hpp"]), + }, + "fx_directory": { + "hpp_files": len(fx_files["hpp"]), + "cpp_hpp_files": len(fx_files["cpp_hpp"]), + "total_files": len(fx_files["hpp"]) + len(fx_files["cpp_hpp"]), + }, + "totals": { + "hpp_files": len(fl_files["hpp"]) + len(fx_files["hpp"]), + "cpp_hpp_files": len(fl_files["cpp_hpp"]) + len(fx_files["cpp_hpp"]), + "total_files": len(fl_files["hpp"]) + + len(fl_files["cpp_hpp"]) + + len(fx_files["hpp"]) + + len(fx_files["cpp_hpp"]), + }, + } + + return summary + + +def print_summary_report(summary: Dict): + """Print a formatted summary report. + + Args: + summary: Summary statistics dictionary + """ + print("\n" + "=" * 80) + print("IMPLEMENTATION FILES SUMMARY REPORT") + print("=" * 80) + + # Use ASCII or Unicode symbols based on environment + folder_symbol = "[DIR]" if USE_ASCII_ONLY else "📁" + chart_symbol = "[STATS]" if USE_ASCII_ONLY else "📊" + ratio_symbol = "[RATIO]" if USE_ASCII_ONLY else "📈" + + print(f"\n{folder_symbol} FL DIRECTORY ({FL_DIR.relative_to(PROJECT_ROOT)}):") + print( + f" Header files (.hpp): {summary['fl_directory']['hpp_files']:3d}" + ) + print( + f" Implementation files (.cpp.hpp): {summary['fl_directory']['cpp_hpp_files']:3d}" + ) + print( + f" Total files: {summary['fl_directory']['total_files']:3d}" + ) + + print(f"\n{folder_symbol} FX DIRECTORY ({FX_DIR.relative_to(PROJECT_ROOT)}):") + print( + f" Header files (.hpp): {summary['fx_directory']['hpp_files']:3d}" + ) + print( + f" Implementation files (.cpp.hpp): {summary['fx_directory']['cpp_hpp_files']:3d}" + ) + print( + f" Total files: {summary['fx_directory']['total_files']:3d}" + ) + + print(f"\n{chart_symbol} TOTALS:") + print(f" Header files (.hpp): {summary['totals']['hpp_files']:3d}") + print( + f" Implementation files (.cpp.hpp): {summary['totals']['cpp_hpp_files']:3d}" + ) + print(f" Total files: {summary['totals']['total_files']:3d}") + + # Calculate ratios + total_hpp = summary["totals"]["hpp_files"] + total_cpp_hpp = summary["totals"]["cpp_hpp_files"] + + if total_hpp > 0: + impl_ratio = (total_cpp_hpp / total_hpp) * 100 + print(f"\n{ratio_symbol} IMPLEMENTATION RATIO:") + print(f" Implementation files per header: {impl_ratio:.1f}%") + print(f" ({total_cpp_hpp} .cpp.hpp files for {total_hpp} .hpp files)") + + +def main(): + """Main function to run the implementation files checker.""" + parser = argparse.ArgumentParser( + description="Check *.hpp and *.cpp.hpp files in fl/ and fx/ directories" + ) + parser.add_argument("--list", action="store_true", help="List all files found") + parser.add_argument( + "--check-inclusion", + action="store_true", + help="Check which .cpp.hpp files are included in all-source build", + ) + parser.add_argument("--json", action="store_true", help="Output summary as JSON") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + parser.add_argument( + "--ascii-only", + action="store_true", + help="Use ASCII-only output (no Unicode emoji)", + ) + parser.add_argument( + "--suppress-summary-on-100-percent", + action="store_true", + help="Suppress summary report when inclusion percentage is 100%", + ) + + args = parser.parse_args() + + # Override USE_ASCII_ONLY if command line flag is set + global USE_ASCII_ONLY + if args.ascii_only: + USE_ASCII_ONLY = True + + # Collect files from both directories + print("Scanning implementation files...") + fl_files = collect_files_by_type(FL_DIR) + fx_files = collect_files_by_type(FX_DIR) + + # Generate summary + summary = generate_summary_report(fl_files, fx_files) + + # Define symbols once for all output modes + search_symbol = "[SEARCH]" if USE_ASCII_ONLY else "🔍" + stats_symbol = "[STATS]" if USE_ASCII_ONLY else "📊" + config_symbol = "[CONFIG]" if USE_ASCII_ONLY else "🔧" + + # Calculate inclusion percentage to determine if we should suppress summary + should_suppress_summary = False + if args.suppress_summary_on_100_percent: + all_cpp_hpp_files = fl_files["cpp_hpp"] + fx_files["cpp_hpp"] + all_source_includes = get_all_source_build_includes() + + included_count = 0 + for file_path in all_cpp_hpp_files: + relative_path = file_path.relative_to(SRC_ROOT) + relative_path_str = str(relative_path).replace("\\", "/") + if relative_path_str in all_source_includes: + included_count += 1 + + total_impl_files = len(all_cpp_hpp_files) + if total_impl_files > 0: + inclusion_percentage = (included_count / total_impl_files) * 100 + should_suppress_summary = inclusion_percentage >= 100.0 + + # Output based on requested format + if args.json: + # Add file lists to summary for JSON output + summary["fl_files"] = { + "hpp": [str(p.relative_to(SRC_ROOT)) for p in fl_files["hpp"]], + "cpp_hpp": [str(p.relative_to(SRC_ROOT)) for p in fl_files["cpp_hpp"]], + } + summary["fx_files"] = { + "hpp": [str(p.relative_to(SRC_ROOT)) for p in fx_files["hpp"]], + "cpp_hpp": [str(p.relative_to(SRC_ROOT)) for p in fx_files["cpp_hpp"]], + } + print(json.dumps(summary, indent=2)) + return + + # Print summary report only if not suppressed + if not should_suppress_summary: + print_summary_report(summary) + else: + print("Summary report suppressed: 100% inclusion coverage achieved") + + # List files if requested + if args.list: + print("\n" + "=" * 80) + print("DETAILED FILE LISTINGS") + print("=" * 80) + + # FL directory files + print_file_list(fl_files["hpp"], "FL Header Files (.hpp)", FL_DIR) + print_file_list( + fl_files["cpp_hpp"], "FL Implementation Files (.cpp.hpp)", FL_DIR + ) + + # FX directory files + print_file_list(fx_files["hpp"], "FX Header Files (.hpp)", FX_DIR) + print_file_list( + fx_files["cpp_hpp"], "FX Implementation Files (.cpp.hpp)", FX_DIR + ) + + # Check inclusion in all-source build if requested + if args.check_inclusion: + # Only show inclusion check if not suppressing or if we don't have 100% coverage + if not should_suppress_summary: + print("\n" + "=" * 80) + print("ALL-SOURCE BUILD INCLUSION CHECK") + print("=" * 80) + + fl_inclusion = check_inclusion_in_all_source_build(fl_files, FL_DIR) + fx_inclusion = check_inclusion_in_all_source_build(fx_files, FX_DIR) + + print(f"\n{search_symbol} FL DIRECTORY INCLUSION:") + print_inclusion_report(fl_inclusion, FL_DIR) + + print(f"\n{search_symbol} FX DIRECTORY INCLUSION:") + print_inclusion_report(fx_inclusion, FX_DIR) + + # Overall inclusion statistics + all_cpp_hpp_files = fl_files["cpp_hpp"] + fx_files["cpp_hpp"] + all_source_includes = get_all_source_build_includes() + + included_count = 0 + for file_path in all_cpp_hpp_files: + relative_path = file_path.relative_to(SRC_ROOT) + relative_path_str = str(relative_path).replace("\\", "/") + if relative_path_str in all_source_includes: + included_count += 1 + + total_impl_files = len(all_cpp_hpp_files) + if total_impl_files > 0: + inclusion_percentage = (included_count / total_impl_files) * 100 + print(f"\n{stats_symbol} OVERALL INCLUSION STATISTICS:") + print(f" Total .cpp.hpp files found: {total_impl_files}") + print(f" Included in all-source build: {included_count}") + print(f" Inclusion percentage: {inclusion_percentage:.1f}%") + + # Always check for missing files and exit with error if any are missing + all_cpp_hpp_files = fl_files["cpp_hpp"] + fx_files["cpp_hpp"] + all_source_includes = get_all_source_build_includes() + + included_count = 0 + for file_path in all_cpp_hpp_files: + relative_path = file_path.relative_to(SRC_ROOT) + relative_path_str = str(relative_path).replace("\\", "/") + if relative_path_str in all_source_includes: + included_count += 1 + + total_impl_files = len(all_cpp_hpp_files) + total_missing = total_impl_files - included_count + if total_missing > 0: + # Print an explicit error message before exiting + error_symbol = "[ERROR]" if USE_ASCII_ONLY else "🚨" + print( + f"\n{error_symbol} {total_missing} implementation file(s) are missing from the all-source build!" + ) + print(" Failing script due to incomplete all-source build inclusion.") + import sys + + sys.exit(1) + + if args.verbose: + print(f"\n{config_symbol} CONFIGURATION:") + print(f" Project root: {PROJECT_ROOT}") + print(f" FL directory: {FL_DIR}") + print(f" FX directory: {FX_DIR}") + print(f" All-source build file: {ALL_SOURCE_BUILD_FILE}") + print(" Hierarchical compile files:") + for hfile in HIERARCHICAL_FILES: + status = "✓" if hfile.exists() else "✗" + print(f" {status} {hfile.relative_to(PROJECT_ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/util/concurrent_run.py b/v1.5/lib/FastLED/ci/util/concurrent_run.py new file mode 100644 index 0000000..649b6bd --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/concurrent_run.py @@ -0,0 +1,250 @@ +# pyright: reportUnknownMemberType=false +""" +Concurrent run utilities. +""" + +import os +import subprocess +import time +from concurrent.futures import Future, ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List + +from ci.boards import Board # type: ignore +from ci.compiler.compile_for_board import compile_examples, errors_happened +from ci.util.cpu_count import cpu_count +from ci.util.create_build_dir import create_build_dir +from ci.util.locked_print import locked_print + + +# Board initialization doesn't take a lot of memory or cpu so it's safe to run in parallel +PARRALLEL_PROJECT_INITIALIZATION = ( + os.environ.get("PARRALLEL_PROJECT_INITIALIZATION", "0") == "1" +) + + +def _banner_print(msg: str) -> None: + """Print a banner message.""" + # will produce + ####### + # msg # + ####### + lines = msg.splitlines() + for line in lines: + print("#" * (len(line) + 4)) + print(f"# {line} #") + print("#" * (len(line) + 4)) + + +@dataclass +class ConcurrentRunArgs: + projects: list[Board] + examples: list[Path] + skip_init: bool + defines: list[str] + customsdk: str | None + extra_packages: list[str] + libs: list[str] | None + build_dir: str | None + extra_scripts: str | None + cwd: str | None + board_dir: str | None + build_flags: list[str] | None + verbose: bool = False + extra_examples: dict[Board, list[Path]] | None = None + symbols: bool = False + + +def concurrent_run( + args: ConcurrentRunArgs, +) -> int: + projects = args.projects + examples = args.examples + skip_init = args.skip_init + defines = args.defines + customsdk = args.customsdk + extra_packages = args.extra_packages + build_dir = args.build_dir + extra_scripts = args.extra_scripts + cwd = args.cwd + start_time = time.time() + first_project = projects[0] + prev_cwd: str | None = None + board_dir = args.board_dir + libs = args.libs + extra_examples: dict[Board, list[Path]] = args.extra_examples or {} + if cwd: + prev_cwd = os.getcwd() + locked_print(f"Changing to directory {cwd}") + os.chdir(cwd) + + start_time = time.time() + create_build_dir( + board=first_project, + defines=defines, + customsdk=customsdk, + no_install_deps=skip_init, + extra_packages=extra_packages, + build_dir=build_dir, + board_dir=board_dir, + build_flags=args.build_flags, + extra_scripts=extra_scripts, + ) + diff = time.time() - start_time + + msg = f"Build directory created in {diff:.2f} seconds for board" + locked_print(msg) + + verbose = args.verbose + # This is not memory/cpu bound but is instead network bound so we can run one thread + # per board to speed up the process. + parallel_init_workers = 1 if not PARRALLEL_PROJECT_INITIALIZATION else len(projects) + # Initialize the build directories for all boards + locked_print( + f"Initializing build directories for {len(projects)} boards with {parallel_init_workers} parallel workers" + ) + with ThreadPoolExecutor(max_workers=parallel_init_workers) as executor: + future_to_board: Dict[Future[Any], Board] = {} + for board in projects: + locked_print( + f"Submitting build directory initialization for board: {board.board_name}" + ) + future = executor.submit( + create_build_dir, + board, + defines, + customsdk, + skip_init, + extra_packages, + build_dir, + board_dir, + args.build_flags, + extra_scripts, + ) + future_to_board[future] = board + + completed_boards = 0 + failed_boards = 0 + for future in as_completed(future_to_board): + board = future_to_board[future] + try: + success, msg = future.result() + if not success: + locked_print( + f"ERROR: Failed to initialize build_dir for board {board.board_name}:\n{msg}" + ) + failed_boards += 1 + # cancel all other tasks + for f in future_to_board: + if not f.done(): + f.cancel() + locked_print( + "Cancelled initialization for remaining boards due to failure" + ) + return 1 + else: + completed_boards += 1 + locked_print( + f"SUCCESS: Finished initializing build_dir for board {board.board_name} ({completed_boards}/{len(projects)})" + ) + except Exception as e: + locked_print( + f"EXCEPTION: Build directory initialization failed for board {board.board_name}: {e}" + ) + failed_boards += 1 + # cancel all other tasks + for f in future_to_board: + if not f.done(): + f.cancel() + locked_print( + "Cancelled initialization for remaining boards due to exception" + ) + return 1 + init_end_time = time.time() + init_time = (init_end_time - start_time) / 60 + locked_print(f"\nAll build directories initialized in {init_time:.2f} minutes.") + errors: list[str] = [] + # Run the compilation process + num_cpus = max(1, min(cpu_count(), len(projects))) + with ThreadPoolExecutor(max_workers=num_cpus) as executor: + future_to_board: Dict[Future[Any], Board] = { + executor.submit( + compile_examples, + board, + examples + extra_examples.get(board, []), + build_dir, + verbose, + libs=libs, + ): board + for board in projects + } + for future in as_completed(future_to_board): + board = future_to_board[future] + success, msg = future.result() + if not success: + msg = f"Compilation failed for board {board}: {msg}" + errors.append(msg) + locked_print(f"Compilation failed for board {board}: {msg}.\nStopping.") + for f in future_to_board: + f.cancel() + break + if prev_cwd: + locked_print(f"Changing back to directory {prev_cwd}") + os.chdir(prev_cwd) + if errors_happened(): + locked_print("\nDone. Errors happened during compilation.") + locked_print("\n".join(errors)) + return 1 + + # Run symbol analysis if requested + if args.symbols: + locked_print("\nRunning symbol analysis on compiled outputs...") + symbol_analysis_errors: List[str] = [] + + for board in projects: + try: + locked_print(f"Running symbol analysis for board: {board.board_name}") + + # Run the symbol analysis tool for this board + cmd = [ + "uv", + "run", + "ci/util/symbol_analysis.py", + "--board", + board.board_name, + ] + + result = subprocess.run( + cmd, capture_output=True, text=True, cwd=cwd or os.getcwd() + ) + + if result.returncode != 0: + error_msg = f"Symbol analysis failed for board {board.board_name}: {result.stderr}" + symbol_analysis_errors.append(error_msg) + locked_print(f"ERROR: {error_msg}") + else: + locked_print( + f"Symbol analysis completed for board: {board.board_name}" + ) + # Print the symbol analysis output + if result.stdout: + print(result.stdout) + + except Exception as e: + error_msg = f"Exception during symbol analysis for board {board.board_name}: {e}" + symbol_analysis_errors.append(error_msg) + locked_print(f"ERROR: {error_msg}") + + if symbol_analysis_errors: + locked_print( + f"\nSymbol analysis completed with {len(symbol_analysis_errors)} error(s):" + ) + for error in symbol_analysis_errors: + locked_print(f" - {error}") + else: + locked_print( + f"\nSymbol analysis completed successfully for all {len(projects)} board(s)." + ) + + return 0 diff --git a/v1.5/lib/FastLED/ci/util/console_utf8.py b/v1.5/lib/FastLED/ci/util/console_utf8.py new file mode 100644 index 0000000..2cf86f3 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/console_utf8.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import os +import sys + + +def configure_utf8_console() -> None: + """Ensure stdout/stderr use UTF-8 encoding on Windows consoles. + + Safe no-op on non-Windows platforms and on environments where + reconfigure is unavailable. + """ + if os.name != "nt": + return + + try: + if hasattr(sys.stdout, "reconfigure") and callable( + getattr(sys.stdout, "reconfigure", None) + ): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined] + if hasattr(sys.stderr, "reconfigure") and callable( + getattr(sys.stderr, "reconfigure", None) + ): + sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined] + except (AttributeError, OSError): + # Older Python versions or redirected streams may not support reconfigure + pass diff --git a/v1.5/lib/FastLED/ci/util/cpu_count.py b/v1.5/lib/FastLED/ci/util/cpu_count.py new file mode 100644 index 0000000..2c09860 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/cpu_count.py @@ -0,0 +1,12 @@ +import os + + +def cpu_count() -> int: + """Get the number of CPUs.""" + # Force sequential execution if NO_PARALLEL is set + if "GITHUB_ACTIONS" in os.environ: + return 1 + no_parallel = os.environ.get("NO_PARALLEL", "0") in ["1", "true", "True"] + if no_parallel: + return 1 + return os.cpu_count() or 1 diff --git a/v1.5/lib/FastLED/ci/util/create_build_dir.py b/v1.5/lib/FastLED/ci/util/create_build_dir.py new file mode 100644 index 0000000..b0fba54 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/create_build_dir.py @@ -0,0 +1,590 @@ +# pyright: reportUnknownMemberType=false +""" +Create build directory for project. +""" + +import json +import os +import shutil +import subprocess +import time +import warnings +from pathlib import Path +from typing import Any, Callable, Dict + +from ci.boards import Board # type: ignore +from ci.util.locked_print import locked_print + + +def _install_global_package(package: str) -> None: + # example pio pkg -g -p "https://github.com/maxgerhardt/platform-raspberrypi.git". + locked_print(f"*** Installing {package} ***") + cmd_list = [ + "pio", + "pkg", + "install", + "-g", + "-p", + package, + ] + cmd_str = subprocess.list2cmdline(cmd_list) + locked_print(f"Running command:\n\n{cmd_str}\n\n") + result = subprocess.run( + cmd_str, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=True, + ) + locked_print(result.stdout) + locked_print(f"*** Finished installing {package} ***") + + +def insert_tool_aliases(meta_json: Dict[str, Dict[str, Any]]) -> None: + for board in meta_json.keys(): + aliases: dict[str, str | None] = {} + cc_path_value = meta_json[board].get("cc_path") + resolved_cc_path: Path | None = None + if cc_path_value: + try: + candidate = Path(str(cc_path_value)) + if candidate.is_absolute() and candidate.exists(): + resolved_cc_path = candidate + elif candidate.exists(): + resolved_cc_path = candidate.resolve() + else: + which_result = shutil.which( + candidate.name if candidate.name else str(candidate) + ) + if which_result: + resolved_cc_path = Path(which_result) + except Exception: + resolved_cc_path = None + + # Try to infer toolchain bin directory and prefix from either CC or GDB path + tool_bin_dir: Path | None = None + tool_prefix: str = "" + tool_suffix: str = "" + + if resolved_cc_path and resolved_cc_path.exists(): + cc_base = resolved_cc_path.name + # If cc_path points at a real gcc binary, derive prefix/suffix from it. + # If it's a wrapper (e.g. cached_CC.cmd) without "gcc" in the name, + # fall back to using gdb_path to derive the actual toolchain prefix/suffix. + if "gcc" in cc_base: + tool_bin_dir = resolved_cc_path.parent + tool_prefix = cc_base.split("gcc")[0] + tool_suffix = resolved_cc_path.suffix + else: + resolved_cc_path = None # Force gdb-based fallback below + if resolved_cc_path is None: + gdb_path_value = meta_json[board].get("gdb_path") + if gdb_path_value: + try: + gdb_path = Path(str(gdb_path_value)) + if not gdb_path.exists(): + which_gdb = shutil.which(gdb_path.name) + if which_gdb: + gdb_path = Path(which_gdb) + if gdb_path.exists(): + tool_bin_dir = gdb_path.parent + gdb_base = gdb_path.name + # Derive prefix like 'arm-none-eabi-' from 'arm-none-eabi-gdb' + tool_prefix = ( + gdb_base.split("gdb")[0] if "gdb" in gdb_base else "" + ) + tool_suffix = gdb_path.suffix + except Exception: + pass + + tools = [ + "gcc", + "g++", + "ar", + "objcopy", + "objdump", + "size", + "nm", + "ld", + "as", + "ranlib", + "strip", + "c++filt", + "readelf", + "addr2line", + ] + + if tool_bin_dir is not None: + for tool in tools: + name = f"{tool_prefix}{tool}" + tool_suffix + tool_path = tool_bin_dir / name + if tool_path.exists(): + aliases[tool] = str(tool_path) + else: + which_result = shutil.which(name) + aliases[tool] = str(Path(which_result)) if which_result else None + else: + # Fallback: resolve via PATH only + for tool in tools: + which_result = shutil.which(tool) + aliases[tool] = str(Path(which_result)) if which_result else None + + meta_json[board]["aliases"] = aliases + + +def remove_readonly(func: Callable[..., Any], path: str, _: Any) -> None: + "Clear the readonly bit and reattempt the removal" + if os.name == "nt": + os.system(f"attrib -r {path}") + else: + try: + os.chmod(path, 0o777) + except Exception: + print(f"Error removing readonly attribute from {path}") + + func(path) + + +def robust_rmtree(path: Path, max_retries: int, delay: float) -> bool: + """ + Robustly remove a directory tree, handling race conditions and concurrent access. + + Args: + path: Path to the directory to remove + max_retries: Maximum number of retry attempts + delay: Delay between retries in seconds + + Returns: + True if removal was successful, False otherwise + """ + if not path.exists(): + locked_print(f"Directory {path} doesn't exist, skipping removal") + return True + + for attempt in range(max_retries): + try: + locked_print( + f"Attempting to remove directory {path} (attempt {attempt + 1}/{max_retries})" + ) + shutil.rmtree(path, onerror=remove_readonly) + locked_print(f"Successfully removed directory {path}") + return True + except OSError as e: + if attempt == max_retries - 1: + locked_print( + f"Failed to remove directory {path} after {max_retries} attempts: {e}" + ) + return False + + # Log the specific error and retry + locked_print( + f"Failed to remove directory {path} on attempt {attempt + 1}: {e}" + ) + + # Check if another process removed it + if not path.exists(): + locked_print(f"Directory {path} was removed by another process") + return True + + # Wait before retrying + time.sleep(delay * (2**attempt)) # Exponential backoff + + except Exception as e: + locked_print(f"Unexpected error removing directory {path}: {e}") + return False + + return False + + +def safe_file_removal(file_path: Path, max_retries: int) -> bool: + """ + Safely remove a file with retry logic. + + Args: + file_path: Path to the file to remove + max_retries: Maximum number of retry attempts + + Returns: + True if removal was successful, False otherwise + """ + if not file_path.exists(): + return True + + for attempt in range(max_retries): + try: + file_path.unlink() + locked_print(f"Successfully removed file {file_path}") + return True + except OSError as e: + if attempt == max_retries - 1: + locked_print( + f"Failed to remove file {file_path} after {max_retries} attempts: {e}" + ) + return False + + locked_print( + f"Failed to remove file {file_path} on attempt {attempt + 1}: {e}" + ) + + # Check if another process removed it + if not file_path.exists(): + locked_print(f"File {file_path} was removed by another process") + return True + + time.sleep(0.1 * (attempt + 1)) + + except Exception as e: + locked_print(f"Unexpected error removing file {file_path}: {e}") + return False + + return False + + +def create_build_dir( + board: Board, + defines: list[str], + customsdk: str | None, + no_install_deps: bool, + extra_packages: list[str], + build_dir: str | None, + board_dir: str | None, + build_flags: list[str] | None, + extra_scripts: str | None, +) -> tuple[bool, str]: + """Create the build directory for the given board.""" + import threading + + # filter out "web" board because it's not a real board. + if board.board_name == "web": + locked_print(f"Skipping web target for board {board.board_name}") + return True, "" + if board.defines: + defines.extend(board.defines) + # remove duplicates + defines = list(set(defines)) + board_name = board.board_name + real_board_name = board.get_real_board_name() + thread_id = threading.current_thread().ident + locked_print( + f"*** [Thread {thread_id}] Initializing environment for {board_name} ***" + ) + # builddir = Path(build_dir) / board if build_dir else Path(".build") / board + build_dir = build_dir or ".build" + builddir = Path(build_dir) / board_name + + locked_print(f"[Thread {thread_id}] Creating build directory: {builddir}") + try: + builddir.mkdir(parents=True, exist_ok=True) + locked_print( + f"[Thread {thread_id}] Successfully created build directory: {builddir}" + ) + except Exception as e: + locked_print( + f"[Thread {thread_id}] Error creating build directory {builddir}: {e}" + ) + return False, f"Failed to create build directory: {e}" + # if lib directory (where FastLED lives) exists, remove it. This is necessary to run on + # recycled build directories for fastled to update. This is a fast operation. + srcdir = builddir / "lib" + if srcdir.exists(): + locked_print(f"[Thread {thread_id}] Removing existing lib directory: {srcdir}") + # STRICT: Explicit retry parameters - NO defaults allowed + if not robust_rmtree(srcdir, max_retries=5, delay=0.1): + locked_print( + f"[Thread {thread_id}] Warning: Failed to remove lib directory {srcdir}, continuing anyway" + ) + + platformio_ini = builddir / "platformio.ini" + if platformio_ini.exists(): + locked_print( + f"[Thread {thread_id}] Removing existing platformio.ini: {platformio_ini}" + ) + # STRICT: Explicit retry parameter - NO defaults allowed + if not safe_file_removal(platformio_ini, max_retries=3): + locked_print( + f"[Thread {thread_id}] Warning: Failed to remove {platformio_ini}, continuing anyway" + ) + + if board_dir: + dst_dir = builddir / "boards" + locked_print( + f"[Thread {thread_id}] Processing board directory: {board_dir} -> {dst_dir}" + ) + + if dst_dir.exists(): + locked_print( + f"[Thread {thread_id}] Removing existing boards directory: {dst_dir}" + ) + # STRICT: Explicit retry parameters - NO defaults allowed + if not robust_rmtree(dst_dir, max_retries=5, delay=0.1): + locked_print( + f"[Thread {thread_id}] Error: Failed to remove boards directory {dst_dir}" + ) + return False, f"Failed to remove existing boards directory {dst_dir}" + + try: + locked_print( + f"[Thread {thread_id}] Copying board directory: {board_dir} -> {dst_dir}" + ) + shutil.copytree(str(board_dir), str(dst_dir)) + locked_print( + f"[Thread {thread_id}] Successfully copied board directory to {dst_dir}" + ) + except Exception as e: + locked_print(f"[Thread {thread_id}] Error copying board directory: {e}") + return False, f"Failed to copy board directory: {e}" + if board.platform_needs_install: + if board.platform: + try: + _install_global_package(board.platform) + except subprocess.CalledProcessError as e: + stdout = e.stdout + return False, stdout + else: + warnings.warn("Platform install was specified but no platform was given.") + + cmd_list = [ + "pio", + "project", + "init", + "--project-dir", + builddir.as_posix(), + "--board", + real_board_name, + ] + if board.platform: + cmd_list.append(f"--project-option=platform={board.platform}") + if board.platform_packages: + cmd_list.append(f"--project-option=platform_packages={board.platform_packages}") + if board.framework: + cmd_list.append(f"--project-option=framework={board.framework}") + if board.board_build_core: + cmd_list.append(f"--project-option=board_build.core={board.board_build_core}") + if board.board_build_filesystem_size: + cmd_list.append( + f"--project-option=board_build.filesystem_size={board.board_build_filesystem_size}" + ) + if build_flags is not None: + for build_flag in build_flags: + cmd_list.append(f"--project-option=build_flags={build_flag}") + if defines: + build_flags_str = " ".join(f"-D{define}" for define in defines) + cmd_list.append(f"--project-option=build_flags={build_flags_str}") + if board.customsdk: + cmd_list.append(f"--project-option=custom_sdkconfig={customsdk}") + if extra_packages: + cmd_list.append(f"--project-option=lib_deps={','.join(extra_packages)}") + if no_install_deps: + cmd_list.append("--no-install-dependencies") + + # Add CCACHE configuration script + ccache_script = builddir / "ccache_config.py" + if not ccache_script.exists(): + locked_print( + f"[Thread {thread_id}] Creating CCACHE configuration script at {ccache_script}" + ) + with open(ccache_script, "w") as f: + f.write( + '''"""Configure CCACHE for PlatformIO builds.""" + +import os +import platform +import subprocess +from pathlib import Path + +Import("env") + +def is_ccache_available(): + """Check if ccache is available in the system.""" + try: + subprocess.run(["ccache", "--version"], capture_output=True, check=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + +def get_ccache_path(): + """Get the full path to ccache executable.""" + if platform.system() == "Windows": + # On Windows, look in chocolatey's bin directory + ccache_paths = [ + "C:\\ProgramData\\chocolatey\\bin\\ccache.exe", + os.path.expanduser("~\\scoop\\shims\\ccache.exe") + ] + for path in ccache_paths: + if os.path.exists(path): + return path + else: + # On Unix-like systems, use which to find ccache + try: + return subprocess.check_output(["which", "ccache"]).decode().strip() + except subprocess.CalledProcessError: + pass + return None + +def configure_ccache(env): + """Configure CCACHE for the build environment.""" + if not is_ccache_available(): + print("CCACHE is not available. Skipping CCACHE configuration.") + return + + ccache_path = get_ccache_path() + if not ccache_path: + print("Could not find CCACHE executable. Skipping CCACHE configuration.") + return + + print(f"Found CCACHE at: {ccache_path}") + + # Set up CCACHE environment variables if not already set + if "CCACHE_DIR" not in os.environ: + # STRICT: PROJECT_DIR must be explicitly set - NO fallbacks allowed + project_dir_for_ccache = env.get("PROJECT_DIR") + if not project_dir_for_ccache: + raise RuntimeError( + "CRITICAL: PROJECT_DIR environment variable is required for CCACHE_DIR setup. " + "Please set PROJECT_DIR to the project root directory." + ) + ccache_dir = os.path.join(project_dir_for_ccache, ".ccache") + os.environ["CCACHE_DIR"] = ccache_dir + Path(ccache_dir).mkdir(parents=True, exist_ok=True) + + # Configure CCACHE for this build + # STRICT: PROJECT_DIR must be explicitly set - NO fallbacks allowed + project_dir = env.get("PROJECT_DIR") + if not project_dir: + raise RuntimeError( + "CRITICAL: PROJECT_DIR environment variable is required but not set. " + "Please set PROJECT_DIR to the project root directory." + ) + os.environ["CCACHE_BASEDIR"] = project_dir + os.environ["CCACHE_COMPRESS"] = "true" + os.environ["CCACHE_COMPRESSLEVEL"] = "6" + os.environ["CCACHE_MAXSIZE"] = "400M" + + # Wrap compiler commands with ccache + # STRICT: CC and CXX must be explicitly set - NO fallbacks allowed + original_cc = env.get("CC") + if not original_cc: + raise RuntimeError( + "CRITICAL: CC environment variable is required but not set. " + "Please set CC to the C compiler path (e.g., gcc, clang)." + ) + original_cxx = env.get("CXX") + if not original_cxx: + raise RuntimeError( + "CRITICAL: CXX environment variable is required but not set. " + "Please set CXX to the C++ compiler path (e.g., g++, clang++)." + ) + + # Don't wrap if already wrapped + if "ccache" not in original_cc: + env.Replace( + CC=f"{ccache_path} {original_cc}", + CXX=f"{ccache_path} {original_cxx}", + ) + print(f"Wrapped CC: {env.get('CC')}") + print(f"Wrapped CXX: {env.get('CXX')}") + + # Show CCACHE stats + subprocess.run([ccache_path, "--show-stats"], check=False) + +configure_ccache(env)''' + ) + + # Get absolute paths for scripts + project_root = Path.cwd() + ci_flags_script = (project_root / "ci" / "ci-flags.py").resolve().as_posix() + ccache_script = (builddir / "ccache_config.py").resolve().as_posix() + + # Create a list of scripts with pre: prefix + script_list = [f"pre:{ci_flags_script}", f"pre:{ccache_script}"] + + # Add any additional scripts + if extra_scripts: + # Convert to absolute path and use Unix-style separators + extra_scripts_path = str(Path(extra_scripts).resolve().as_posix()) + if not extra_scripts_path.startswith("pre:"): + extra_scripts_path = f"pre:{extra_scripts_path}" + script_list.append(extra_scripts_path) + + # Add the scripts as a list + cmd_list.append(f"--project-option=extra_scripts=[{','.join(script_list)}]") + + cmd_str = subprocess.list2cmdline(cmd_list) + locked_print(f"\n\nRunning command:\n {cmd_str}\n") + result = subprocess.run( + cmd_str, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + stdout = result.stdout + locked_print(result.stdout) + if result.returncode != 0: + locked_print( + f"*** [Thread {thread_id}] Error setting up board {board_name} ***" + ) + return False, stdout + locked_print( + f"*** [Thread {thread_id}] Finished initializing environment for board {board_name} ***" + ) + + # Print the location of the generated platformio.ini file + platformio_ini_path = builddir / "platformio.ini" + locked_print(f"Writing to platformio.ini {platformio_ini_path}") + + # Print the contents of the generated platformio.ini file for debugging + platformio_ini_path = builddir / "platformio.ini" + if platformio_ini_path.exists(): + locked_print( + f"\n*** Contents of {platformio_ini_path} after initialization ***" + ) + try: + with open(platformio_ini_path, "r") as f: + ini_contents = f.read() + locked_print(f"\n\n{ini_contents}\n\n") + except Exception as e: + locked_print(f"Error reading {platformio_ini_path}: {e}") + locked_print(f"*** End of {platformio_ini_path} contents ***\n") + else: + locked_print( + f"Warning: {platformio_ini_path} was not found after initialization" + ) + + # dumping enviorment variables to help debug. + # this is the command: pio run --target envdump + cwd = str(builddir.resolve()) + cmd_list = [ + "pio", + "project", + "metadata", + "--json-output", + ] + cmd_str = subprocess.list2cmdline(cmd_list) + stdout = subprocess.run( + cmd_list, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ).stdout + + try: + data = json.loads(stdout) + # now dump the values to the file at the root of the build directory. + matadata_json = builddir / "build_info.json" + try: + insert_tool_aliases(data) + formatted = json.dumps(data, indent=4, sort_keys=True) + with open(matadata_json, "w") as f: + f.write(formatted) + except Exception: + with open(matadata_json, "w") as f: + f.write(stdout) + except json.JSONDecodeError: + msg = f"build_info.json will not be generated because of error because stdout does not look like a json file:\n#### STDOUT ####\n{stdout}\n#### END STDOUT ####\n" + locked_print(msg) + return True, stdout diff --git a/v1.5/lib/FastLED/ci/util/elf.py b/v1.5/lib/FastLED/ci/util/elf.py new file mode 100644 index 0000000..dd4a2e3 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/elf.py @@ -0,0 +1,207 @@ +# pyright: reportUnknownMemberType=false +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List + + +def run_command(command: List[str], show_output: bool = False) -> str: + """ + Run a command using subprocess and capture the output. + + Args: + command (list): Command to run. + show_output (bool): Print command and its output if True. + + Returns: + str: Standard output of the command. + + Raises: + RuntimeError: If the command fails. + """ + if show_output: + print(f"Running command: {' '.join(command)}") + result = subprocess.run(command, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(f"Command failed: {' '.join(command)}\n{result.stderr}") + if show_output and result.stdout: + print(f"Command output: {result.stdout}") + return result.stdout + + +def analyze_elf_file(objdump_path: Path, cppfilt_path: Path, elf_file: Path): + """ + Analyze the ELF file using objdump to display its contents. + + Args: + objdump_path (Path): Path to the objdump executable. + cppfilt_path (Path): Path to the c++filt executable. + elf_file (Path): Path to the ELF file. + """ + command = [str(objdump_path), "-h", str(elf_file)] # "-h" option shows headers. + print(f"Analyzing ELF file: {elf_file}") + output = run_command(command, show_output=True) + print("\nELF File Analysis:") + print(output) + list_symbols_and_sizes(objdump_path, cppfilt_path, elf_file) + + +def cpp_filt(cppfilt_path: Path, input_text: str) -> str: + """ + Demangle C++ symbols using c++filt. + + Args: + cppfilt_path (Path): Path to c++filt executable. + input_text (str): Text to demangle. + + Returns: + str: Demangled text. + """ + command = [str(cppfilt_path), "-t", "-n"] + print(f"Running c++filt on input text with {cppfilt_path}") + process = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + stdout, stderr = process.communicate(input=input_text) + if process.returncode != 0: + raise RuntimeError(f"Error running c++filt: {stderr}") + return stdout + + +def dump_symbol_sizes(nm_path: Path, cpp_filt_path: Path, elf_file: Path) -> str: + nm_command = [ + str(nm_path), + "-S", + "--size-sort", + str(elf_file), + ] + print(f"Listing symbols and sizes in ELF file: {elf_file}") + print("Running command: ", " ".join(nm_command)) + nm_result = subprocess.run( + nm_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if nm_result.returncode != 0: + raise RuntimeError(f"Error running nm command: {nm_result.stderr}") + + cpp_filt_command = [str(cpp_filt_path), "--no-strip-underscore"] + print("Running c++filt command: ", " ".join(cpp_filt_command)) + cpp_filt_result = subprocess.run( + cpp_filt_command, + input=nm_result.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if cpp_filt_result.returncode != 0: + raise RuntimeError(f"Error running c++filt command: {cpp_filt_result.stderr}") + + # now reverse sort the lines + lines = cpp_filt_result.stdout.splitlines() + + @dataclass + class Entry: + address: str + size: int + everything_else: str + + def parse_line(line: str) -> Entry: + address, size, *rest = line.split() + return Entry(address, int(size, 16), " ".join(rest)) + + data: list[Entry] = [parse_line(line) for line in lines] + data.sort(key=lambda x: x.size, reverse=True) + lines = [f"{d.size:6d} {d.everything_else}" for d in data] + return "\n".join(lines) + + +def demangle_symbol(cppfilt_path: Path, symbol: str) -> str: + """ + Demangle a C++ symbol using c++filt. + + Args: + cppfilt_path (Path): Path to the c++filt executable. + symbol (str): The symbol to demangle. + + Returns: + str: The demangled symbol. + """ + command = [str(cppfilt_path), symbol] + return run_command(command, show_output=False).strip() + + +def list_symbols_and_sizes(objdump_path: Path, cppfilt_path: Path, elf_file: Path): + """ + List all symbols and their sizes from the ELF file using objdump. + + Args: + objdump_path (Path): Path to the objdump executable. + cppfilt_path (Path): Path to the c++filt executable. + elf_file (Path): Path to the ELF file. + """ + command = [ + str(objdump_path), + "-t", + str(elf_file), + ] # "-t" option lists symbols with sizes. + print(f"Listing symbols and sizes in ELF file: {elf_file}") + output = run_command(command, show_output=False) + + symbols: List[Dict[str, Any]] = [] + for line in output.splitlines(): + parts = line.split() + # Expected parts length can vary, check if size and section index (parts[2] & parts[4]) are valid + if len(parts) > 5 and parts[2].isdigit() and parts[4].startswith("."): + symbol = { + "name": parts[-1], + "size": int(parts[2], 16), # size is in hex format + "section": parts[4], + "type": parts[3], + } + symbols.append(symbol) + + if symbols: + print("\nSymbols and Sizes in ELF File:") + for symbol in symbols: + demangled_name = demangle_symbol(cppfilt_path, symbol["name"]) + print( + f"Symbol: {demangled_name}, Size: {symbol['size']} bytes, Type: {symbol['type']}, Section: {symbol['section']}" + ) + else: + print("No symbols found or unable to parse symbols correctly.") + + +def check_elf_format(objdump_path: Path, elf_file: Path): + """ + Check the format of the ELF file using objdump to confirm it's being read correctly. + + Args: + objdump_path (Path): Path to the objdump executable. + elf_file (Path): Path to the ELF file. + """ + command = [str(objdump_path), "-f", str(elf_file)] + print(f"Checking ELF file format: {elf_file}") + output = run_command(command, show_output=True) + print("\nELF File Format Information:") + print(output) + + +def check_section_contents(objdump_path: Path, elf_file: Path): + """ + Dump the contents of all sections in the ELF file using objdump. + + Args: + objdump_path (Path): Path to the objdump executable. + elf_file (Path): Path to the ELF file. + """ + command = [str(objdump_path), "-s", str(elf_file)] + print(f"Dumping all sections of ELF file: {elf_file}") + output = run_command(command, show_output=True) + print("\nELF File Sections Content:") + print(output) diff --git a/v1.5/lib/FastLED/ci/util/esp32_symbol_analysis.py b/v1.5/lib/FastLED/ci/util/esp32_symbol_analysis.py new file mode 100644 index 0000000..40718e5 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/esp32_symbol_analysis.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false, reportMissingParameterType=false, reportUnknownLambdaType=false, reportArgumentType=false +""" +ESP32 Symbol Analysis Tool +Analyzes the ESP32 ELF file to identify symbols that can be eliminated for binary size reduction. +""" + +import json +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Tuple + + +@dataclass +class SymbolInfo: + """Represents a symbol in the ESP32 binary""" + + address: str + size: int + type: str + name: str + demangled_name: str + source: str # STRICT: NO defaults - all callers must provide explicit source + + +def run_command(cmd: str) -> str: + """Run a command and return stdout""" + try: + result = subprocess.run( + cmd, shell=True, capture_output=True, text=True, check=True + ) + return result.stdout + except subprocess.CalledProcessError as e: + print(f"Error running command: {cmd}") + print(f"Error: {e.stderr}") + return "" + + +def demangle_symbol(mangled_name: str, cppfilt_path: str) -> str: + """Demangle a C++ symbol using c++filt""" + try: + cmd = f'echo "{mangled_name}" | "{cppfilt_path}"' + result = subprocess.run( + cmd, shell=True, capture_output=True, text=True, check=True + ) + demangled = result.stdout.strip() + # If demangling failed, c++filt returns the original name + return demangled if demangled != mangled_name else mangled_name + except Exception as e: + print(f"Error demangling symbol: {mangled_name}") + print(f"Error: {e}") + return mangled_name + + +def analyze_symbols( + elf_file: str, nm_path: str, cppfilt_path: str +) -> Tuple[List[SymbolInfo], List[SymbolInfo], List[SymbolInfo]]: + """Analyze symbols in ELF file using nm with C++ demangling""" + print("Analyzing symbols...") + + # Get all symbols with sizes + cmd = f'"{nm_path}" --print-size --size-sort --radix=d "{elf_file}"' + output = run_command(cmd) + + symbols: List[SymbolInfo] = [] + fastled_symbols: List[SymbolInfo] = [] + large_symbols: List[SymbolInfo] = [] + + print("Demangling C++ symbols...") + + for line in output.strip().split("\n"): + if not line.strip(): + continue + + parts = line.split() + if len(parts) >= 4: + addr = parts[0] + size = int(parts[1]) + symbol_type = parts[2] + mangled_name = " ".join(parts[3:]) + + # Demangle the symbol name + demangled_name = demangle_symbol(mangled_name, cppfilt_path) + + symbol_info = SymbolInfo( + address=addr, + size=size, + type=symbol_type, + name=mangled_name, + demangled_name=demangled_name, + source="esp32_nm", + ) + + symbols.append(symbol_info) + + # Identify FastLED-related symbols using demangled names + search_text = demangled_name.lower() + if any( + keyword in search_text + for keyword in [ + "fastled", + "cfastled", + "crgb", + "hsv", + "pixel", + "controller", + "led", + "rmt", + "strip", + "neopixel", + "ws2812", + "apa102", + ] + ): + fastled_symbols.append(symbol_info) + + # Identify large symbols (>100 bytes) + if size > 100: + large_symbols.append(symbol_info) + + return symbols, fastled_symbols, large_symbols + + +def analyze_map_file(map_file: str) -> Dict[str, List[str]]: + """Analyze the map file to understand module dependencies""" + print("Analyzing map file...") + + dependencies: Dict[str, List[str]] = {} + current_archive = None + + try: + with open(map_file, "r") as f: + for line in f: + line = line.strip() + + # Look for archive member includes + if line.startswith(".pio/build/esp32dev/liba4c/libsrc.a("): + # Extract module name + start = line.find("(") + 1 + end = line.find(")") + if start > 0 and end > start: + current_archive = line[start:end] + dependencies[current_archive] = [] + + elif current_archive and line and not line.startswith(".pio"): + # This line shows what pulled in the module + if "(" in line and ")" in line: + # Extract the symbol that caused the inclusion + symbol_start = line.find("(") + 1 + symbol_end = line.find(")") + if symbol_start > 0 and symbol_end > symbol_start: + symbol = line[symbol_start:symbol_end] + dependencies[current_archive].append(symbol) + current_archive = None + except FileNotFoundError: + print(f"Map file not found: {map_file}") + return {} + + return dependencies + + +def generate_report( + symbols: List[SymbolInfo], + fastled_symbols: List[SymbolInfo], + large_symbols: List[SymbolInfo], + dependencies: Dict[str, List[str]], +) -> Dict[str, Any]: + """Generate a comprehensive report""" + print("\n" + "=" * 80) + print("ESP32 FASTLED SYMBOL ANALYSIS REPORT") + print("=" * 80) + + # Summary statistics + total_symbols = len(symbols) + total_fastled = len(fastled_symbols) + fastled_size = sum(s.size for s in fastled_symbols) + + print("\nSUMMARY:") + print(f" Total symbols: {total_symbols}") + print(f" FastLED symbols: {total_fastled}") + print(f" Total FastLED size: {fastled_size} bytes ({fastled_size / 1024:.1f} KB)") + + # Largest FastLED symbols + print("\nLARGEST FASTLED SYMBOLS (potential elimination targets):") + fastled_sorted: List[SymbolInfo] = sorted( + fastled_symbols, key=lambda x: x.size, reverse=True + ) + for i, sym in enumerate(fastled_sorted[:20]): + display_name = sym.demangled_name + print(f" {i + 1:2d}. {sym.size:6d} bytes - {display_name}") + if sym.demangled_name != sym.name: + print( + f" (mangled: {sym.name[:80]}{'...' if len(sym.name) > 80 else ''})" + ) + + # FastLED modules analysis + print("\nFASTLED MODULES PULLED IN:") + fastled_modules: List[str] = [ + mod + for mod in dependencies.keys() + if any(kw in mod.lower() for kw in ["fastled", "crgb", "led", "rmt", "strip"]) + ] + + for module in sorted(fastled_modules): + print(f" {module}:") + for symbol in dependencies[module][:5]: # Show first 5 symbols + print(f" - {symbol}") + if len(dependencies[module]) > 5: + print(f" ... and {len(dependencies[module]) - 5} more") + + # Largest overall symbols (non-FastLED) + print("\nLARGEST NON-FASTLED SYMBOLS:") + non_fastled: List[SymbolInfo] = [ + s + for s in large_symbols + if not any( + keyword in s.demangled_name.lower() + for keyword in ["fastled", "cfastled", "crgb", "hsv"] + ) + ] + non_fastled_sorted: List[SymbolInfo] = sorted( + non_fastled, key=lambda x: x.size, reverse=True + ) + + for i, sym in enumerate(non_fastled_sorted[:15]): + display_name = sym.demangled_name + print(f" {i + 1:2d}. {sym.size:6d} bytes - {display_name}") + + # Recommendations + print("\nRECOMMENDATIONS FOR SIZE REDUCTION:") + + # Identify unused features + # unused_features = [] + feature_patterns = { + "JSON functionality": ["json", "Json"], + "Audio processing": ["audio", "fft", "Audio"], + "2D effects": ["2d", "noise", "matrix"], + "Video functionality": ["video", "Video"], + "UI components": ["ui", "button", "slider"], + "File system": ["file", "File", "fs_"], + "Mathematical functions": ["sqrt", "sin", "cos", "math"], + "String processing": ["string", "str", "String"], + } + + for feature, patterns in feature_patterns.items(): + feature_symbols: List[SymbolInfo] = [ + s for s in fastled_symbols if any(p in s.demangled_name for p in patterns) + ] + if feature_symbols: + total_size = sum(s.size for s in feature_symbols) + print(f" - {feature}: {len(feature_symbols)} symbols, {total_size} bytes") + if total_size > 1000: # Only show features with >1KB + print( + f" Consider removing if not needed (could save ~{total_size / 1024:.1f} KB)" + ) + # Show a few example symbols + for sym in feature_symbols[:3]: + display_name = sym.demangled_name[:60] + print(f" * {sym.size} bytes: {display_name}") + if len(feature_symbols) > 3: + print(f" ... and {len(feature_symbols) - 3} more") + + return { + "total_symbols": total_symbols, + "fastled_symbols": total_fastled, + "fastled_size": fastled_size, + "largest_fastled": fastled_sorted[:10], + "dependencies": fastled_modules, + } + + +def main(): + # Detect build directory and board - try multiple possible locations + possible_build_dirs = [ + Path("../../.build"), + Path("../.build"), + Path(".build"), + Path("../../build"), + Path("../build"), + Path("build"), + ] + + build_dir = None + for build_path in possible_build_dirs: + if build_path.exists(): + build_dir = build_path + break + + if not build_dir: + print("Error: Could not find build directory (.build)") + sys.exit(1) + + # Find ESP32 board directory + esp32_boards = [ + "esp32dev", + "esp32", + "esp32s2", + "esp32s3", + "esp32c3", + "esp32c6", + "esp32h2", + "esp32p4", + "esp32c2", + ] + board_dir = None + board_name = None + + for board in esp32_boards: + candidate_dir = build_dir / board + if candidate_dir.exists(): + build_info_file = candidate_dir / "build_info.json" + if build_info_file.exists(): + board_dir = candidate_dir + board_name = board + break + + if not board_dir: + print("Error: No ESP32 board with build_info.json found in build directory") + print(f"Searched in: {build_dir}") + print(f"Looking for boards: {esp32_boards}") + sys.exit(1) + + build_info_path = board_dir / "build_info.json" + print(f"Found ESP32 build info for {board_name}: {build_info_path}") + + with open(build_info_path) as f: + build_info = json.load(f) + + # Use the detected board name instead of hardcoded "esp32dev" + esp32_info = build_info[board_name] + nm_path = esp32_info["aliases"]["nm"] + elf_file = esp32_info["prog_path"] + + # Find map file + map_file = Path(elf_file).with_suffix(".map") + + print(f"Analyzing ELF file: {elf_file}") + print(f"Using nm tool: {nm_path}") + print(f"Map file: {map_file}") + + # Analyze symbols + cppfilt_path = esp32_info["aliases"]["c++filt"] + symbols, fastled_symbols, large_symbols = analyze_symbols( + elf_file, nm_path, cppfilt_path + ) + + # Analyze dependencies + dependencies = analyze_map_file(map_file) + + # Generate report + report = generate_report(symbols, fastled_symbols, large_symbols, dependencies) + + # Save detailed data to JSON (sorted by size, largest first) + # Use board-specific filename and place it relative to build directory + output_file = build_dir / f"{board_name}_symbol_analysis.json" + detailed_data = { + "summary": report, + "all_fastled_symbols": sorted( + fastled_symbols, key=lambda x: x.size, reverse=True + ), + "all_symbols_sorted_by_size": sorted( + symbols, key=lambda x: x.size, reverse=True + )[:100], # Top 100 largest symbols + "dependencies": dependencies, + "large_symbols": sorted(large_symbols, key=lambda x: x.size, reverse=True)[ + :50 + ], # Top 50 largest symbols + } + + with open(output_file, "w") as f: + json.dump(detailed_data, f, indent=2) + + print(f"\nDetailed analysis saved to: {output_file}") + print("You can examine this file to identify specific symbols to eliminate.") + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/util/locked_print.py b/v1.5/lib/FastLED/ci/util/locked_print.py new file mode 100644 index 0000000..1de36e0 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/locked_print.py @@ -0,0 +1,12 @@ +from threading import Lock + + +PRINT_LOCK = Lock() + + +def locked_print(string: str): + """Print with a lock to prevent garbled output for multiple threads.""" + with PRINT_LOCK: + # print only prints so much, break up the string into lines + for line in string.splitlines(): + print(line) diff --git a/v1.5/lib/FastLED/ci/util/map_dump.py b/v1.5/lib/FastLED/ci/util/map_dump.py new file mode 100644 index 0000000..1f57d82 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/map_dump.py @@ -0,0 +1,19 @@ +import os +from pathlib import Path + + +def map_dump(map_file: Path) -> None: + # os.system("uv run fpvgcc ci/tests/uno/firmware.map --lmap root") + + cmds = [ + f"uv run fpvgcc {map_file} --sar", + f"uv run fpvgcc {map_file} --lmap root", + f"uv run fpvgcc {map_file} --uf", + f"uv run fpvgcc {map_file} --uregions", + # --usections + f"uv run fpvgcc {map_file} --usections", + f"uv run fpvgcc {map_file} --la", + ] + for cmd in cmds: + print("\nRunning command: ", cmd) + os.system(cmd) diff --git a/v1.5/lib/FastLED/ci/util/output_formatter.py b/v1.5/lib/FastLED/ci/util/output_formatter.py new file mode 100644 index 0000000..9859b7e --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/output_formatter.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import re +import time +from typing import Protocol + + +class OutputFormatter(Protocol): + """Protocol for output formatters used with RunningProcess.""" + + def begin(self) -> None: ... + + def transform(self, line: str) -> str: ... + + def end(self) -> None: ... + + +class NullOutputFormatter: + """No-op formatter that returns input unchanged and has no lifecycle effects.""" + + def begin(self) -> None: + return None + + def transform(self, line: str) -> str: + return line + + def end(self) -> None: + return None + + +class _MultiPathSubstitutionFormatter: + """Formatter that applies multiple path replacements and prefixes timestamps.""" + + def __init__(self, substitutions: list[tuple[str, str, str]]) -> None: + """Initialize with list of (needle, regex_pattern, replacement) tuples.""" + self._substitutions: list[tuple[str, str, re.Pattern[str]]] = [] + for needle, regex_pattern, replacement in substitutions: + compiled_pattern = re.compile(regex_pattern) + self._substitutions.append((needle, replacement, compiled_pattern)) + self._start_time: float = 0.0 + + def begin(self) -> None: + self._start_time = time.time() + + def transform(self, line: str) -> str: + if not line: + return line + formatted: str = self._format_paths(line) + elapsed: float = time.time() - self._start_time + return f"{elapsed:.2f} {formatted}" + + def end(self) -> None: + return None + + def _format_paths(self, line: str) -> str: + result = line + for needle, replacement, compiled_pattern in self._substitutions: + if needle in result and compiled_pattern.search(result): + result = compiled_pattern.sub(replacement, result) + return result + + +def create_sketch_path_formatter(example: str) -> OutputFormatter: + """Create a formatter that maps lib/FastLED paths to src/ and src/sketch paths to examples/{example}/ and timestamps lines. + + Args: + example: Example name or path (e.g., "Pintest" or "examples/SmartMatrix"). + + Returns: + OutputFormatter: Configured formatter instance. + """ + # Normalize example path + display_example_str: str + if "/" in example or "\\" in example: + display_example_str = example.replace("\\", "/") + else: + display_example_str = f"examples/{example}" + + # Define multiple path substitutions + substitutions = [ + # Replace lib/FastLED/ paths with src/ for better UX + ("lib/FastLED", r"lib[/\\]+FastLED[/\\]+", "src/"), + # Replace src/sketch/ paths with examples/{example}/ + ("sketch", r"src[/\\]+sketch[/\\]+", f"{display_example_str}/"), + ] + + return _MultiPathSubstitutionFormatter(substitutions) diff --git a/v1.5/lib/FastLED/ci/util/paths.py b/v1.5/lib/FastLED/ci/util/paths.py new file mode 100644 index 0000000..0a2b724 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/paths.py @@ -0,0 +1,6 @@ +from pathlib import Path + + +_HERE = Path(__file__).resolve().parent +PROJECT_ROOT = _HERE.parent.parent +BUILD = PROJECT_ROOT / ".build" diff --git a/v1.5/lib/FastLED/ci/util/process_status_display.py b/v1.5/lib/FastLED/ci/util/process_status_display.py new file mode 100644 index 0000000..f984461 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/process_status_display.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +"""Process status display classes for real-time monitoring.""" + +import logging +import threading +import time +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TYPE_CHECKING, List, Optional + + +if TYPE_CHECKING: + from ci.util.running_process_group import GroupStatus, RunningProcessGroup + +logger = logging.getLogger(__name__) + + +@dataclass +class DisplayConfig: + """Configuration for display output format.""" + + format_type: str = "ascii" # "ascii", "rich", "csv" + use_colors: bool = True + update_interval: float = 0.1 + max_name_width: int = 20 + max_output_width: int = 40 + + +class ProcessStatusDisplay(ABC): + """Abstract base class for process status displays.""" + + def __init__( + self, group: "RunningProcessGroup", config: Optional[DisplayConfig] = None + ): + self.group = group + self.config = config or DisplayConfig() + self._display_thread: Optional[threading.Thread] = None + self._stop_event = threading.Event() + + @abstractmethod + def format_status_line( + self, group_status: "GroupStatus", spinner_index: int + ) -> str: + """Format the complete status display.""" + pass + + def start_display(self) -> threading.Thread: + """Start the real-time display in a background thread.""" + if self._display_thread and self._display_thread.is_alive(): + return self._display_thread + + self._stop_event.clear() + self._display_thread = threading.Thread(target=self._display_loop, daemon=True) + self._display_thread.start() + return self._display_thread + + def stop_display(self) -> None: + """Stop the display thread.""" + if self._display_thread: + self._stop_event.set() + self._display_thread.join(timeout=1.0) + + def _display_loop(self) -> None: + """Main display loop running in background thread.""" + spinner_index = 0 + last_status_time = 0 + status_interval = 2 # Show status every 2 seconds + + while not self._stop_event.is_set(): + try: + # Wait for monitoring to start, or exit if stopped + if not self.group._status_monitoring_active: + time.sleep(0.1) + continue + + current_time = time.time() + group_status = self.group.get_status() + + # Show periodic status updates instead of continuous display + if current_time - last_status_time >= status_interval: + running_count = sum(1 for p in group_status.processes if p.is_alive) + completed_count = group_status.completed_processes + + if running_count > 0: + # Show a brief status update + spinner_char = ["|", "/", "-", "\\\\"][spinner_index % 4] + print( + f"{spinner_char} Progress: {completed_count}/{group_status.total_processes} completed, {running_count} running..." + ) + last_status_time = current_time + + spinner_index = (spinner_index + 1) % 4 + time.sleep(1.0) # Check every second but only print every 5 seconds + + except Exception as e: + logger.warning(f"Display update error: {e}") + time.sleep(1.0) + + +class ASCIIStatusDisplay(ProcessStatusDisplay): + """ASCII-compatible status display.""" + + def __init__( + self, group: "RunningProcessGroup", config: Optional[DisplayConfig] = None + ): + if config is None: + config = DisplayConfig(format_type="ascii") + super().__init__(group, config) + + self._spinner_chars = ["|", "/", "-", "\\\\"] + self._status_chars = { + "running": "|>", + "done": "OK", + "failed": "XX", + "pending": "--", + } + + def format_status_line( + self, group_status: "GroupStatus", spinner_index: int + ) -> str: + """Format status display using ASCII characters.""" + lines: List[str] = [] + + # Header + lines.append( + f"Process Group: {group_status.group_name} - Progress: {group_status.completed_processes}/{group_status.total_processes} ({group_status.completion_percentage:.1f}%)" + ) + lines.append("-" * 80) + + # Process status lines + for proc_status in group_status.processes: + # Get status character + if proc_status.is_alive: + status_char = self._spinner_chars[ + spinner_index % len(self._spinner_chars) + ] + elif proc_status.is_completed: + if proc_status.return_value == 0: + status_char = self._status_chars["done"] + else: + status_char = self._status_chars["failed"] + else: + status_char = self._status_chars["pending"] + + # Format fields + name = proc_status.name[: self.config.max_name_width].ljust( + self.config.max_name_width + ) + + if proc_status.is_completed: + status_text = f"DONE({proc_status.return_value or 0})" + elif proc_status.is_alive: + status_text = "RUNNING" + else: + status_text = "PENDING" + + duration = f"{proc_status.running_time_seconds:.1f}s" + output = (proc_status.last_output_line or "")[ + : self.config.max_output_width + ] + + lines.append( + f"{status_char} {name} | {status_text:>8} | {duration:>8} | {output}" + ) + + return "\\n".join(lines) + + +class RichStatusDisplay(ProcessStatusDisplay): + """Rich library-based status display with enhanced formatting.""" + + def __init__( + self, group: "RunningProcessGroup", config: Optional[DisplayConfig] = None + ): + if config is None: + config = DisplayConfig(format_type="rich") + super().__init__(group, config) + + try: + from rich.console import Console + from rich.live import Live + from rich.progress import ( + Progress, + SpinnerColumn, + TextColumn, + TimeElapsedColumn, + ) + from rich.table import Table + + self.Progress = Progress + self.Live = Live + self.Table = Table + self.Console = Console + self._rich_available = True + + self._spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"] + self._status_chars = { + "running": "⠋", + "done": "✓", + "failed": "✗", + "pending": " ", + } + except ImportError: + logger.warning("Rich not available, falling back to ASCII display") + self._rich_available = False + # Fallback to ASCII chars + self._spinner_chars = ["|", "/", "-", "\\\\"] + self._status_chars = { + "running": "|>", + "done": "OK", + "failed": "XX", + "pending": "--", + } + + def format_status_line( + self, group_status: "GroupStatus", spinner_index: int + ) -> str: + """Format status display using Rich library features.""" + if not self._rich_available: + # Fallback to ASCII formatting + return self._format_ascii_fallback(group_status, spinner_index) + + try: + return self._format_rich_display(group_status, spinner_index) + except Exception as e: + logger.warning(f"Rich formatting failed, using ASCII fallback: {e}") + return self._format_ascii_fallback(group_status, spinner_index) + + def _format_rich_display( + self, group_status: "GroupStatus", spinner_index: int + ) -> str: + """Format using Rich library.""" + table = self.Table(title=f"Process Group: {group_status.group_name}") + + table.add_column("Status", width=8) + table.add_column("Process", width=self.config.max_name_width) + table.add_column("State", width=10) + table.add_column("Duration", width=10) + table.add_column("Last Output", width=self.config.max_output_width) + + for proc_status in group_status.processes: + # Get status character + if proc_status.is_alive: + status_char = self._spinner_chars[ + spinner_index % len(self._spinner_chars) + ] + elif proc_status.is_completed: + if proc_status.return_value == 0: + status_char = self._status_chars["done"] + else: + status_char = self._status_chars["failed"] + else: + status_char = self._status_chars["pending"] + + # Format fields + name = proc_status.name[: self.config.max_name_width] + + if proc_status.is_completed: + status_text = f"DONE({proc_status.return_value or 0})" + if proc_status.return_value == 0: + status_text = f"[green]{status_text}[/green]" + else: + status_text = f"[red]{status_text}[/red]" + elif proc_status.is_alive: + status_text = "[yellow]RUNNING[/yellow]" + else: + status_text = "[dim]PENDING[/dim]" + + duration = f"{proc_status.running_time_seconds:.1f}s" + output = (proc_status.last_output_line or "")[ + : self.config.max_output_width + ] + + table.add_row(status_char, name, status_text, duration, output) + + # Render table to string + console = self.Console(width=120, force_terminal=False) + with console.capture() as capture: + console.print(table) + + return capture.get() + + def _format_ascii_fallback( + self, group_status: "GroupStatus", spinner_index: int + ) -> str: + """Fallback ASCII formatting when Rich fails.""" + lines: List[str] = [] + + # Header + lines.append( + f"Process Group: {group_status.group_name} - Progress: {group_status.completed_processes}/{group_status.total_processes} ({group_status.completion_percentage:.1f}%)" + ) + lines.append("-" * 80) + + # Process status lines + for proc_status in group_status.processes: + # Get status character + if proc_status.is_alive: + status_char = self._spinner_chars[ + spinner_index % len(self._spinner_chars) + ] + elif proc_status.is_completed: + if proc_status.return_value == 0: + status_char = self._status_chars["done"] + else: + status_char = self._status_chars["failed"] + else: + status_char = self._status_chars["pending"] + + # Format fields + name = proc_status.name[: self.config.max_name_width].ljust( + self.config.max_name_width + ) + + if proc_status.is_completed: + status_text = f"DONE({proc_status.return_value or 0})" + elif proc_status.is_alive: + status_text = "RUNNING" + else: + status_text = "PENDING" + + duration = f"{proc_status.running_time_seconds:.1f}s" + output = (proc_status.last_output_line or "")[ + : self.config.max_output_width + ] + + lines.append( + f"{status_char} {name} | {status_text:>8} | {duration:>8} | {output}" + ) + + return "\\n".join(lines) + + +def get_display_format() -> DisplayConfig: + """Auto-detect best display format for current environment.""" + try: + # Test Rich availability + import sys + + import rich + + if sys.stdout.encoding and sys.stdout.encoding.lower() in ["utf-8", "utf8"]: + return DisplayConfig(format_type="rich") + except ImportError: + pass + + # Fallback to ASCII + return DisplayConfig(format_type="ascii") + + +def create_status_display( + group: "RunningProcessGroup", display_type: str = "auto" +) -> ProcessStatusDisplay: + """Factory function to create best available display.""" + + if display_type == "auto": + config = get_display_format() + display_type = config.format_type + + if display_type == "rich": + try: + return RichStatusDisplay(group) + except Exception as e: + logger.warning(f"Rich display creation failed, falling back to ASCII: {e}") + return ASCIIStatusDisplay(group) + + # Default to ASCII + return ASCIIStatusDisplay(group) + + +def display_process_status( + group: "RunningProcessGroup", + display_type: str = "auto", + update_interval: float = 0.1, +) -> threading.Thread: + """Convenience function to start real-time process status display.""" + + display = create_status_display(group, display_type) + display.config.update_interval = update_interval + + return display.start_display() diff --git a/v1.5/lib/FastLED/ci/util/resumable_downloader.py b/v1.5/lib/FastLED/ci/util/resumable_downloader.py new file mode 100644 index 0000000..6431676 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/resumable_downloader.py @@ -0,0 +1,135 @@ +"""Resumable HTTP downloader with chunked download support and automatic retry.""" + +import _thread +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Optional + + +class ResumableDownloader: + """Downloader with resume capability for large files.""" + + def __init__(self, chunk_size: int = 8192, max_retries: int = 5): + self.chunk_size = chunk_size + self.max_retries = max_retries + + def download(self, url: str, file_path: Path) -> None: + """Download with resume capability. + + Args: + url: URL to download + file_path: Path where to save the file + """ + import time + import urllib.error + import urllib.request + + # Get the total file size + total_size = self._get_file_size(url) + if total_size is None: + print(f"WARNING: Could not determine file size for {url}") + total_size = 0 + else: + print( + f"File size: {total_size:,} bytes ({total_size / (1024 * 1024):.1f} MB)" + ) + + # Check if partial file exists + start_byte = 0 + if file_path.exists(): + start_byte = file_path.stat().st_size + if start_byte == total_size: + print(f"File already completely downloaded: {file_path}") + return + elif start_byte > 0: + print( + f"Resuming download from byte {start_byte:,} ({start_byte / (1024 * 1024):.1f} MB)" + ) + + retry_count = 0 + while retry_count <= self.max_retries: + try: + self._download_range(url, file_path, start_byte, total_size) + print(f"SUCCESS: Download completed successfully: {file_path}") + return + except (urllib.error.URLError, ConnectionError, OSError) as e: + retry_count += 1 + current_size = file_path.stat().st_size if file_path.exists() else 0 + + if retry_count <= self.max_retries: + wait_time = min(2**retry_count, 30) # Exponential backoff, max 30s + print( + f"\nCONNECTION LOST: At {current_size:,} bytes. Retry {retry_count}/{self.max_retries} in {wait_time}s..." + ) + time.sleep(wait_time) + start_byte = current_size + else: + print(f"\nERROR: Download failed after {self.max_retries} retries") + raise + except KeyboardInterrupt: + print("\nWARNING: Download interrupted by user") + _thread.interrupt_main() + raise + + def _get_file_size(self, url: str) -> Optional[int]: + """Get the total file size via HEAD request.""" + try: + import urllib.request + + req = urllib.request.Request(url, method="HEAD") + with urllib.request.urlopen(req, timeout=30) as response: + content_length = response.headers.get("Content-Length") + return int(content_length) if content_length else None + except KeyboardInterrupt: + _thread.interrupt_main() + raise + except Exception: + return None + + def _download_range( + self, url: str, file_path: Path, start_byte: int, total_size: int + ) -> None: + """Download from start_byte to end of file.""" + import urllib.request + + # Create range request + headers: dict[str, str] = {} + if start_byte > 0: + headers["Range"] = f"bytes={start_byte}-" + + req = urllib.request.Request(url, headers=headers) + + # Open file in append mode if resuming, write mode if starting fresh + mode = "ab" if start_byte > 0 else "wb" + + with urllib.request.urlopen(req, timeout=30) as response: + with open(file_path, mode) as f: + downloaded = start_byte + + while True: + chunk = response.read(self.chunk_size) + if not chunk: + break + + f.write(chunk) + downloaded += len(chunk) + + # Progress reporting + if total_size > 0: + progress = downloaded / total_size * 100 + mb_downloaded = downloaded / (1024 * 1024) + mb_total = total_size / (1024 * 1024) + print( + f"\rProgress: {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", + end="", + flush=True, + ) + else: + mb_downloaded = downloaded / (1024 * 1024) + print( + f"\rDownloaded: {mb_downloaded:.1f} MB", end="", flush=True + ) + + print() # New line after progress diff --git a/v1.5/lib/FastLED/ci/util/running_process.py b/v1.5/lib/FastLED/ci/util/running_process.py new file mode 100644 index 0000000..0743e5b --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/running_process.py @@ -0,0 +1,992 @@ +# pyright: reportUnknownMemberType=false, reportMissingParameterType=false +import _thread +import os +import queue +import shlex +import subprocess +import sys +import tempfile +import threading +import time +import traceback +import warnings +from pathlib import Path +from queue import Queue +from typing import Any, Callable, ContextManager, Iterator + + +class EndOfStream(Exception): + """Sentinel used to indicate end-of-stream from the reader.""" + + +from ci.util.output_formatter import NullOutputFormatter, OutputFormatter + + +# Console UTF-8 configuration is now handled globally in ci/__init__.py + + +class ProcessOutputReader: + """Dedicated reader that drains a process's stdout and enqueues lines. + + This keeps the stdout pipe drained to prevent blocking and forwards + transformed, non-empty lines to the provided output queue. It also invokes + lifecycle callbacks for timing/unregister behaviors. + """ + + def __init__( + self, + proc: subprocess.Popen[Any], + shutdown: threading.Event, + output_formatter: OutputFormatter | None, + on_output: Callable[[str | EndOfStream], None], + on_end: Callable[[], None], + ) -> None: + output_formatter = output_formatter or NullOutputFormatter() + self._proc = proc + self._shutdown = shutdown + self._output_formatter = output_formatter + self._on_output = on_output + self._on_end = on_end + self.last_stdout_ts: float | None = None + self._eos_emitted: bool = False + + def _emit_eos_once(self) -> None: + """Ensure EndOfStream is only forwarded a single time.""" + if not self._eos_emitted: + self._eos_emitted = True + self._on_output(EndOfStream()) + + def run(self) -> None: + """Continuously read stdout lines and forward them until EOF or shutdown.""" + try: + # Begin formatter lifecycle within the reader context + try: + self._output_formatter.begin() + except Exception as e: + warnings.warn(f"Output formatter begin() failed: {e}") + + assert self._proc.stdout is not None + + try: + for line in self._proc.stdout: + self.last_stdout_ts = time.time() + if self._shutdown.is_set(): + break + + line_stripped = line.rstrip() + if not line_stripped: + continue + + transformed_line = self._output_formatter.transform(line_stripped) + + self._on_output(transformed_line) + + except KeyboardInterrupt: + # Per project rules, handle interrupts in threads explicitly + thread_id = threading.current_thread().ident + thread_name = threading.current_thread().name + print(f"Thread {thread_id} ({thread_name}) caught KeyboardInterrupt") + print(f"Stack trace for thread {thread_id}:") + traceback.print_exc() + # Try to ensure child process is terminated promptly + try: + self._proc.kill() + except Exception: + pass + # Propagate to main thread and re-raise + _thread.interrupt_main() + # EOF + self._emit_eos_once() + raise + + except (ValueError, OSError) as e: + # Normal shutdown scenarios include closed file descriptors. + if "closed file" in str(e) or "Bad file descriptor" in str(e): + warnings.warn(f"Output reader encountered closed file: {e}") + pass + else: + print(f"Warning: Output reader encountered error: {e}") + finally: + # Signal end-of-stream to consumers exactly once + self._emit_eos_once() + finally: + # Cleanup stream and invoke completion callback + if self._proc.stdout and not self._proc.stdout.closed: + try: + self._proc.stdout.close() + except (ValueError, OSError) as err: + warnings.warn(f"Output reader encountered error: {err}") + pass + + # Notify parent for timing/unregistration + try: + self._on_end() + finally: + # End formatter lifecycle within the reader context + try: + self._output_formatter.end() + except Exception as e: + warnings.warn(f"Output formatter end() failed: {e}") + + +class ProcessWatcher: + """Background watcher that polls a process until it terminates.""" + + def __init__(self, running_process: "RunningProcess") -> None: + self._rp = running_process + self._thread: threading.Thread | None = None + + def start(self) -> None: + name: str = "RPWatcher" + try: + if self._rp.proc is not None and self._rp.proc.pid is not None: + name = f"RPWatcher-{self._rp.proc.pid}" + except Exception: + pass + + self._thread = threading.Thread(target=self._run, name=name, daemon=True) + self._thread.start() + + def _run(self) -> None: + thread_id = threading.current_thread().ident + thread_name = threading.current_thread().name + try: + while not self._rp.shutdown.is_set(): + # Enforce per-process timeout independently of wait() + if ( + self._rp.timeout is not None + and self._rp.start_time is not None + and (time.time() - self._rp.start_time) > self._rp.timeout + ): + print( + f"Process timeout after {self._rp.timeout} seconds (watcher), killing: {self._rp.command}" + ) + if self._rp.enable_stack_trace: + try: + print("\n" + "=" * 80) + print("STACK TRACE DUMP (GDB Output)") + print("=" * 80) + print(self._rp._dump_stack_trace()) + print("=" * 80) + except Exception as e: + print(f"Watcher stack trace dump failed: {e}") + self._rp.kill() + break + + rc: int | None = self._rp.poll() + if rc is not None: + break + time.sleep(0.1) + except KeyboardInterrupt: + print(f"Thread {thread_id} ({thread_name}) caught KeyboardInterrupt") + print(f"Stack trace for thread {thread_id}:") + traceback.print_exc() + _thread.interrupt_main() + raise + except Exception as e: + # Surface unexpected errors and keep behavior consistent + print(f"Watcher thread error in {thread_name}: {e}") + traceback.print_exc() + + @property + def thread(self) -> threading.Thread | None: + return self._thread + + +class _RunningProcessLineIterator(ContextManager[Iterator[str]], Iterator[str]): + """Context-managed iterator over a RunningProcess's output lines. + + Yields only strings (never None). Stops on EndOfStream or when a per-line + timeout elapses. + """ + + def __init__(self, rp: "RunningProcess", timeout: float | None) -> None: + self._rp = rp + self._timeout = timeout + + # Context manager protocol + def __enter__(self) -> "_RunningProcessLineIterator": + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: Any | None, + ) -> bool: + # Do not suppress exceptions + return False + + # Iterator protocol + def __iter__(self) -> Iterator[str]: + return self + + def __next__(self) -> str: + next_item: str | EndOfStream = self._rp.get_next_line(timeout=self._timeout) + + if isinstance(next_item, EndOfStream): + raise StopIteration + + # Must be a string by contract + return next_item + + +class RunningProcess: + """ + A class to manage and stream output from a running subprocess. + + This class provides functionality to execute shell commands, stream their output + in real-time via a queue, and control the subprocess execution. It merges stderr + into stdout and provides thread-safe access to process output. + + Key features: + - Real-time output streaming via queue + - Thread-safe output consumption + - Timeout protection with optional stack traces + - Echo mode for immediate output printing + - Process tree termination support + """ + + def __init__( + self, + command: str | list[str], + cwd: Path | None = None, + check: bool = False, + auto_run: bool = True, + shell: bool | None = None, + timeout: int | None = None, # None means no global timeout + enable_stack_trace: bool = False, # Enable stack trace dumping on timeout + on_complete: Callable[[], None] + | None = None, # Callback to execute when process completes + output_formatter: OutputFormatter | None = None, + env: dict[str, str] | None = None, # Environment variables + ): + """ + Initialize the RunningProcess instance. + + Note: stderr is automatically merged into stdout for unified output handling. + + Args: + command: The command to execute as string or list of arguments. + cwd: Working directory to execute the command in. + check: If True, raise CalledProcessError if command returns non-zero exit code. + auto_run: If True, automatically start the command when instance is created. + shell: Shell execution mode. None auto-detects based on command type. + timeout: Global timeout in seconds for process execution. None disables timeout. + enable_stack_trace: If True, dump GDB stack trace when process times out. + on_complete: Callback function executed when process completes normally. + output_formatter: Optional formatter for transforming output lines. + env: Environment variables to pass to the subprocess. None uses current environment. + """ + if shell is None: + # Default: use shell only when given a string, or when a list includes shell metachars + if isinstance(command, str): + shell = True + elif isinstance(command, list): + shell_meta = {"&&", "||", "|", ";", ">", "<", "2>", "&"} + shell = any(part in shell_meta for part in command) + else: + shell = False + self.command = command + self.shell: bool = shell + self.cwd = str(cwd) if cwd is not None else None + self.env = env + self.output_queue: Queue[str | EndOfStream] = Queue() + self.accumulated_output: list[str] = [] # Store all output for later retrieval + self.proc: subprocess.Popen[Any] | None = None + self.check = check + # Force auto_run to False if NO_PARALLEL is set + self.auto_run = False if os.environ.get("NO_PARALLEL") else auto_run + self.timeout = timeout + self.enable_stack_trace = enable_stack_trace + self.on_complete = on_complete + # Always keep a non-None formatter + self.output_formatter = ( + output_formatter if output_formatter is not None else NullOutputFormatter() + ) + self.reader_thread: threading.Thread | None = None + self.watcher_thread: threading.Thread | None = None + self.shutdown: threading.Event = threading.Event() + self._start_time: float | None = None + self._end_time: float | None = None + self._time_last_stdout_line: float | None = None + self._termination_notified: bool = False + if auto_run: + self.run() + + def get_command_str(self) -> str: + return ( + subprocess.list2cmdline(self.command) + if isinstance(self.command, list) + else self.command + ) + + def _dump_stack_trace(self) -> str: + """ + Dump stack trace of the running process using GDB. + + Returns: + str: GDB output containing stack trace information. + """ + if self.proc is None: + return "No process to dump stack trace for." + + try: + # Get the process ID + pid = self.proc.pid + + # Create GDB script for attaching to running process + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as gdb_script: + gdb_script.write("set pagination off\n") + gdb_script.write(f"attach {pid}\n") + gdb_script.write("bt full\n") + gdb_script.write("info registers\n") + gdb_script.write("x/16i $pc\n") + gdb_script.write("thread apply all bt full\n") + gdb_script.write("detach\n") + gdb_script.write("quit\n") + + # Run GDB to get stack trace + gdb_command = f"gdb -batch -x {gdb_script.name}" + gdb_process = subprocess.Popen( + gdb_command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, + text=True, + ) + + gdb_output, _ = gdb_process.communicate( + timeout=30 + ) # 30 second timeout for GDB + + # Clean up GDB script + os.unlink(gdb_script.name) + + return gdb_output + + except Exception as e: + return f"Failed to dump stack trace: {e}" + + def time_last_stdout_line(self) -> float | None: + return self._time_last_stdout_line + + def _handle_timeout(self, timeout: float, echo: bool = False) -> None: + """Handle process timeout with optional stack trace and cleanup.""" + cmd_str = self.get_command_str() + + # Drain any remaining output before killing if echo is enabled + if echo: + remaining_lines = self.drain_stdout() + for line in remaining_lines: + print( + line, flush=(os.name == "nt") + ) # Force flush only on Windows per-line + if remaining_lines: + print( + f"[Drained {len(remaining_lines)} final lines before timeout]", + flush=(os.name == "nt"), + ) + + if self.enable_stack_trace: + print(f"\nProcess timeout after {timeout} seconds, dumping stack trace...") + print(f"Command: {cmd_str}") + print(f"Process ID: {self.proc.pid}") + + try: + stack_trace = self._dump_stack_trace() + print("\n" + "=" * 80) + print("STACK TRACE DUMP (GDB Output)") + print("=" * 80) + print(stack_trace) + print("=" * 80) + except Exception as e: + print(f"Failed to dump stack trace: {e}") + + print(f"Killing timed out process: {cmd_str}") + self.kill() + raise TimeoutError(f"Process timed out after {timeout} seconds: {cmd_str}") + + def drain_stdout(self) -> list[str]: + """ + Drain all currently pending stdout lines without blocking. + + Consumes all available lines from the output queue until either the queue + is empty or EndOfStream is encountered. The EndOfStream sentinel is preserved + by get_next_line() for other callers. + + Returns: + List of output lines that were available. Empty list if no output pending. + """ + lines: list[str] = [] + + while True: + try: + line = self.get_next_line(timeout=0) + if isinstance(line, EndOfStream): + break # get_next_line already handled EndOfStream preservation + lines.append(line) + except TimeoutError: + break # Queue is empty + + return lines + + def has_pending_output(self) -> bool: + """ + Check if there are pending output lines without consuming them. + + Returns: + True if output lines are available in the queue, False otherwise. + Returns False if only EndOfStream sentinel is present. + """ + try: + with self.output_queue.mutex: + if len(self.output_queue.queue) == 0: + return False + # If the only item is EndOfStream, no actual output is pending + if len(self.output_queue.queue) == 1 and isinstance( + self.output_queue.queue[0], EndOfStream + ): + return False + return True + except Exception: + return False + + def run(self) -> None: + """ + Execute the command and stream its output to the queue. + + Raises: + subprocess.CalledProcessError: If the command returns a non-zero exit code. + """ + assert self.proc is None + shell = self.shell + popen_command: str | list[str] + if shell and isinstance(self.command, list): + # Convert list to a single shell string with proper quoting + popen_command = subprocess.list2cmdline(self.command) + else: + popen_command = self.command + + self.proc = subprocess.Popen( + popen_command, + shell=shell, + cwd=self.cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, # Use text mode + encoding="utf-8", # Explicitly use UTF-8 + errors="replace", # Replace invalid chars instead of failing + env=self.env, # Use provided environment variables + ) + + # Track start time after process is successfully created + # This excludes process creation overhead from timing measurements + self._start_time = time.time() + + # Register with global process manager + try: + from ci.util.running_process_manager import ( + RunningProcessManagerSingleton, + ) + + RunningProcessManagerSingleton.register(self) + except Exception as e: + warnings.warn(f"RunningProcessManager.register failed: {e}") + + # Output formatter lifecycle is managed by ProcessOutputReader + + # Prepare output reader helper + assert self.proc is not None + + def _on_reader_end() -> None: + # Set end time when stdout pumper finishes; captures completion time of useful output + if self._end_time is None: + self._end_time = time.time() + # Unregister when stdout is fully drained + try: + self._notify_terminated() + except Exception as e: + warnings.warn(f"RunningProcess termination notify (drain) failed: {e}") + + def _on_output(item: str | EndOfStream) -> None: + # Forward to queue and capture text lines for accumulated output + if isinstance(item, EndOfStream): + self.output_queue.put(item) + else: + # Track time of last stdout line observed + self._time_last_stdout_line = time.time() + self.output_queue.put(item) + self.accumulated_output.append(item) + + reader = ProcessOutputReader( + proc=self.proc, + shutdown=self.shutdown, + output_formatter=self.output_formatter, + on_output=_on_output, + on_end=_on_reader_end, + ) + + # Start output reader thread + self.reader_thread = threading.Thread(target=reader.run, daemon=True) + self.reader_thread.start() + + # Start watcher thread via helper class and expose thread for compatibility + self._watcher = ProcessWatcher(self) + self._watcher.start() + self.watcher_thread = self._watcher.thread + + def get_next_line(self, timeout: float | None = None) -> str | EndOfStream: + """ + Get the next line of output from the process. + + Args: + timeout: How long to wait for the next line in seconds. + None means wait forever, 0 means don't wait. + + Returns: + str: The next line of output if available. + EndOfStream: Process has finished, no more output will be available. + + Raises: + TimeoutError: If timeout is reached before a line becomes available. + """ + assert self.proc is not None + + # Fast non-blocking path: honor timeout==0 by peeking before raising + if timeout == 0: + # Peek EOS without consuming + with self.output_queue.mutex: + if len(self.output_queue.queue) > 0: + head = self.output_queue.queue[0] + if isinstance(head, EndOfStream): + return EndOfStream() + # Try immediate get + try: + item_nb: str | EndOfStream = self.output_queue.get_nowait() + if isinstance(item_nb, EndOfStream): + with self.output_queue.mutex: + self.output_queue.queue.appendleft(item_nb) + return EndOfStream() + return item_nb + except queue.Empty: + if self.finished: + return EndOfStream() + raise TimeoutError("Timeout after 0 seconds") + + expired_time = time.time() + timeout if timeout is not None else None + + while True: + if expired_time is not None and time.time() > expired_time: + raise TimeoutError(f"Timeout after {timeout} seconds") + + # Peek without popping if EndOfStream is at the front + with self.output_queue.mutex: + if len(self.output_queue.queue) > 0: + head = self.output_queue.queue[0] + if isinstance(head, EndOfStream): + return EndOfStream() + + # Nothing available yet; wait briefly in blocking mode + if self.output_queue.empty(): + time.sleep(0.01) + if self.finished and self.output_queue.empty(): + return EndOfStream() + continue + + try: + # Safe to pop now; head is not EndOfStream + item: str | EndOfStream = self.output_queue.get(timeout=0.1) + if isinstance(item, EndOfStream): + # In rare race conditions, EndOfStream could appear after peek; put back for other callers + with self.output_queue.mutex: + self.output_queue.queue.appendleft(item) + return EndOfStream() + return item + except queue.Empty: + if self.finished: + return EndOfStream() + continue + + def get_next_line_non_blocking(self) -> str | None | EndOfStream: + """ + Get the next line of output from the process without blocking. + + Returns: + str: Next line of output if available + None: No output available right now (should continue polling) + EndOfStream: Process has finished, no more output will be available + """ + try: + line: str | EndOfStream = self.get_next_line(timeout=0) + return line # get_next_line already handled EndOfStream preservation + except TimeoutError: + # Check if process finished while we were waiting + if self.finished: + return EndOfStream() + return None + + def poll(self) -> int | None: + """ + Check the return code of the process. + """ + if self.proc is None: + return None + rc = self.proc.poll() + if rc is not None: + # Ensure unregistration only happens once + try: + self._notify_terminated() + except Exception as e: + warnings.warn(f"RunningProcess termination notify (poll) failed: {e}") + return rc + + @property + def finished(self) -> bool: + return self.poll() is not None + + def wait(self, echo: bool = False, timeout: float | None = None) -> int: + """ + Wait for the process to complete with timeout protection. + + When echo=True, continuously drains and prints stdout lines while waiting. + Performs final output drain after process completion and thread cleanup. + + Args: + echo: If True, continuously print stdout lines as they become available. + timeout: Overall timeout in seconds. If None, uses instance timeout. + If both are None, waits indefinitely. + + Returns: + Process exit code. + + Raises: + ValueError: If the process hasn't been started. + TimeoutError: If the process exceeds the timeout duration. + """ + if self.proc is None: + raise ValueError("Process is not running.") + + # Determine effective timeout: parameter > instance > none + effective_timeout = timeout if timeout is not None else self.timeout + + # Use a timeout to prevent hanging + start_time = time.time() + + while self.poll() is None: + # Check overall timeout + if ( + effective_timeout is not None + and (time.time() - start_time) > effective_timeout + ): + self._handle_timeout(effective_timeout, echo=echo) + + # Echo: drain all available output, then sleep + if echo: + lines = self.drain_stdout() + if lines: + for line in lines: + # Use print flush=True for Windows compatibility, avoid separate flush calls + print( + line, flush=(os.name == "nt") + ) # Force flush only on Windows per-line + # Additional flush for Unix systems for better performance + if os.name != "nt": + sys.stdout.flush() + continue # Check for more output immediately + + time.sleep(0.01) # Check every 10ms + + # Process completed - drain any remaining output if echo is enabled + if echo: + remaining_lines = self.drain_stdout() + for line in remaining_lines: + print( + line, flush=(os.name == "nt") + ) # Force flush only on Windows per-line + if remaining_lines: + print( + f"[Drained {len(remaining_lines)} final lines after completion]", + flush=(os.name == "nt"), + ) + + # Process has completed, get return code + assert self.proc is not None # For type checker + rtn = self.proc.returncode + assert rtn is not None # Process has completed, so returncode exists + + is_keyboard_interrupt = (rtn == -11) or (rtn == 3221225786) + if is_keyboard_interrupt: + import _thread + + print("Keyboard interrupt detected, interrupting main thread") + _thread.interrupt_main() + return 1 + + # Record end time only if not already set by output reader + # The output reader sets end time when stdout pumper finishes, which is more accurate + if self._end_time is None: + self._end_time = time.time() + + # Wait for reader thread to finish and cleanup + if self.reader_thread is not None: + self.reader_thread.join( + timeout=0.05 + ) # 50ms should be plenty for thread cleanup + if self.reader_thread.is_alive(): + # Reader thread didn't finish, force shutdown + self.shutdown.set() + self.reader_thread.join(timeout=0.05) # 50ms for forced shutdown + + # Final drain after reader threads shut down - catch any remaining queued output + if echo: + final_lines = self.drain_stdout() + for line in final_lines: + print( + line, flush=(os.name == "nt") + ) # Force flush only on Windows per-line + + # Execute completion callback if provided + if self.on_complete is not None: + try: + self.on_complete() + except Exception as e: + print(f"Warning: on_complete callback failed: {e}") + + # Output formatter end is handled by ProcessOutputReader + + # Unregister from global process manager on normal completion + try: + self._notify_terminated() + except Exception as e: + warnings.warn(f"RunningProcess termination notify (wait) failed: {e}") + + return rtn + + def kill(self) -> None: + """ + Immediately terminate the process and all child processes. + + Signals reader threads to shutdown, kills the entire process tree to prevent + orphaned processes, and waits for thread cleanup. Safe to call multiple times. + + Note: Does not raise if process is already terminated or was never started. + """ + if self.proc is None: + return + + # Record end time when killed (only if not already set by output reader) + if self._end_time is None: + self._end_time = time.time() + + # Signal reader thread to stop + self.shutdown.set() + + # Kill the entire process tree (parent + all children) + # This prevents orphaned clang++ processes from hanging the system + try: + from ci.util.test_env import kill_process_tree + + kill_process_tree(self.proc.pid) + except KeyboardInterrupt: + print("Keyboard interrupt detected, interrupting main thread") + _thread.interrupt_main() + try: + self.proc.kill() + except (ProcessLookupError, PermissionError, OSError, ValueError) as e: + print(f"Warning: Failed to kill process tree for {self.proc.pid}: {e}") + pass + raise + except Exception as e: + # Fallback to simple kill if tree kill fails + print(f"Warning: Failed to kill process tree for {self.proc.pid}: {e}") + try: + self.proc.kill() + except (ProcessLookupError, PermissionError, OSError, ValueError): + pass # Process might already be dead + + # Wait for reader thread to finish + if self.reader_thread is not None: + self.reader_thread.join(timeout=0.05) # 50ms should be plenty for cleanup + + # # Drain any remaining output + # while True: + # try: + # line = self.output_queue.get_nowait() + # if line is None: # End of output marker + # break + # except queue.Empty: + # break + + # Ensure unregistration even on forced kill + try: + from ci.util.running_process_manager import ( + RunningProcessManagerSingleton, + ) + + RunningProcessManagerSingleton.unregister(self) + except Exception as e: + warnings.warn(f"RunningProcessManager.unregister (kill) failed: {e}") + + def _notify_terminated(self) -> None: + """Idempotent notification that the process has terminated. + + Ensures unregister is called only once across multiple termination paths + (poll, wait, stdout drain, watcher thread) and records end time when + available. + """ + if self._termination_notified: + return + self._termination_notified = True + + # Record end time only if not already set + if self._end_time is None: + self._end_time = time.time() + + try: + from ci.util.running_process_manager import RunningProcessManagerSingleton + + RunningProcessManagerSingleton.unregister(self) + except Exception as e: + warnings.warn(f"RunningProcessManager.unregister notify failed: {e}") + + def terminate(self) -> None: + """ + Gracefully terminate the process with SIGTERM. + + Raises: + ValueError: If the process hasn't been started. + """ + if self.proc is None: + raise ValueError("Process is not running.") + self.shutdown.set() + self.proc.terminate() + + @property + def returncode(self) -> int | None: + if self.proc is None: + return None + return self.proc.returncode + + @property + def start_time(self) -> float | None: + """Get the process start time""" + return self._start_time + + @property + def end_time(self) -> float | None: + """Get the process end time""" + return self._end_time + + @property + def duration(self) -> float | None: + """Get the process duration in seconds, or None if not completed""" + if self._start_time is None or self._end_time is None: + return None + return self._end_time - self._start_time + + @property + def stdout(self) -> str: + """ + Get the complete stdout output accumulated so far. + + Returns all output lines that have been processed by the reader thread, + joined with newlines. Available even while process is still running. + + Returns: + Complete stdout output as a string. Empty string if no output yet. + """ + # Return accumulated output (available even if process is still running) + return "\n".join(self.accumulated_output) + + def line_iter(self, timeout: float | None) -> _RunningProcessLineIterator: + """Return a context-managed iterator over output lines. + + Args: + timeout: Per-line timeout in seconds. None waits indefinitely for each line. + + Returns: + A context-managed iterator yielding non-empty, transformed stdout lines. + """ + return _RunningProcessLineIterator(self, timeout) + + +# NOTE: RunningProcessManager and its singleton live in ci/util/running_process_manager.py + + +def subprocess_run( + command: str | list[str], + cwd: Path | None, + check: bool, + timeout: int, + enable_stack_trace: bool, +) -> subprocess.CompletedProcess[str]: + """ + Execute a command with robust stdout handling, emulating subprocess.run(). + + Uses RunningProcess as the backend to provide: + - Continuous stdout streaming to prevent pipe blocking + - Merged stderr into stdout for unified output + - Timeout protection with optional stack trace dumping + - Standard subprocess.CompletedProcess return value + + Args: + command: Command to execute as string or list of arguments. + cwd: Working directory for command execution. Required parameter. + check: If True, raise CalledProcessError for non-zero exit codes. + timeout: Maximum execution time in seconds. + enable_stack_trace: Enable GDB stack trace dumping on timeout. + + Returns: + CompletedProcess with combined stdout and process return code. + stderr field is None since it's merged into stdout. + + Raises: + RuntimeError: If process times out (wraps TimeoutError). + CalledProcessError: If check=True and process exits with non-zero code. + """ + # Use RunningProcess for robust stdout pumping with merged stderr + proc = RunningProcess( + command=command, + cwd=cwd, + check=False, + auto_run=True, + timeout=timeout, + enable_stack_trace=enable_stack_trace, + on_complete=None, + output_formatter=None, + ) + + try: + return_code: int = proc.wait() + except KeyboardInterrupt: + # Propagate interrupt behavior consistent with subprocess.run + raise + except TimeoutError as e: + # Align with subprocess.TimeoutExpired semantics by raising a CalledProcessError-like + # error with available output. Using TimeoutError here is consistent with internal RP. + completed_output: str = proc.stdout + raise RuntimeError( + f"CRITICAL: Process timed out after {timeout} seconds: {command}" + ) from e + + combined_stdout: str = proc.stdout + + # Construct CompletedProcess (stderr is merged into stdout by design) + completed = subprocess.CompletedProcess( + args=command, + returncode=return_code, + stdout=combined_stdout, + stderr=None, + ) + + if check and return_code != 0: + # Raise the standard exception with captured output + raise subprocess.CalledProcessError( + returncode=return_code, + cmd=command, + output=combined_stdout, + stderr=None, + ) + + return completed diff --git a/v1.5/lib/FastLED/ci/util/running_process_group.py b/v1.5/lib/FastLED/ci/util/running_process_group.py new file mode 100644 index 0000000..5c1b22a --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/running_process_group.py @@ -0,0 +1,689 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false +"""RunningProcessGroup - Unified process execution management for FastLED CI.""" + +import os +import subprocess +import sys +import threading +import time +import traceback +from dataclasses import dataclass +from datetime import datetime, timedelta +from enum import Enum +from typing import Dict, List, Optional, cast + +from ci.util.running_process import RunningProcess +from ci.util.test_exceptions import ( + TestExecutionFailedException, + TestFailureInfo, + TestTimeoutException, +) +from ci.util.test_runner import ( + _GLOBAL_TIMEOUT, + MAX_FAILURES_BEFORE_ABORT, + ProcessOutputHandler, + ProcessStuckMonitor, + ProcessTiming, + StuckProcessSignal, + _extract_test_name, + _get_friendly_test_name, + _handle_stuck_processes, + extract_error_snippet, +) + + +class ExecutionMode(Enum): + """Execution mode for process groups.""" + + PARALLEL = "parallel" + SEQUENTIAL = "sequential" + SEQUENTIAL_WITH_DEPENDENCIES = "sequential_with_dependencies" + + +@dataclass +class ProcessStatus: + """Real-time status information for a running process.""" + + name: str + is_alive: bool + is_completed: bool + start_time: datetime + running_duration: timedelta + last_output_line: Optional[str] = None + return_value: Optional[int] = None + + @property + def running_time_seconds(self) -> float: + """Get running duration in seconds for display.""" + return self.running_duration.total_seconds() + + +@dataclass +class GroupStatus: + """Status information for all processes in a group.""" + + group_name: str + processes: List[ProcessStatus] + total_processes: int + completed_processes: int + failed_processes: int + + @property + def completion_percentage(self) -> float: + """Percentage of processes completed.""" + if self.total_processes == 0: + return 100.0 + return (self.completed_processes / self.total_processes) * 100.0 + + +@dataclass +class ProcessExecutionConfig: + """Configuration for how a process group should execute.""" + + execution_mode: ExecutionMode = ExecutionMode.PARALLEL + timeout_seconds: Optional[int] = None + max_failures_before_abort: int = MAX_FAILURES_BEFORE_ABORT + verbose: bool = False + enable_stuck_detection: bool = True + stuck_timeout_seconds: int = _GLOBAL_TIMEOUT + # Real-time display options + display_type: str = "auto" # "auto", "rich", "textual", "ascii" + live_updates: bool = True + update_interval: float = 0.1 + + +class RunningProcessGroup: + """Manages execution of a group of RunningProcess instances.""" + + def __init__( + self, + processes: Optional[List[RunningProcess]] = None, + config: Optional[ProcessExecutionConfig] = None, + name: str = "ProcessGroup", + ): + """Initialize a new process group. + + Args: + processes: List of RunningProcess instances to manage + config: Configuration for execution behavior + name: Name for this process group (used for logging) + """ + self.processes = processes or [] + self.config = config or ProcessExecutionConfig() + self.name = name + self._dependencies: Dict[RunningProcess, List[RunningProcess]] = {} + + # Status tracking + self._process_start_times: Dict[RunningProcess, datetime] = {} + self._process_last_output: Dict[RunningProcess, str] = {} + self._status_monitoring_active: bool = False + + def add_process(self, process: RunningProcess) -> None: + """Add a process to the group. + + Args: + process: RunningProcess instance to add + """ + if process not in self.processes: + self.processes.append(process) + + def add_dependency( + self, process: RunningProcess, depends_on: RunningProcess + ) -> None: + """Add a dependency relationship between processes. + + Args: + process: The dependent process + depends_on: The process that must complete first + """ + if process not in self.processes: + self.add_process(process) + if depends_on not in self.processes: + self.add_process(depends_on) + + if process not in self._dependencies: + self._dependencies[process] = [] + self._dependencies[process].append(depends_on) + + def add_sequential_chain(self, processes: List[RunningProcess]) -> None: + """Add processes that must run in sequence. + + Args: + processes: List of processes to run sequentially + """ + for process in processes: + self.add_process(process) + + # Create dependency chain + for i in range(1, len(processes)): + self.add_dependency(processes[i], processes[i - 1]) + + def run(self) -> List[ProcessTiming]: + """Execute all processes according to the configuration. + + Returns: + List of ProcessTiming objects with execution results + """ + if not self.processes: + return [] + + print(f"Running process group: {self.name}") + + if self.config.execution_mode == ExecutionMode.PARALLEL: + return self._run_parallel() + elif self.config.execution_mode == ExecutionMode.SEQUENTIAL: + return self._run_sequential() + elif self.config.execution_mode == ExecutionMode.SEQUENTIAL_WITH_DEPENDENCIES: + return self._run_with_dependencies() + else: + raise ValueError(f"Unknown execution mode: {self.config.execution_mode}") + + def _run_parallel(self) -> List[ProcessTiming]: + """Execute processes in parallel (based on test_runner._run_processes_parallel).""" + if not self.processes: + return [] + + # Create a shared output handler for formatting + output_handler = ProcessOutputHandler(verbose=self.config.verbose) + + # Configure Windows console for UTF-8 output if needed + if os.name == "nt": # Windows + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore + + # Track start times and enable status monitoring + self._status_monitoring_active = True + for proc in self.processes: + self._track_process_start(proc) + + # Start processes that aren't already running + for proc in self.processes: + cmd_str = proc.get_command_str() + if proc.proc is None: # Only start if not already running + proc.run() + print(f"Started: {cmd_str}") + else: + print(f"Process already running: {cmd_str}") + + # Monitor all processes for output and completion + active_processes = self.processes.copy() + start_time = time.time() + + runner_timeouts: list[int] = [ + p.timeout for p in self.processes if p.timeout is not None + ] + global_timeout: int | None = self.config.timeout_seconds + if global_timeout is None and runner_timeouts: + global_timeout = max(runner_timeouts) + 60 # Add 1 minute buffer + + # Track last activity time for each process to detect stuck processes + last_activity_time = {proc: time.time() for proc in active_processes} + stuck_process_timeout = self.config.stuck_timeout_seconds + + # Track failed processes for proper error reporting + failed_processes: list[str] = [] # Processes killed due to timeout/stuck + exit_failed_processes: list[ + tuple[RunningProcess, int] + ] = [] # Processes that failed with non-zero exit code + + # Track completed processes for timing summary + completed_timings: List[ProcessTiming] = [] + + # Create thread-based stuck process monitor if enabled + stuck_monitor = None + if self.config.enable_stuck_detection: + stuck_monitor = ProcessStuckMonitor(stuck_process_timeout) + + try: + # Start monitoring threads for each process + if stuck_monitor: + for proc in active_processes: + stuck_monitor.start_monitoring(proc) + + def time_expired() -> bool: + if global_timeout is None: + return False + return time.time() - start_time > global_timeout + + while active_processes: + # Check global timeout + if time_expired(): + print(f"\nGlobal timeout reached after {global_timeout} seconds") + print("\033[91m###### ERROR ######\033[0m") + print("Tests failed due to global timeout") + failures: list[TestFailureInfo] = [] + for p in active_processes: + failed_processes.append( + subprocess.list2cmdline(p.command) + ) # Track all active processes as failed + p.kill() + failures.append( + TestFailureInfo( + test_name=_extract_test_name(p.command), + command=str(p.command), + return_code=1, + output="Process killed due to global timeout", + error_type="global_timeout", + ) + ) + raise TestTimeoutException("Global timeout reached", failures) + + # Check for stuck processes (using threaded monitoring) + if stuck_monitor: + stuck_signals = stuck_monitor.check_for_stuck_processes() + if stuck_signals: + self._handle_stuck_processes( + stuck_signals, + active_processes, + failed_processes, + stuck_monitor, + ) + + # Early abort if failure threshold reached via stuck processes + if ( + len(exit_failed_processes) + len(failed_processes) + ) >= self.config.max_failures_before_abort: + print( + f"\nExceeded failure threshold ({self.config.max_failures_before_abort}). Aborting remaining tests." + ) + # Kill any remaining active processes + for p in active_processes: + p.kill() + # Build detailed failures + failures = self._build_failure_list( + exit_failed_processes, failed_processes + ) + raise TestExecutionFailedException( + "Exceeded failure threshold", failures + ) + + # Process each active test individually + any_activity = self._process_active_tests( + active_processes, + exit_failed_processes, + failed_processes, + completed_timings, + stuck_monitor, + ) + + # Brief sleep to avoid spinning if no activity + if not any_activity: + time.sleep(0.01) + + finally: + # Clean up monitoring + if stuck_monitor: + for proc in self.processes: + stuck_monitor.stop_monitoring(proc) + self._status_monitoring_active = False + + # Check for processes that failed with non-zero exit codes + if exit_failed_processes: + print(f"\n\033[91m###### ERROR ######\033[0m") + print( + f"Tests failed due to {len(exit_failed_processes)} process(es) with non-zero exit codes:" + ) + for proc, exit_code in exit_failed_processes: + print(f" - {proc.command} (exit code {exit_code})") + failures: list[TestFailureInfo] = [] + for proc, exit_code in exit_failed_processes: + # Extract error snippet from process output + error_snippet = extract_error_snippet(proc.accumulated_output) + + failures.append( + TestFailureInfo( + test_name=_extract_test_name(proc.command), + command=str(proc.command), + return_code=exit_code, + output=error_snippet, + error_type="exit_failure", + ) + ) + raise TestExecutionFailedException("Tests failed", failures) + + # Check for failed processes (killed due to timeout/stuck) + if failed_processes: + print(f"\n\033[91m###### ERROR ######\033[0m") + print(f"Tests failed due to {len(failed_processes)} killed process(es):") + for cmd in failed_processes: + print(f" - {cmd}") + print("Processes were killed due to timeout/stuck detection") + failures: list[TestFailureInfo] = [] + for cmd in failed_processes: + failures.append( + TestFailureInfo( + test_name=_extract_test_name(cmd), + command=str(cmd), + return_code=1, + output="Process was killed due to timeout/stuck detection", + error_type="killed_process", + ) + ) + raise TestExecutionFailedException("Processes were killed", failures) + + return completed_timings + + def get_status(self) -> GroupStatus: + """Get current status of all processes in the group.""" + process_statuses = [] + completed = 0 + failed = 0 + + for process in self.processes: + start_time = self._process_start_times.get(process) + if start_time is None: + # Process hasn't started yet, use current time + start_time = datetime.now() + running_duration = timedelta(0) + else: + running_duration = datetime.now() - start_time + + # Get last output line from process + last_output = self._get_last_output_line(process) + + # Get process name - use command friendly name if available + process_name = getattr(process, "name", None) + if not process_name: + if hasattr(process, "command"): + process_name = _get_friendly_test_name(process.command) + else: + process_name = f"Process-{id(process)}" + + status = ProcessStatus( + name=process_name, + is_alive=not process.finished, + is_completed=process.finished, + start_time=start_time, + running_duration=running_duration, + last_output_line=last_output, + return_value=process.returncode, + ) + + process_statuses.append(status) + + if status.is_completed: + completed += 1 + if status.return_value != 0: + failed += 1 + + return GroupStatus( + group_name=self.name, + processes=process_statuses, + total_processes=len(self.processes), + completed_processes=completed, + failed_processes=failed, + ) + + def _get_last_output_line(self, process: RunningProcess) -> Optional[str]: + """Extract the last line of output from a process.""" + # Try to get from cached last output first + cached = self._process_last_output.get(process) + if cached: + return cached + + # Fall back to accumulated output + if process.accumulated_output: + last_line = process.accumulated_output[-1].strip() + self._process_last_output[process] = last_line + return last_line + + return None + + def _track_process_start(self, process: RunningProcess) -> None: + """Record when a process starts for timing calculations.""" + self._process_start_times[process] = datetime.now() + + def _update_process_output(self, process: RunningProcess) -> None: + """Update cached last output line for a process.""" + if process.accumulated_output: + last_line = process.accumulated_output[-1].strip() + self._process_last_output[process] = last_line + + def _handle_stuck_processes( + self, + stuck_signals: list[StuckProcessSignal], + active_processes: list[RunningProcess], + failed_processes: list[str], + stuck_monitor: ProcessStuckMonitor, + ) -> None: + """Handle processes that are detected as stuck.""" + _handle_stuck_processes( + stuck_signals, active_processes, failed_processes, stuck_monitor + ) + + def _build_failure_list( + self, + exit_failed_processes: list[tuple[RunningProcess, int]], + failed_processes: list[str], + ) -> list[TestFailureInfo]: + """Build a list of TestFailureInfo objects from failed processes.""" + failures: list[TestFailureInfo] = [] + + for proc, exit_code in exit_failed_processes: + error_snippet = extract_error_snippet(proc.accumulated_output) + failures.append( + TestFailureInfo( + test_name=_extract_test_name(proc.command), + command=str(proc.command), + return_code=exit_code, + output=error_snippet, + error_type="exit_failure", + ) + ) + + for cmd in failed_processes: + if isinstance(cmd, list): + cmd_str = subprocess.list2cmdline(cmd) + else: + cmd_str = str(cmd) + failures.append( + TestFailureInfo( + test_name=_extract_test_name(cmd_str), + command=cmd_str, + return_code=1, + output="Process was killed due to timeout/stuck detection", + error_type="killed_process", + ) + ) + + return failures + + def _process_active_tests( + self, + active_processes: list[RunningProcess], + exit_failed_processes: list[tuple[RunningProcess, int]], + failed_processes: list[str], + completed_timings: List[ProcessTiming], + stuck_monitor: Optional[ProcessStuckMonitor], + ) -> bool: + """Process active tests, return True if any activity occurred.""" + any_activity = False + + # Iterate backwards to safely remove processes from the list + for i in range(len(active_processes) - 1, -1, -1): + proc = active_processes[i] + + if self.config.verbose: + with proc.line_iter(timeout=60) as line_iter: + for line in line_iter: + print(line) + any_activity = True + + # Check if process has finished + if proc.finished: + any_activity = True + # Get the exit code to check for failure + exit_code = proc.wait() + + # Process completed, remove from active list + active_processes.remove(proc) + # Stop monitoring this process + if stuck_monitor: + stuck_monitor.stop_monitoring(proc) + + # Collect timing data + if proc.duration is not None: + timing = ProcessTiming( + name=_get_friendly_test_name(proc.command), + duration=proc.duration, + command=str(proc.command), + ) + completed_timings.append(timing) + + # Check for non-zero exit code (failure) + if exit_code != 0: + print(f"Process failed with exit code {exit_code}: {proc.command}") + exit_failed_processes.append((proc, exit_code)) + # Early abort if we reached the failure threshold + if ( + len(exit_failed_processes) + len(failed_processes) + ) >= self.config.max_failures_before_abort: + print( + f"\nExceeded failure threshold ({self.config.max_failures_before_abort}). Aborting remaining tests." + ) + # Kill remaining active processes + for p in active_processes: + if p is not proc: + p.kill() + # Prepare failures with snippets + failures = self._build_failure_list( + exit_failed_processes, failed_processes + ) + raise TestExecutionFailedException( + "Exceeded failure threshold", failures + ) + + return any_activity + + def _run_sequential(self) -> List[ProcessTiming]: + """Execute processes in sequence.""" + completed_timings: List[ProcessTiming] = [] + + # Enable status monitoring + self._status_monitoring_active = True + try: + for process in self.processes: + print(f"Running: {process.get_command_str()}") + + # Track process start time + self._track_process_start(process) + + # Start the process if not already running + if process.proc is None: + process.run() + + try: + exit_code = process.wait() + + # Collect timing data + if process.duration is not None: + timing = ProcessTiming( + name=_get_friendly_test_name(process.command), + duration=process.duration, + command=str(process.command), + ) + completed_timings.append(timing) + + # Check for failure + if exit_code != 0: + error_snippet = extract_error_snippet( + process.accumulated_output + ) + failure = TestFailureInfo( + test_name=_extract_test_name(process.command), + command=str(process.command), + return_code=exit_code, + output=error_snippet, + error_type="exit_failure", + ) + raise TestExecutionFailedException( + f"Process failed with exit code {exit_code}", [failure] + ) + + except Exception as e: + print(f"Process failed: {process.get_command_str()} - {e}") + raise + finally: + self._status_monitoring_active = False + + return completed_timings + + def _run_with_dependencies(self) -> List[ProcessTiming]: + """Execute processes respecting dependency order.""" + completed_timings: List[ProcessTiming] = [] + completed_processes: set[RunningProcess] = set() + remaining_processes = self.processes.copy() + + # Enable status monitoring + self._status_monitoring_active = True + try: + while remaining_processes: + # Find processes that can run (all dependencies completed) + ready_processes = [] + for process in remaining_processes: + dependencies = self._dependencies.get(process, []) + if all(dep in completed_processes for dep in dependencies): + ready_processes.append(process) + + if not ready_processes: + # No processes can run - circular dependency or missing process + remaining_names = [ + _get_friendly_test_name(p.command) for p in remaining_processes + ] + raise RuntimeError( + f"Circular dependency or missing dependency detected for: {remaining_names}" + ) + + # Run the first ready process + process = ready_processes[0] + print(f"Running: {process.get_command_str()}") + + # Track process start time + self._track_process_start(process) + + # Start the process if not already running + if process.proc is None: + process.run() + + try: + exit_code = process.wait() + + # Collect timing data + if process.duration is not None: + timing = ProcessTiming( + name=_get_friendly_test_name(process.command), + duration=process.duration, + command=str(process.command), + ) + completed_timings.append(timing) + + # Mark as completed + completed_processes.add(process) + remaining_processes.remove(process) + + # Check for failure + if exit_code != 0: + error_snippet = extract_error_snippet( + process.accumulated_output + ) + failure = TestFailureInfo( + test_name=_extract_test_name(process.command), + command=str(process.command), + return_code=exit_code, + output=error_snippet, + error_type="exit_failure", + ) + raise TestExecutionFailedException( + f"Process failed with exit code {exit_code}", [failure] + ) + + except Exception as e: + print(f"Process failed: {process.get_command_str()} - {e}") + raise + finally: + self._status_monitoring_active = False + + return completed_timings diff --git a/v1.5/lib/FastLED/ci/util/running_process_manager.py b/v1.5/lib/FastLED/ci/util/running_process_manager.py new file mode 100644 index 0000000..a3275b1 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/running_process_manager.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import threading +from typing import List + +# Import at runtime since this module is part of util package and used broadly +from ci.util.running_process import RunningProcess + + +class RunningProcessManager: + """Thread-safe registry of currently running processes for diagnostics.""" + + def __init__(self) -> None: + self._lock = threading.RLock() + self._processes: list[RunningProcess] = [] + + def register(self, proc: RunningProcess) -> None: + with self._lock: + if proc not in self._processes: + self._processes.append(proc) + + def unregister(self, proc: RunningProcess) -> None: + with self._lock: + try: + self._processes.remove(proc) + except ValueError: + pass + + def list_active(self) -> list[RunningProcess]: + with self._lock: + return [p for p in self._processes if not p.finished] + + def dump_active(self) -> None: + active: list[RunningProcess] = self.list_active() + if not active: + print("\nNO ACTIVE SUBPROCESSES DETECTED - MAIN PROCESS LIKELY HUNG") + return + + print("\nSTUCK SUBPROCESS COMMANDS:") + import time + + now = time.time() + for idx, p in enumerate(active, 1): + pid: int | None = None + try: + if p.proc is not None: + pid = p.proc.pid + except Exception: + pid = None + + start = p.start_time + last_out = p.time_last_stdout_line() + duration_str = f"{(now - start):.1f}s" if start is not None else "?" + since_out_str = ( + f"{(now - last_out):.1f}s" if last_out is not None else "no-output" + ) + + print( + f" {idx}. cmd={p.command} pid={pid} duration={duration_str} last_output={since_out_str}" + ) + + +# Global singleton instance for convenient access +RunningProcessManagerSingleton = RunningProcessManager() diff --git a/v1.5/lib/FastLED/ci/util/sccache_config.py b/v1.5/lib/FastLED/ci/util/sccache_config.py new file mode 100644 index 0000000..6ae16f3 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/sccache_config.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +SCCACHE Configuration for FastLED Builds + +Provides sccache (distributed compiler cache) support for faster compilation. +SCCACHE is faster and more reliable than ccache, especially for CI/CD environments. +""" + +import os +import platform +import shutil +import subprocess +from pathlib import Path +from typing import Any, Protocol + + +class PlatformIOEnv(Protocol): + """Type hint for PlatformIO environment object.""" + + def get(self, key: str) -> str | None: + """Get environment variable value.""" + ... + + def Replace(self, **kwargs: Any) -> None: + """Replace environment variables.""" + ... + + +def is_sccache_available() -> bool: + """Check if sccache is available in the system.""" + try: + subprocess.run(["sccache", "--version"], capture_output=True, check=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + + +def get_sccache_path() -> str | None: + """Get the full path to sccache executable.""" + # Use shutil.which for cross-platform executable finding + sccache_path = shutil.which("sccache") + if sccache_path: + return sccache_path + + # Additional Windows-specific paths + if platform.system() == "Windows": + additional_paths = [ + "C:\\ProgramData\\chocolatey\\bin\\sccache.exe", + os.path.expanduser("~\\scoop\\shims\\sccache.exe"), + os.path.expanduser("~\\.cargo\\bin\\sccache.exe"), + ] + for path in additional_paths: + if os.path.exists(path): + return path + + return None + + +def configure_sccache(env: PlatformIOEnv) -> None: + """Configure SCCACHE for the build environment.""" + if not is_sccache_available(): + print("SCCACHE is not available. Skipping SCCACHE configuration.") + return + + sccache_path = get_sccache_path() + if not sccache_path: + print("Could not find SCCACHE executable. Skipping SCCACHE configuration.") + return + + print(f"Found SCCACHE at: {sccache_path}") + + # Set up SCCACHE environment variables + project_dir = env.get("PROJECT_DIR") + if project_dir is None: + project_dir = os.getcwd() + + # Use board-specific sccache directory if PIOENV (board environment) is available + board_name = env.get("PIOENV") + if board_name: + sccache_dir = os.path.join(project_dir, ".sccache", board_name) + else: + sccache_dir = os.path.join(project_dir, ".sccache", "default") + + # Create sccache directory + Path(sccache_dir).mkdir(parents=True, exist_ok=True) + print(f"Using board-specific SCCACHE directory: {sccache_dir}") + + # Configure SCCACHE environment variables + os.environ["SCCACHE_DIR"] = sccache_dir + os.environ["SCCACHE_CACHE_SIZE"] = "2G" # Larger cache for better hit rates + + # Optional: Configure distributed caching (Redis/Memcached) if available + # This can be enabled by setting environment variables before build: + # export SCCACHE_REDIS=redis://localhost:6379 + # export SCCACHE_MEMCACHED=localhost:11211 + + # Configure compression for better storage efficiency + if platform.system() != "Windows": + # Only set on Unix-like systems where it's more reliable + os.environ["SCCACHE_LOG"] = "info" + + # Get current compiler paths + original_cc = env.get("CC") + original_cxx = env.get("CXX") + + # Note: For PlatformIO, we don't need to manually wrap CC/CXX here + # Instead, we'll use build_flags to wrap the compiler commands + # This is handled in the Board configuration via extra_scripts + + print(f"SCCACHE configuration completed") + print(f"Cache directory: {sccache_dir}") + print(f"Cache size limit: 2G") + + # Show SCCACHE stats if available + try: + result = subprocess.run( + [sccache_path, "--show-stats"], capture_output=True, text=True, check=False + ) + if result.returncode == 0: + print("SCCACHE Statistics:") + print(result.stdout) + except Exception: + pass # Don't fail build if stats aren't available + + +def get_sccache_wrapper_script_content() -> str: + """Generate content for sccache wrapper script for PlatformIO extra_scripts.""" + return ''' +# SCCACHE wrapper script for PlatformIO builds +# This script automatically wraps compiler commands with sccache + +Import("env") + +import os +import shutil +from pathlib import Path + +def setup_sccache_wrapper(): + """Setup sccache wrapper for compiler commands.""" + + # Check if sccache is available + sccache_path = shutil.which("sccache") + if not sccache_path: + print("SCCACHE not found, compilation will proceed without caching") + return + + print(f"Setting up SCCACHE wrapper: {sccache_path}") + + # Get current build environment + project_dir = env.get("PROJECT_DIR", os.getcwd()) + board_name = env.get("PIOENV", "default") + + # Setup sccache directory + sccache_dir = os.path.join(project_dir, ".sccache", board_name) + Path(sccache_dir).mkdir(parents=True, exist_ok=True) + + # Configure sccache environment + os.environ["SCCACHE_DIR"] = sccache_dir + os.environ["SCCACHE_CACHE_SIZE"] = "2G" + + # Wrap compiler commands + current_cc = env.get("CC", "gcc") + current_cxx = env.get("CXX", "g++") + + # Only wrap if not already wrapped + if "sccache" not in current_cc: + env.Replace( + CC=f'"{sccache_path}" {current_cc}', + CXX=f'"{sccache_path}" {current_cxx}', + ) + print(f"Wrapped CC with SCCACHE: {env.get('CC')}") + print(f"Wrapped CXX with SCCACHE: {env.get('CXX')}") + +# Setup sccache wrapper +setup_sccache_wrapper() +''' + + +# This script can be executed directly by PlatformIO as an extra_script +if __name__ == "__main__": + # For direct execution by PlatformIO extra_scripts + pass diff --git a/v1.5/lib/FastLED/ci/util/scrapers/README.md b/v1.5/lib/FastLED/ci/util/scrapers/README.md new file mode 100644 index 0000000..42631de --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/scrapers/README.md @@ -0,0 +1,50 @@ +# FastLED Web Scrapers + +This directory contains web scraping tools for testing and capturing screenshots from the online FastLED tool. + +## Contents + +- **`scrape_festival_stick.py`** - Main web scraping script that navigates to the online FastLED tool and captures screenshots +- **`run_fastled_scraper.py`** - Utility script for easy execution with different configurations +- **`screenshots/`** - Directory containing captured screenshots with timestamps +- **`fastled_python_investigation.md`** - Comprehensive documentation of the investigation and implementation + +## Quick Start + +### Run from project root: +```bash +# Basic usage +uv run python ci/ci/scrapers/scrape_festival_stick.py + +# With utility script options +uv run python ci/ci/scrapers/run_fastled_scraper.py --headless --timeout 60 +``` + +### Run from scrapers directory: +```bash +cd ci/ci/scrapers +uv run python scrape_festival_stick.py +uv run python run_fastled_scraper.py --help +``` + +## Features + +- **Automated browser navigation** to https://fastled.onrender.com/docs +- **Smart element detection** for FastLED interface components +- **Screenshot capture** with timestamp organization +- **File upload attempts** for FastLED examples +- **Error handling** with debug screenshots +- **Headless and visible modes** for different use cases + +## Requirements + +The scrapers use the existing FastLED Python environment with: +- `playwright` for browser automation +- Python dependencies from `pyproject.toml` +- Automatic Playwright browser installation + +## Integration + +These scrapers are integrated with the FastLED CI infrastructure and complement the existing web testing in `ci/wasm_test.py`. + +For detailed information, see `fastled_python_investigation.md`. diff --git a/v1.5/lib/FastLED/ci/util/scrapers/fastled_python_investigation.md b/v1.5/lib/FastLED/ci/util/scrapers/fastled_python_investigation.md new file mode 100644 index 0000000..0983ada --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/scrapers/fastled_python_investigation.md @@ -0,0 +1,197 @@ +# FastLED Python Code Investigation + +## Overview +This document summarizes the investigation of existing Python code in the FastLED project, particularly focusing on Playwright integration and web scraping capabilities for the FastLED online tool. + +**Location**: All scripts are located in the `ci/ci/scrapers/` directory, organized within the CI infrastructure alongside the existing testing infrastructure, including the original Playwright implementation in `ci/wasm_test.py`. + +## Existing Python Infrastructure + +### Dependencies (pyproject.toml) +The project already includes comprehensive dependencies for development and testing: +- **Playwright**: `playwright` - for browser automation +- **Testing**: `pytest`, `pytest-xdist` for parallel testing +- **FastLED**: `fastled>=1.2.26` - the FastLED Python package +- **FastLED WASM**: `fastled-wasm` - WebAssembly support +- **HTTP Client**: `httpx` - for HTTP requests +- **Build Tools**: `uv`, `ziglang`, `ninja`, `cmake` +- **Code Quality**: `ruff`, `mypy`, `pyright`, `clang-format`, `isort`, `black` + +### Existing Playwright Implementation (`ci/wasm_test.py`) + +The project already has a sophisticated Playwright setup that: + +1. **Automatic Browser Installation**: + ```python + def install_playwright_browsers(): + os.system(f"{sys.executable} -m playwright install chromium") + ``` + +2. **FastLED WASM Testing**: + - Starts an HTTP server for WASM examples + - Tests browser automation with the FastLED.js library + - Monitors `FastLED_onFrame` callback execution + - Validates WebGL/WASM functionality + +3. **Error Handling**: + - Console log monitoring for errors + - Timeout handling for page loads + - Proper server cleanup + +### MCP Server Integration (`mcp_server.py`) + +The project includes a comprehensive MCP (Model Context Protocol) server with tools for: +- Running tests with various options +- Compiling examples for different platforms +- Code fingerprinting and change detection +- Linting and formatting +- Project information and status + +## FestivalStick Example Analysis + +### Core Functionality +The FestivalStick example (`examples/FestivalStick/`) is a sophisticated LED pattern demo featuring: + +1. **Corkscrew LED Mapping**: + - 19.25 turns, 288 LEDs + - Maps 2D patterns to spiral LED positions + - Uses `fl::Corkscrew` class for geometric calculations + +2. **UI Controls**: + - Speed, position, brightness controls + - Noise pattern generation with customizable parameters + - Color palette selection (Party, Heat, Ocean, Forest, Rainbow) + - Rendering mode options (Noise, Position, Mixed) + - Color boost with saturation/luminance functions + +3. **Advanced Features**: + - Multi-sampling for accurate LED positioning + - Real-time noise generation with cylindrical mapping + - Auto-advance mode with manual position override + - ScreenMap integration for web visualization + +### Key Components +```cpp +// Corkscrew configuration +#define NUM_LEDS 288 +#define CORKSCREW_TURNS 19.25 + +// Runtime corkscrew with flexible configuration +Corkscrew::Input corkscrewInput(CORKSCREW_TURNS, NUM_LEDS, 0); +Corkscrew corkscrew(corkscrewInput); + +// Frame buffer for 2D pattern drawing +fl::Grid frameBuffer; + +// ScreenMap for web interface visualization +fl::ScreenMap corkscrewScreenMap = corkscrew.toScreenMap(0.2f); +``` + +## Web Scraping Script Implementation + +### Script Features (`scrape_festival_stick.py`) + +1. **Robust Web Navigation**: + - Navigates to https://fastled.onrender.com/docs + - Handles dynamic content loading + - Searches for multiple possible interface elements + +2. **Smart Element Detection**: + - Looks for example selection dropdowns + - Detects file upload capabilities + - Finds canvas/visualization elements + - Searches for FestivalStick-specific content + +3. **Screenshot Capabilities**: + - Full page screenshots with timestamps + - Focused canvas screenshots when available + - Error screenshots for debugging + - Multiple resolution support (1920x1080 default) + +4. **File Upload Attempt**: + - Automatically tries to upload `FestivalStick.ino` + - Handles missing file scenarios gracefully + - Waits for upload processing + +### Script Workflow +1. Install Playwright browsers automatically +2. Launch visible browser with slow motion for debugging +3. Navigate to online FastLED tool +4. Search for example/upload functionality +5. Attempt to load FestivalStick example +6. Capture screenshots of the visualization +7. Save timestamped results to `screenshots/` directory + +## Project Testing Infrastructure + +### Unit Tests +- Location: `tests/` directory +- Command: `bash test` (per user rules) +- Comprehensive C++ unit tests for all components +- Platform compilation tests +- Code quality checks + +### Example Compilation +- Multi-platform support: `uno`, `esp32`, `teensy`, etc. +- Command: `./compile --examples ` +- Batch compilation for multiple platforms +- Interactive and automated modes + +## Key Findings + +1. **Comprehensive Infrastructure**: The FastLED project already has extensive Python tooling with Playwright, testing, and web automation capabilities. + +2. **Advanced LED Visualization**: The FestivalStick example represents sophisticated LED pattern generation with real-time parameter control and web visualization. + +3. **Web Integration Ready**: The existing WASM testing infrastructure provides a solid foundation for web-based LED visualization and interaction. + +4. **Documentation Gap**: While the code is well-implemented, there could be more comprehensive documentation of the web tooling capabilities. + +## Recommendations + +1. **Extend Web Scraping**: The created script could be enhanced to: + - Test multiple examples automatically + - Capture video recordings of animations + - Perform parameter sweeps for different configurations + +2. **Integration Testing**: Consider adding the web scraping script to the CI/CD pipeline for automated web interface testing. + +3. **User Documentation**: Create user guides for the online FastLED tool and example usage. + +## Results + +✅ **Successfully captured screenshot**: `ci/ci/scrapers/screenshots/festival_stick_20250620_224055.png` (82KB) + +The script successfully: +1. Navigated to https://fastled.onrender.com/docs +2. Detected and interacted with the FastLED web interface +3. Captured a full-page screenshot of the FestivalStick example visualization +4. Saved the result with timestamp for easy identification + +## Files Created/Modified + +- `ci/ci/scrapers/scrape_festival_stick.py` - Main web scraping script with Playwright automation +- `ci/ci/scrapers/run_fastled_scraper.py` - Utility script for easy execution with different configurations +- `ci/ci/scrapers/screenshots/` - Directory containing captured images +- `ci/ci/scrapers/screenshots/festival_stick_20250620_224055.png` - Successfully captured screenshot (82KB) +- `ci/ci/scrapers/fastled_python_investigation.md` - This documentation file + +## Usage Examples + +```bash +# Run the scraper directly from project root +uv run ci/ci/scrapers/scrape_festival_stick.py + +# Use the utility script with options +uv run ci/ci/scrapers/run_fastled_scraper.py --example FestivalStick --headless --timeout 60 + +# Make scripts executable and run from project root +chmod +x ci/ci/scrapers/scrape_festival_stick.py ci/ci/scrapers/run_fastled_scraper.py +./ci/ci/scrapers/scrape_festival_stick.py +./ci/ci/scrapers/run_fastled_scraper.py --help + +# Or run from within the scrapers directory +cd ci/ci/scrapers +uv run scrape_festival_stick.py +uv run run_fastled_scraper.py --help +``` diff --git a/v1.5/lib/FastLED/ci/util/scrapers/run_fastled_scraper.py b/v1.5/lib/FastLED/ci/util/scrapers/run_fastled_scraper.py new file mode 100644 index 0000000..c1a1ebf --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/scrapers/run_fastled_scraper.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +FastLED Web Scraper Utility + +This script provides an easy way to run the FastLED web scraper with different +configurations and examples. +""" + +import argparse +import subprocess +import sys +from pathlib import Path + + +def run_scraper( + example_name: str = "FestivalStick", headless: bool = False, timeout: int = 30 +) -> int: + """Run the FastLED web scraper with specified parameters""" + + script_path = Path(__file__).parent / "scrape_festival_stick.py" + + if not script_path.exists(): + print(f"Error: Scraper script not found at {script_path}", file=sys.stderr) + return 1 + + print(f"Running FastLED web scraper for {example_name}...") + print(f"Headless mode: {headless}") + print(f"Timeout: {timeout} seconds") + + try: + # For now, just run the existing script + # In the future, this could be enhanced to pass parameters + result = subprocess.run( + [sys.executable, str(script_path)], + check=False, + capture_output=True, + text=True, + ) + + if result.returncode == 0: + print("✅ Scraping completed successfully!") + print(result.stdout) + else: + print("❌ Scraping failed!") + print("STDOUT:", result.stdout) + print("STDERR:", result.stderr) + + return result.returncode + + except Exception as e: + print(f"Error running scraper: {e}", file=sys.stderr) + return 1 + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run FastLED web scraper") + parser.add_argument( + "--example", + "-e", + default="FestivalStick", + help="Example name to scrape (default: FestivalStick)", + ) + parser.add_argument( + "--headless", action="store_true", help="Run browser in headless mode" + ) + parser.add_argument( + "--timeout", "-t", type=int, default=30, help="Timeout in seconds (default: 30)" + ) + + args = parser.parse_args() + + return run_scraper(args.example, args.headless, args.timeout) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/util/scrapers/scrape_festival_stick.py b/v1.5/lib/FastLED/ci/util/scrapers/scrape_festival_stick.py new file mode 100644 index 0000000..e80d615 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/scrapers/scrape_festival_stick.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +import asyncio +import os +import sys +from datetime import datetime +from pathlib import Path + +from playwright.async_api import async_playwright # type: ignore + + +HERE = Path(__file__).parent +PROJECT_ROOT = HERE.parent.parent.parent # scrapers is 3 levels down from project root +SCREENSHOTS_DIR = HERE / "screenshots" + +# Ensure screenshots directory exists +SCREENSHOTS_DIR.mkdir(exist_ok=True) + + +# Ensure Playwright browsers are installed +def install_playwright_browsers(): + print("Installing Playwright browsers...") + try: + os.system(f"{sys.executable} -m playwright install chromium") + print("Playwright browsers installed successfully.") + except Exception as e: + print(f"Failed to install Playwright browsers: {e}", file=sys.stderr) + sys.exit(1) + + +async def scrape_festival_stick_example(): + """ + Navigate to the online FastLED tool and capture a screenshot of the FestivalStick example + """ + install_playwright_browsers() + + # Online FastLED tool URL + fastled_url = "https://fastled.onrender.com/docs" + + async with async_playwright() as p: + # Launch browser with a visible window for debugging + browser = await p.chromium.launch(headless=False, slow_mo=1000) + page = await browser.new_page() + + # Set viewport size for consistent screenshots + await page.set_viewport_size({"width": 1920, "height": 1080}) + + try: + print(f"Navigating to {fastled_url}...") + await page.goto(fastled_url, timeout=30000) + + # Wait for the page to load + await page.wait_for_load_state("networkidle") + + # Look for FastLED examples or upload functionality + print("Looking for FastLED example functionality...") + + # Wait a bit for dynamic content to load + await page.wait_for_timeout(3000) + + # Try to find any example selection or file upload elements + examples_selector = None + possible_selectors = [ + "select[name*='example']", + "select[id*='example']", + ".example-selector", + "input[type='file']", + "button:has-text('Example')", + "button:has-text('FestivalStick')", + "a:has-text('Example')", + "a:has-text('FestivalStick')", + ] + + for selector in possible_selectors: + try: + element = await page.wait_for_selector(selector, timeout=2000) + if element: + print(f"Found element with selector: {selector}") + examples_selector = selector + break + except Exception: + continue + + if not examples_selector: + # If no specific example selector found, look for text content + print("Looking for FestivalStick text on page...") + try: + await page.wait_for_selector("text=FestivalStick", timeout=5000) + print("Found FestivalStick text on page!") + except Exception: + print( + "FestivalStick text not found, taking screenshot of current page..." + ) + + # Try to interact with the FastLED tool interface + print("Attempting to interact with FastLED interface...") + + # Look for common web interface elements + interface_elements = [ + "canvas", + ".led-display", + ".visualization", + "#fastled-canvas", + ".fastled-viewer", + ] + + canvas_found = False + canvas = None + for element_selector in interface_elements: + try: + canvas_element = await page.wait_for_selector( + element_selector, timeout=2000 + ) + if canvas_element: + print(f"Found display element: {element_selector}") + canvas_found = True + canvas = canvas_element + break + except Exception: + continue + + if not canvas_found: + print("No specific display canvas found, capturing full page...") + + # If there's a file upload option, try to upload the FestivalStick example + try: + file_input = await page.query_selector("input[type='file']") + if file_input: + print("Found file input, attempting to upload FestivalStick.ino...") + festival_stick_path = ( + PROJECT_ROOT + / "examples" + / "FestivalStick" + / "FestivalStick.ino" + ) + if festival_stick_path.exists(): + await file_input.set_input_files(str(festival_stick_path)) + await page.wait_for_timeout(3000) # Wait for upload to process + print("FestivalStick.ino uploaded successfully!") + else: + print(f"FestivalStick.ino not found at {festival_stick_path}") + except Exception as e: + print(f"Could not upload file: {e}") + + # Wait for any animations or dynamic content to settle + await page.wait_for_timeout(5000) + + # Take screenshot + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + screenshot_path = SCREENSHOTS_DIR / f"festival_stick_{timestamp}.png" + + print(f"Taking screenshot and saving to {screenshot_path}...") + await page.screenshot(path=str(screenshot_path), full_page=True) + + print(f"Screenshot saved successfully to {screenshot_path}") + + # Also take a focused screenshot if we found a canvas + if canvas_found and canvas is not None: + try: + canvas_screenshot_path = ( + SCREENSHOTS_DIR / f"festival_stick_canvas_{timestamp}.png" + ) + await canvas.screenshot(path=str(canvas_screenshot_path)) + print(f"Canvas screenshot saved to {canvas_screenshot_path}") + except Exception as e: + print(f"Could not take canvas screenshot: {e}") + + # Keep browser open for a few seconds to see the result + print("Keeping browser open for 10 seconds for inspection...") + await page.wait_for_timeout(10000) + + except Exception as e: + print(f"An error occurred during scraping: {e}", file=sys.stderr) + + # Take an error screenshot for debugging + error_screenshot_path = ( + SCREENSHOTS_DIR + / f"error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + ) + try: + await page.screenshot(path=str(error_screenshot_path), full_page=True) + print(f"Error screenshot saved to {error_screenshot_path}") + except Exception: + pass + + raise e + + finally: + await browser.close() + + +async def main(): + """Main function to run the scraping operation""" + try: + await scrape_festival_stick_example() + print("FastLED FestivalStick scraping completed successfully!") + except Exception as e: + print(f"Scraping failed: {e}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + exit_code = asyncio.run(main()) + sys.exit(exit_code) diff --git a/v1.5/lib/FastLED/ci/util/symbol_analysis.py b/v1.5/lib/FastLED/ci/util/symbol_analysis.py new file mode 100644 index 0000000..03da60d --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/symbol_analysis.py @@ -0,0 +1,898 @@ +#!/usr/bin/env python3 +# pyright: reportUnknownMemberType=false, reportOperatorIssue=false, reportArgumentType=false +""" +Enhanced Symbol Analysis Tool with Function Call Graph Analysis +Analyzes ELF files to identify symbols and function call relationships. +Shows which functions call other functions (call graph analysis). +Works with any platform (ESP32S3, UNO, etc.) that has build_info.json. +""" + +import json +import re +import subprocess +import sys +from collections import defaultdict +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Dict, Iterator, List, Optional, Tuple + +# Import board mapping system +from ci.boards import create_board + + +@dataclass +class SymbolInfo: + """Represents a symbol in the binary""" + + address: str + size: int + type: str + name: str + demangled_name: str + source: str # STRICT: NO defaults - all callers must provide explicit source + + +@dataclass +class TypeBreakdown: + """Breakdown of symbols by type""" + + type: str + count: int + total_size: int + + +@dataclass +class CallStats: + """Statistics about function calls""" + + functions_with_calls: int + functions_called_by_others: int + most_called: List[Tuple[str, int]] = field(default_factory=lambda: []) + most_calling: List[Tuple[str, int]] = field(default_factory=lambda: []) + + +@dataclass +class AnalysisReport: + """Complete symbol analysis report""" + + board: str + total_symbols: int + total_size: int + largest_symbols: List[SymbolInfo] = field(default_factory=lambda: []) + type_breakdown: List[TypeBreakdown] = field(default_factory=lambda: []) + dependencies: Dict[str, List[str]] = field(default_factory=lambda: {}) + call_graph: Optional[Dict[str, List[str]]] = None + reverse_call_graph: Optional[Dict[str, List[str]]] = None + call_stats: Optional[CallStats] = None + + +@dataclass +class DetailedAnalysisData: + """Complete detailed analysis data structure for JSON output""" + + summary: AnalysisReport + all_symbols_sorted_by_size: List[SymbolInfo] + dependencies: Dict[str, List[str]] + call_graph: Optional[Dict[str, List[str]]] = None + reverse_call_graph: Optional[Dict[str, List[str]]] = None + + +@dataclass +class TypeStats: + """Statistics for symbol types with dictionary-like functionality""" + + stats: Dict[str, TypeBreakdown] = field(default_factory=lambda: {}) + + def add_symbol(self, symbol: SymbolInfo) -> None: + """Add a symbol to the type statistics""" + sym_type = symbol.type + if sym_type not in self.stats: + self.stats[sym_type] = TypeBreakdown(type=sym_type, count=0, total_size=0) + self.stats[sym_type].count += 1 + self.stats[sym_type].total_size += symbol.size + + def items(self) -> List[Tuple[str, TypeBreakdown]]: + """Return items for iteration, sorted by total_size descending""" + return sorted(self.stats.items(), key=lambda x: x[1].total_size, reverse=True) + + def values(self) -> List[TypeBreakdown]: + """Return values for iteration""" + return list(self.stats.values()) + + def __getitem__(self, key: str) -> TypeBreakdown: + """Allow dictionary-style access""" + return self.stats[key] + + def __contains__(self, key: str) -> bool: + """Allow 'in' operator""" + return key in self.stats + + +def run_command(cmd: str) -> str: + """Run a command and return stdout""" + try: + result = subprocess.run( + cmd, shell=True, capture_output=True, text=True, check=True + ) + return result.stdout + except subprocess.CalledProcessError as e: + print(f"Error running command: {cmd}") + print(f"Error: {e.stderr}") + return "" + + +def demangle_symbol(mangled_name: str, cppfilt_path: str) -> str: + """Demangle a C++ symbol using c++filt""" + try: + cmd = f'echo "{mangled_name}" | "{cppfilt_path}"' + result = subprocess.run( + cmd, shell=True, capture_output=True, text=True, check=True + ) + demangled = result.stdout.strip() + # If demangling failed, c++filt returns the original name + return demangled if demangled != mangled_name else mangled_name + except Exception as e: + print(f"Error demangling symbol: {mangled_name}") + print(f"Error: {e}") + return mangled_name + + +def analyze_symbols( + elf_file: str, nm_path: str, cppfilt_path: str, readelf_path: Optional[str] = None +) -> List[SymbolInfo]: + """Analyze ALL symbols in ELF file using both nm and readelf for comprehensive coverage""" + print("Analyzing symbols with enhanced coverage...") + + symbols: List[SymbolInfo] = [] + symbols_dict: Dict[str, SymbolInfo] = {} # To deduplicate by address+name + + # Method 1: Use readelf to get ALL symbols (including those without size) + if readelf_path: + print("Getting all symbols using readelf...") + readelf_cmd = f'"{readelf_path}" -s "{elf_file}"' + output = run_command(readelf_cmd) + + if output: + for line in output.strip().split("\n"): + line = line.strip() + # Skip header and empty lines + if ( + not line + or "Num:" in line + or "Symbol table" in line + or line.startswith("--") + ): + continue + + # Parse readelf output format: Num: Value Size Type Bind Vis Ndx Name + parts = line.split() + if len(parts) >= 8: + try: + # Skip num (parts[0]) - not needed + addr = parts[1] + size = int(parts[2]) if parts[2].isdigit() else 0 + symbol_type = parts[3] + bind = parts[4] + # Skip vis and ndx (parts[5], parts[6]) - not needed + name = " ".join(parts[7:]) if len(parts) > 7 else "" + + # Skip empty names and section symbols + if not name or name.startswith(".") or symbol_type == "SECTION": + continue + + # Create a unique key for deduplication (use name as primary key since addresses can vary) + key = name.strip() + + if key not in symbols_dict: + # Demangle the symbol name + demangled_name = demangle_symbol(name, cppfilt_path) + + symbol_info = SymbolInfo( + address=addr, + size=size, + type=symbol_type[0].upper(), # T, D, B, etc. + name=name, + demangled_name=demangled_name, + source="readelf", + ) + + symbols_dict[key] = symbol_info + + except (ValueError, IndexError): + continue # Skip malformed lines + + # Method 2: Use nm with --print-size to get symbols with sizes (for accurate size info) + print("Getting sized symbols using nm...") + nm_cmd = f'"{nm_path}" --print-size --size-sort --radix=d "{elf_file}"' + output = run_command(nm_cmd) + + if output: + for line in output.strip().split("\n"): + if not line.strip(): + continue + + parts = line.split() + if len(parts) >= 4: + try: + addr = parts[0] + size = int(parts[1]) + symbol_type = parts[2] + name = " ".join(parts[3:]) + + # Create a unique key for deduplication (use name as primary key) + key = name.strip() + + # If we already have this symbol from readelf, update with accurate size + if key in symbols_dict: + symbols_dict[key].size = size + symbols_dict[key].type = symbol_type + symbols_dict[key].source = "nm+readelf" + else: + # New symbol not found by readelf + demangled_name = demangle_symbol(name, cppfilt_path) + + symbol_info = SymbolInfo( + address=addr, + size=size, + type=symbol_type, + name=name, + demangled_name=demangled_name, + source="nm", + ) + + symbols_dict[key] = symbol_info + + except (ValueError, IndexError): + continue # Skip malformed lines + + # Method 3: Use nm with -a to get all symbols including debugger-only + print("Getting additional symbols using nm -a...") + nm_all_cmd = f'"{nm_path}" -a --radix=d "{elf_file}"' + output = run_command(nm_all_cmd) + + if output: + for line in output.strip().split("\n"): + if not line.strip(): + continue + + parts = line.split() + if len(parts) >= 3: + try: + addr = parts[0] + symbol_type = parts[1] + name = " ".join(parts[2:]) + + # Skip empty names + if not name: + continue + + # Create a unique key for deduplication (use name as primary key) + key = name.strip() + + if key not in symbols_dict: + # New symbol not found by other methods + demangled_name = demangle_symbol(name, cppfilt_path) + + symbol_info = SymbolInfo( + address=addr, + size=0, # nm -a doesn't provide size + type=symbol_type, + name=name, + demangled_name=demangled_name, + source="nm-a", + ) + + symbols_dict[key] = symbol_info + + except (ValueError, IndexError): + continue # Skip malformed lines + + # Convert dict to list + symbols: List[SymbolInfo] = list(symbols_dict.values()) + + print(f"Found {len(symbols)} total symbols using enhanced analysis") + print(f" - Symbols with size info: {len([s for s in symbols if s.size > 0])}") + print(f" - Symbols without size: {len([s for s in symbols if s.size == 0])}") + + return symbols + + +def analyze_function_calls( + elf_file: str, objdump_path: str, cppfilt_path: str +) -> Dict[str, List[str]]: + """Analyze function calls using objdump to build call graph""" + print("Analyzing function calls using objdump...") + + # Use objdump to disassemble the binary + cmd = f'"{objdump_path}" -t "{elf_file}"' + print(f"Running: {cmd}") + symbol_output = run_command(cmd) + + # Build symbol address map for function symbols + symbol_map: Dict[str, str] = {} # address -> symbol_name + function_symbols: set[str] = set() # set of function names + + for line in symbol_output.strip().split("\n"): + if not line.strip(): + continue + + # Parse objdump symbol table output + # Format: address flags section size name + parts = line.split() + if len(parts) >= 5 and ("F" in parts[1] or "f" in parts[1]): # Function symbol + try: + address = parts[0] + symbol_name = " ".join(parts[4:]) + + # Demangle the symbol name + demangled_name = demangle_symbol(symbol_name, cppfilt_path) + + symbol_map[address] = {"name": symbol_name, "demangled": demangled_name} + function_symbols.add(demangled_name) + except (ValueError, IndexError): + continue + + print(f"Found {len(function_symbols)} function symbols") + + # Now disassemble text sections to find function calls + cmd = f'"{objdump_path}" -d "{elf_file}"' + print(f"Running: {cmd}") + disasm_output = run_command(cmd) + + if not disasm_output: + print("Warning: No disassembly output received") + return {} + + # Parse disassembly to find function calls + call_graph: defaultdict[str, set[str]] = defaultdict( + set + ) # caller -> set of callees + current_function = None + + # Common call instruction patterns for different architectures + call_patterns = [ + r"call\s+(\w+)", # x86/x64 call + r"bl\s+(\w+)", # ARM branch with link + r"jal\s+(\w+)", # RISC-V jump and link + r"callx?\d*\s+(\w+)", # Xtensa call variations + ] + + call_regex = re.compile( + "|".join(f"(?:{pattern})" for pattern in call_patterns), re.IGNORECASE + ) + + function_start_pattern = re.compile(r"^([0-9a-f]+)\s+<([^>]+)>:") + + lines = disasm_output.split("\n") + for i, line in enumerate(lines): + line = line.strip() + + # Check for function start + func_match = function_start_pattern.match(line) + if func_match: + func_name = func_match.group(2) + + # Demangle function name + current_function = demangle_symbol(func_name, cppfilt_path) + continue + + # Look for call instructions + if current_function and ( + "call" in line.lower() or "bl " in line.lower() or "jal" in line.lower() + ): + call_match = call_regex.search(line) + if call_match: + # Extract the target function name + for group in call_match.groups(): + if group: + target_func = demangle_symbol(group, cppfilt_path) + call_graph[current_function].add(target_func) + break + + print(f"Built call graph with {len(call_graph)} calling functions") + + # Convert sets to lists for JSON serialization + return {caller: list(callees) for caller, callees in call_graph.items()} + + +def build_reverse_call_graph(call_graph: Dict[str, List[str]]) -> Dict[str, List[str]]: + """Build reverse call graph: function -> list of functions that call it""" + reverse_graph: defaultdict[str, List[str]] = defaultdict(list) + + for caller, callees in call_graph.items(): + for callee in callees: + reverse_graph[callee].append(caller) + + return dict(reverse_graph) + + +def analyze_map_file(map_file: Path) -> Dict[str, List[str]]: + """Analyze the map file to understand module dependencies""" + print(f"Analyzing map file: {map_file}") + + dependencies: Dict[str, List[str]] = {} + current_archive: Optional[str] = None + + if not map_file.exists(): + print(f"Map file not found: {map_file}") + return {} + + try: + with open(map_file, "r") as f: + for line in f: + line = line.strip() + + # Look for archive member includes - handle both ESP32 and UNO formats + if ".a(" in line and ")" in line: + # Extract module name + start = line.find("(") + 1 + end = line.find(")") + if start > 0 and end > start: + current_archive = line[start:end] + dependencies[current_archive] = [] + + elif current_archive and line and not line.startswith((".", "/", "*")): + # This line shows what pulled in the module + if "(" in line and ")" in line: + # Extract the symbol that caused the inclusion + symbol_start = line.find("(") + 1 + symbol_end = line.find(")") + if symbol_start > 0 and symbol_end > symbol_start: + symbol = line[symbol_start:symbol_end] + dependencies[current_archive].append(symbol) + current_archive = None + except Exception as e: + print(f"Error reading map file: {e}") + return {} + + return dependencies + + +def generate_report( + board_name: str, + symbols: List[SymbolInfo], + dependencies: Dict[str, List[str]], + call_graph: Optional[Dict[str, List[str]]] = None, + reverse_call_graph: Optional[Dict[str, List[str]]] = None, + enhanced_mode: bool = False, +) -> AnalysisReport: + """Generate a comprehensive report with optional call graph analysis""" + print("\n" + "=" * 80) + if enhanced_mode and call_graph and reverse_call_graph: + print(f"{board_name.upper()} ENHANCED SYMBOL ANALYSIS REPORT") + else: + print(f"{board_name.upper()} SYMBOL ANALYSIS REPORT") + print("=" * 80) + + # Summary statistics + total_symbols = len(symbols) + total_size = sum(s.size for s in symbols) + symbols_with_size = [s for s in symbols if s.size > 0] + symbols_without_size = [s for s in symbols if s.size == 0] + + print("\nSUMMARY:") + print(f" Total symbols: {total_symbols}") + print(f" Symbols with size info: {len(symbols_with_size)}") + print(f" Symbols without size info: {len(symbols_without_size)}") + print( + f" Total symbol size: {total_size} bytes ({total_size / 1024:.1f} KB) [sized symbols only]" + ) + + if enhanced_mode and call_graph and reverse_call_graph: + print(f" Functions with calls: {len(call_graph)}") + print(f" Functions called by others: {len(reverse_call_graph)}") + + # Show source breakdown + source_stats: Dict[str, int] = {} + for sym in symbols: + source = sym.source + if source not in source_stats: + source_stats[source] = 0 + source_stats[source] += 1 + + print("\nSYMBOL SOURCES:") + for source, count in sorted(source_stats.items()): + print(f" {source}: {count} symbols") + + # Largest symbols overall + print("\nLARGEST SYMBOLS (all symbols, sorted by size):") + symbols_sorted = sorted(symbols, key=lambda x: x.size, reverse=True) + + display_count = 30 if enhanced_mode else 50 + for i, sym in enumerate(symbols_sorted[:display_count]): + display_name = sym.demangled_name + print(f" {i + 1:2d}. {sym.size:6d} bytes - {display_name}") + + # Show what functions call this symbol (if enhanced mode and it's a function) + if enhanced_mode and reverse_call_graph and display_name in reverse_call_graph: + callers = reverse_call_graph[display_name] + if callers: + caller_names = [ + name[:40] + "..." if len(name) > 40 else name + for name in callers[:3] + ] + print(f" Called by: {', '.join(caller_names)}") + if len(callers) > 3: + print(f" ... and {len(callers) - 3} more") + + # Show mangled name if different (non-enhanced mode) + elif ( + not enhanced_mode + and hasattr(sym, "demangled_name") + and sym.demangled_name + and sym.demangled_name != sym.name + ): + print( + f" (mangled: {sym.name[:80]}{'...' if len(sym.name) > 80 else ''})" + ) + + # Initialize variables for enhanced mode data + most_called = [] + most_calling = [] + + # Enhanced function call analysis + if enhanced_mode and call_graph and reverse_call_graph: + print("\n" + "=" * 80) + print("FUNCTION CALL ANALYSIS") + print("=" * 80) + + # Most called functions + most_called = sorted( + reverse_call_graph.items(), key=lambda x: len(x[1]), reverse=True + ) + print("\nMOST CALLED FUNCTIONS (functions called by many others):") + for i, (func_name, callers) in enumerate(most_called[:15]): + short_name = func_name[:60] + "..." if len(func_name) > 60 else func_name + print(f" {i + 1:2d}. {short_name}") + print(f" Called by {len(callers)} functions") + if len(callers) <= 5: + for caller in callers: + caller_short = caller[:50] + "..." if len(caller) > 50 else caller + print(f" - {caller_short}") + else: + for caller in callers[:3]: + caller_short = caller[:50] + "..." if len(caller) > 50 else caller + print(f" - {caller_short}") + print(f" ... and {len(callers) - 3} more") + print() + + # Functions that call many others + most_calling = sorted(call_graph.items(), key=lambda x: len(x[1]), reverse=True) + print("\nFUNCTIONS THAT CALL MANY OTHERS:") + for i, (func_name, callees) in enumerate(most_calling[:10]): + short_name = func_name[:60] + "..." if len(func_name) > 60 else func_name + print(f" {i + 1:2d}. {short_name}") + print(f" Calls {len(callees)} functions") + if len(callees) <= 5: + for callee in callees: + callee_short = callee[:50] + "..." if len(callee) > 50 else callee + print(f" -> {callee_short}") + else: + for callee in callees[:3]: + callee_short = callee[:50] + "..." if len(callee) > 50 else callee + print(f" -> {callee_short}") + print(f" ... and {len(callees) - 3} more") + print() + + # Symbol type breakdown + section_title = "\n" + "=" * 80 + "\n" if enhanced_mode else "\n" + print(section_title + "SYMBOL TYPE BREAKDOWN:") + type_stats = TypeStats() + for sym in symbols: + type_stats.add_symbol(sym) + + for sym_type, stats in type_stats.items(): + print( + f" {sym_type}: {stats.count} symbols, {stats.total_size} bytes ({stats.total_size / 1024:.1f} KB)" + ) + + # Dependencies analysis + if dependencies: + print("\nMODULE DEPENDENCIES:") + for module in sorted(dependencies.keys()): + if dependencies[module]: # Only show modules with dependencies + print(f" {module}:") + for symbol in dependencies[module][:5]: # Show first 5 symbols + print(f" - {symbol}") + if len(dependencies[module]) > 5: + print(f" ... and {len(dependencies[module]) - 5} more") + + # Build return data + report_data = AnalysisReport( + board=board_name, + total_symbols=total_symbols, + total_size=total_size, + largest_symbols=symbols_sorted[:20], + type_breakdown=list(type_stats.values()), + dependencies=dependencies, + ) + + # Add enhanced data if available + if enhanced_mode and call_graph and reverse_call_graph: + report_data.call_graph = call_graph + report_data.reverse_call_graph = reverse_call_graph + report_data.call_stats = CallStats( + functions_with_calls=len(call_graph), + functions_called_by_others=len(reverse_call_graph), + most_called=most_called[:10], + most_calling=most_calling[:10], + ) + + return report_data + + +def find_board_build_info(board_name: Optional[str] = None) -> Tuple[Path, str]: + """Find build info for a specific board or detect available boards""" + # Detect build directory + current = Path.cwd() + build_dir: Optional[Path] = None + while current != current.parent: + candidate = current / ".build" + if candidate.exists(): + build_dir = candidate + break + current = current.parent + + if not build_dir: + print("Error: Could not find build directory (.build)") + sys.exit(1) + + # If specific board requested, look for it + if board_name: + # 1) Direct board directory: .build//build_info.json + board_dir = build_dir / board_name + if board_dir.exists() and (board_dir / "build_info.json").exists(): + return board_dir / "build_info.json", board_name + + # 2) PlatformIO nested directory: .build/pio//build_info.json + pio_board_dir = build_dir / "pio" / board_name + if pio_board_dir.exists() and (pio_board_dir / "build_info.json").exists(): + return pio_board_dir / "build_info.json", board_name + + print(f"Error: Board '{board_name}' not found or missing build_info.json") + sys.exit(1) + + # Otherwise, find any available board + available_boards: List[Tuple[Path, str]] = [] + + # 1) Direct children of .build + for item in build_dir.iterdir(): + if item.is_dir(): + build_info_file = item / "build_info.json" + if build_info_file.exists(): + available_boards.append((build_info_file, item.name)) + + # 2) Nested PlatformIO structure .build/pio/* + pio_dir = build_dir / "pio" + if pio_dir.exists() and pio_dir.is_dir(): + for item in pio_dir.iterdir(): + if item.is_dir(): + build_info_file = item / "build_info.json" + if build_info_file.exists(): + available_boards.append((build_info_file, item.name)) + + if not available_boards: + print(f"Error: No boards with build_info.json found in {build_dir}") + sys.exit(1) + + # Return the first available board + return available_boards[0] + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Enhanced symbol analysis with optional function call analysis for any platform" + ) + parser.add_argument( + "--board", help="Board name to analyze (e.g., uno, esp32dev, esp32s3)" + ) + parser.add_argument( + "--no-enhanced", + action="store_false", + dest="enhanced", + default=True, + help="Disable enhanced analysis with function call graph (enhanced is default)", + ) + parser.add_argument( + "--show-calls-to", + help="Show what functions call a specific function (enables enhanced mode)", + ) + parser.add_argument( + "--basic", + action="store_true", + help="Use basic nm-only symbol analysis (for backward compatibility). Default is comprehensive analysis with readelf + nm", + ) + args = parser.parse_args() + + # Enable enhanced mode if show-calls-to is specified + enhanced_mode = args.enhanced or args.show_calls_to + + # Use comprehensive symbol analysis by default, basic only if requested + comprehensive_symbols = not args.basic + + # Find build info + build_info_path, board_name = find_board_build_info(args.board) + print(f"Found build info for {board_name}: {build_info_path}") + + with open(build_info_path) as f: + build_info = json.load(f) + + # Get board info using proper board mapping + board = create_board(board_name) + real_board_name = board.get_real_board_name() + + # Try the real board name first, then fall back to directory name + if real_board_name in build_info: + board_info = build_info[real_board_name] + actual_board_key = real_board_name + if real_board_name != board_name: + print( + f"Note: Using board key '{real_board_name}' from board mapping (directory was '{board_name}')" + ) + elif board_name in build_info: + board_info = build_info[board_name] + actual_board_key = board_name + else: + # Try to find the actual board key in the JSON as fallback + board_keys = list(build_info.keys()) + if len(board_keys) == 1: + actual_board_key = board_keys[0] + board_info = build_info[actual_board_key] + print( + f"Note: Using only available board key '{actual_board_key}' from build_info.json (expected '{real_board_name}' or '{board_name}')" + ) + else: + print( + f"Error: Could not find board '{real_board_name}' or '{board_name}' in build_info.json" + ) + print(f"Available board keys: {board_keys}") + sys.exit(1) + + nm_path = board_info["aliases"]["nm"] + cppfilt_path = board_info["aliases"]["c++filt"] + elf_file = board_info["prog_path"] + + # Get readelf path (derive from nm path if not in aliases) + if "readelf" in board_info["aliases"]: + readelf_path = board_info["aliases"]["readelf"] + else: + # Derive readelf path from nm path (replace 'nm' with 'readelf') + readelf_path = nm_path.replace("-nm", "-readelf") + + # Find map file + map_file = Path(elf_file).with_suffix(".map") + + print(f"Analyzing ELF file: {elf_file}") + print(f"Using nm tool: {nm_path}") + print(f"Using c++filt tool: {cppfilt_path}") + print(f"Using readelf tool: {readelf_path}") + if enhanced_mode: + objdump_path = board_info["aliases"]["objdump"] + print(f"Using objdump tool: {objdump_path}") + print(f"Map file: {map_file}") + + # Analyze symbols + if comprehensive_symbols: + symbols = analyze_symbols(elf_file, nm_path, cppfilt_path, readelf_path) + else: + symbols = analyze_symbols(elf_file, nm_path, cppfilt_path) + + # Analyze function calls if enhanced mode + call_graph = {} + reverse_call_graph = {} + if enhanced_mode: + objdump_path = board_info["aliases"]["objdump"] + call_graph = analyze_function_calls(elf_file, objdump_path, cppfilt_path) + reverse_call_graph = build_reverse_call_graph(call_graph) + + # Analyze dependencies + dependencies = analyze_map_file(map_file) + + # Handle specific function query + if args.show_calls_to: + target_function = args.show_calls_to + print("\n" + "=" * 80) + print(f"FUNCTIONS THAT CALL: {target_function}") + print("=" * 80) + + # Find functions that call the target (exact match first) + exact_callers = reverse_call_graph.get(target_function, []) + + # Also search for partial matches + partial_matches: Dict[str, List[str]] = {} + for func_name, callers in reverse_call_graph.items(): + if ( + target_function.lower() in func_name.lower() + and func_name != target_function + ): + partial_matches[func_name] = callers + + if exact_callers: + print(f"\nExact match - Functions calling '{target_function}':") + for i, caller in enumerate(exact_callers, 1): + print(f" {i}. {caller}") + + if partial_matches: + print(f"\nPartial matches - Functions containing '{target_function}':") + for func_name, callers in partial_matches.items(): + print(f"\n Function: {func_name}") + print(f" Called by {len(callers)} functions:") + for caller in callers[:5]: + print(f" - {caller}") + if len(callers) > 5: + print(f" ... and {len(callers) - 5} more") + + if not exact_callers and not partial_matches: + print(f"No functions found that call '{target_function}'") + print("Available functions (first 20):") + available_funcs = sorted(reverse_call_graph.keys())[:20] + for func in available_funcs: + print(f" - {func}") + + return # Exit early for specific query + + # Generate report using user-friendly board name + report = generate_report( + board_name.upper(), + symbols, + dependencies, + call_graph, + reverse_call_graph, + enhanced_mode, + ) + + # Save detailed data to JSON (sorted by size, largest first) + # Find the build directory (go up from wherever we are to find .build) + current = Path.cwd() + while current != current.parent: + build_dir = current / ".build" + if build_dir.exists(): + filename_suffix = ( + "_enhanced_symbol_analysis.json" + if enhanced_mode + else "_symbol_analysis.json" + ) + output_file = build_dir / f"{board_name}{filename_suffix}" + break + current = current.parent + else: + # Fallback to current directory if .build not found + filename_suffix = ( + "_enhanced_symbol_analysis.json" + if enhanced_mode + else "_symbol_analysis.json" + ) + output_file = Path(f"{board_name}{filename_suffix}") + + # Create detailed analysis data structure + detailed_data = DetailedAnalysisData( + summary=report, + all_symbols_sorted_by_size=sorted(symbols, key=lambda x: x.size, reverse=True), + dependencies=dependencies, + call_graph=call_graph if enhanced_mode else None, + reverse_call_graph=reverse_call_graph if enhanced_mode else None, + ) + + with open(output_file, "w") as f: + json.dump(asdict(detailed_data), f, indent=2) + + description = ( + "enhanced analysis with complete call graph" + if enhanced_mode + else "basic symbol analysis" + ) + print(f"\nDetailed {description} saved to: {output_file}") + + if not enhanced_mode: + print("This file contains ALL symbols without any filtering or classification.") + print( + "Enhanced mode with function call graph analysis is enabled by default. Use --no-enhanced to disable." + ) + else: + print("This file contains ALL symbols and complete call graph analysis.") + + +if __name__ == "__main__": + main() diff --git a/v1.5/lib/FastLED/ci/util/test_args.py b/v1.5/lib/FastLED/ci/util/test_args.py new file mode 100644 index 0000000..ee6bcb3 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/test_args.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys +from pathlib import Path +from typing import Optional + +from typeguard import typechecked + +from ci.util.test_types import TestArgs + + +def _python_test_exists(test_name: str) -> bool: + """Check if a Python test file exists for the given test name""" + # Check for the test file in ci/tests directory + tests_dir = Path("ci/tests") + + # Try various naming patterns for Python tests + possible_names = [ + f"test_{test_name}.py", + f"{test_name}.py", + ] + + for name in possible_names: + test_file = tests_dir / name + if test_file.exists(): + return True + + return False + + +def parse_args(args: Optional[list[str]] = None) -> TestArgs: + """Parse command line arguments""" + parser = argparse.ArgumentParser(description="Run FastLED tests") + parser.add_argument( + "--cpp", + action="store_true", + help="Run C++ tests only (equivalent to --unit --examples, suppresses Python tests)", + ) + parser.add_argument("--unit", action="store_true", help="Run C++ unit tests only") + parser.add_argument("--py", action="store_true", help="Run Python tests only") + parser.add_argument( + "test", + type=str, + nargs="?", + default=None, + help="Specific test to run (Python or C++)", + ) + + # Create mutually exclusive group for compiler selection + compiler_group = parser.add_mutually_exclusive_group() + compiler_group.add_argument( + "--clang", action="store_true", help="Use Clang compiler" + ) + compiler_group.add_argument( + "--gcc", action="store_true", help="Use GCC compiler (default on non-Windows)" + ) + + parser.add_argument( + "--clean", action="store_true", help="Clean build before compiling" + ) + parser.add_argument( + "--no-interactive", + action="store_true", + help="Force non-interactive mode (no confirmation prompts)", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Enable interactive mode (allows confirmation prompts)", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable verbose output showing all test details", + ) + parser.add_argument( + "--show-compile", + action="store_true", + help="Show compilation commands and output", + ) + parser.add_argument( + "--show-link", + action="store_true", + help="Show linking commands and output", + ) + parser.add_argument( + "--quick", action="store_true", help="Enable quick mode with FASTLED_ALL_SRC=1" + ) + parser.add_argument( + "--no-stack-trace", + action="store_true", + help="Disable stack trace dumping on timeout", + ) + parser.add_argument( + "--check", + action="store_true", + help="Enable static analysis (IWYU, clang-tidy) - auto-enables --cpp and --clang", + ) + parser.add_argument( + "--examples", + nargs="*", + help="Run example compilation tests only (optionally specify example names). Use with --full for complete compilation + linking + execution", + ) + parser.add_argument( + "--no-pch", + action="store_true", + help="Disable precompiled headers (PCH) when running example compilation tests", + ) + parser.add_argument( + "--unity", + action="store_true", + help="Enable UNITY build mode for examples - compile all source files as a single unit for improved performance", + ) + parser.add_argument( + "--no-unity", + action="store_true", + help="Disable unity builds for cpp tests and examples", + ) + parser.add_argument( + "--full", + action="store_true", + help="Run full integration tests including compilation + linking + program execution", + ) + + parser.add_argument( + "--no-parallel", + action="store_true", + help="Force sequential test execution", + ) + parser.add_argument( + "--unity-chunks", + type=int, + default=1, + help="Number of unity chunks when building libfastled.a (default: 1)", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug mode for C++ unit tests (e.g., full debug symbols)", + ) + parser.add_argument( + "--qemu", + nargs="+", + help="Run examples in QEMU emulation. Usage: --qemu esp32s3 [example_names...]", + ) + + parsed_args = parser.parse_args(args) + + # Convert argparse.Namespace to TestArgs dataclass + test_args = TestArgs( + cpp=parsed_args.cpp, + unit=parsed_args.unit, + py=parsed_args.py, + test=parsed_args.test, + clang=parsed_args.clang, + gcc=parsed_args.gcc, + clean=parsed_args.clean, + no_interactive=parsed_args.no_interactive, + interactive=parsed_args.interactive, + verbose=parsed_args.verbose, + show_compile=parsed_args.show_compile, + show_link=parsed_args.show_link, + quick=parsed_args.quick, + no_stack_trace=parsed_args.no_stack_trace, + check=parsed_args.check, + examples=parsed_args.examples, + no_pch=parsed_args.no_pch, + unity=parsed_args.unity, + no_unity=parsed_args.no_unity, + full=parsed_args.full, + no_parallel=parsed_args.no_parallel, + unity_chunks=parsed_args.unity_chunks, + debug=parsed_args.debug, + qemu=parsed_args.qemu, + ) + + # Auto-enable --py or --cpp mode when a specific test is provided + if test_args.test: + # Check if this is a Python test first + if _python_test_exists(test_args.test): + # This is a Python test - enable Python mode + if not test_args.py and not test_args.cpp: + test_args.py = True + print( + f"Auto-enabled --py mode for specific Python test: {test_args.test}" + ) + else: + # This is not a Python test - assume it's a C++ test + if not test_args.cpp and not test_args.py: + test_args.cpp = True + print(f"Auto-enabled --cpp mode for specific test: {test_args.test}") + # Also enable --unit when a specific C++ test is provided without any other flags + if ( + not test_args.unit + and not test_args.examples + and not test_args.py + and not test_args.full + ): + test_args.unit = True + print(f"Auto-enabled --unit mode for specific test: {test_args.test}") + + # Auto-enable --verbose when running unit tests (disabled) + # if test_args.unit and not test_args.verbose: + # test_args.verbose = True + # print("Auto-enabled --verbose mode for unit tests") + + # Auto-enable --cpp and --clang when --check is provided + if test_args.check: + if not test_args.cpp: + test_args.cpp = True + print("Auto-enabled --cpp mode for static analysis (--check)") + if not test_args.clang and not test_args.gcc: + test_args.clang = True + print("Auto-enabled --clang compiler for static analysis (--check)") + + # Auto-enable --cpp and --quick when --examples is provided + if test_args.examples is not None: + if not test_args.cpp: + test_args.cpp = True + print("Auto-enabled --cpp mode for example compilation (--examples)") + if not test_args.quick: + test_args.quick = True + print( + "Auto-enabled --quick mode for faster example compilation (--examples)" + ) + + # Handle --full flag behavior + if test_args.full: + if test_args.examples is not None: + # --examples --full: Run examples with full compilation+linking+execution + print("Full examples mode: compilation + linking + program execution") + else: + # --full alone: Run integration tests + if not test_args.cpp: + test_args.cpp = True + print("Auto-enabled --cpp mode for full integration tests (--full)") + print("Full integration tests: compilation + linking + program execution") + + # Default to Clang on Windows unless --gcc is explicitly passed + if sys.platform == "win32" and not test_args.gcc and not test_args.clang: + test_args.clang = True + print("Windows detected: defaulting to Clang compiler (use --gcc to override)") + elif test_args.gcc: + print("Using GCC compiler") + elif test_args.clang: + print("Using Clang compiler") + + # Validate conflicting arguments + if test_args.no_interactive and test_args.interactive: + print( + "Error: --interactive and --no-interactive cannot be used together", + file=sys.stderr, + ) + sys.exit(1) + + # Set NO_PARALLEL environment variable if --no-parallel is used + if test_args.no_parallel: + os.environ["NO_PARALLEL"] = "1" + print("Forcing sequential execution (NO_PARALLEL=1)") + + return test_args diff --git a/v1.5/lib/FastLED/ci/util/test_commands.py b/v1.5/lib/FastLED/ci/util/test_commands.py new file mode 100644 index 0000000..773d2df --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/test_commands.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import subprocess +import sys +from pathlib import Path +from typing import Any, List + +from ci.util.running_process import RunningProcess +from ci.util.test_types import TestArgs + + +def build_cpp_test_command(args: TestArgs) -> str: + """Build the C++ test command based on arguments""" + cmd_list = ["uv", "run", "python", "-m", "ci.compiler.cpp_test_run"] + + if args.clang: + cmd_list.append("--clang") + + if args.test: + cmd_list.append("--test") + cmd_list.append(args.test) + if args.clean: + cmd_list.append("--clean") + if args.verbose: + cmd_list.append("--verbose") # Always pass verbose flag when enabled + if args.show_compile: + cmd_list.append("--show-compile") # Pass show-compile flag + if args.show_link: + cmd_list.append("--show-link") # Pass show-link flag + if args.check: + cmd_list.append("--check") + + if args.no_unity: + cmd_list.append("--no-unity") + + return subprocess.list2cmdline(cmd_list) + + +def make_pio_check_cmd() -> List[str]: + """Create the PlatformIO check command""" + return [ + "pio", + "check", + "--skip-packages", + "--src-filters=+", + "--severity=medium", + "--fail-on-defect=high", + "--flags", + "--inline-suppr --enable=all --std=c++17", + ] + + +def make_compile_uno_test_process(enable_stack_trace: bool = True) -> RunningProcess: + """Create a process to compile the uno tests""" + cmd = [ + "uv", + "run", + "python", + "-m", + "ci.ci-compile", + "uno", + "--examples", + "Blink", + "--no-interactive", + ] + return RunningProcess(cmd, auto_run=True, enable_stack_trace=enable_stack_trace) + + +def run_command(cmd: List[str], **kwargs: Any) -> None: + """Run a command and handle errors""" + try: + subprocess.run(cmd, check=True, **kwargs) + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) diff --git a/v1.5/lib/FastLED/ci/util/test_env.py b/v1.5/lib/FastLED/ci/util/test_env.py new file mode 100644 index 0000000..a0a1866 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/test_env.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +import os +import platform +import sys +import threading +import time +import traceback +from pathlib import Path +from typing import Protocol, cast + +import psutil + +from ci.util.test_types import TestArgs + + +class ReconfigurableIO(Protocol): + def reconfigure(self, *, encoding: str, errors: str) -> None: ... + + +def setup_environment(args: TestArgs) -> None: + """Set up the test environment based on arguments""" + # Handle --quick flag + if args.quick: + os.environ["FASTLED_ALL_SRC"] = "1" + print("Quick mode enabled. FASTLED_ALL_SRC=1") + + # Handle build flags + if args.show_compile: + os.environ["FASTLED_TEST_SHOW_COMPILE"] = "1" + if args.show_link: + os.environ["FASTLED_TEST_SHOW_LINK"] = "1" + + +def setup_windows_console() -> None: + """Configure Windows console for UTF-8 output""" + if os.name == "nt": # Windows + if hasattr(sys.stdout, "reconfigure"): + cast(ReconfigurableIO, sys.stdout).reconfigure( + encoding="utf-8", errors="replace" + ) + if hasattr(sys.stderr, "reconfigure"): + cast(ReconfigurableIO, sys.stderr).reconfigure( + encoding="utf-8", errors="replace" + ) + + +def get_process_tree_info(pid: int) -> str: + """Get information about a process and its children""" + try: + process = psutil.Process(pid) + info = [f"Process {pid} ({process.name()})"] + info.append(f"Status: {process.status()}") + info.append(f"CPU Times: {process.cpu_times()}") + info.append(f"Memory: {process.memory_info()}") + + # Get child processes + children = process.children(recursive=True) + if children: + info.append("\nChild processes:") + for child in children: + info.append(f" Child {child.pid} ({child.name()})") + info.append(f" Status: {child.status()}") + info.append(f" CPU Times: {child.cpu_times()}") + info.append(f" Memory: {child.memory_info()}") + + return "\n".join(info) + except: + return f"Could not get process info for PID {pid}" + + +def kill_process_tree(pid: int) -> None: + """Kill a process and all its children""" + try: + parent = psutil.Process(pid) + children = parent.children(recursive=True) + + # First try graceful termination + for child in children: + try: + child.terminate() + except psutil.NoSuchProcess: + pass + + # Give them a moment to terminate + _, alive = psutil.wait_procs(children, timeout=3) + + # Force kill any that are still alive + for child in alive: + try: + child.kill() + except psutil.NoSuchProcess: + pass + + # Finally terminate the parent + try: + parent.terminate() + parent.wait(3) # Give it 3 seconds to terminate + except (psutil.NoSuchProcess, psutil.TimeoutExpired): + try: + parent.kill() # Force kill if still alive + except psutil.NoSuchProcess: + pass + except psutil.NoSuchProcess: + pass # Process already gone + + +def dump_thread_stacks() -> None: + """Dump stack trace of the main thread and process tree info""" + print("\n=== MAIN THREAD STACK TRACE ===") + for thread in threading.enumerate(): + print(f"\nThread {thread.name}:") + if thread.ident is not None: + frame = sys._current_frames().get(thread.ident) + if frame: + traceback.print_stack(frame) + print("=== END STACK TRACE ===\n") + + # Dump process tree information + print("\n=== PROCESS TREE INFO ===") + print(get_process_tree_info(os.getpid())) + print("=== END PROCESS TREE INFO ===\n") + + +def setup_watchdog(timeout: int = 60) -> threading.Thread: + """Start a watchdog timer to kill the process if it takes too long + + Args: + timeout: Number of seconds to wait before killing process (default: 60 seconds) + """ + + def watchdog_timer() -> None: + time.sleep(timeout) + print( + f"\n🚨 WATCHDOG TIMER EXPIRED - Process took too long! ({timeout} seconds)" + ) + dump_thread_stacks() + + # Kill all child processes and then ourselves + kill_process_tree(os.getpid()) + + # If we're still here, force exit + time.sleep(1) # Give a moment for output to flush + os._exit(2) # Exit with error code 2 to indicate timeout + + watchdog = threading.Thread( + target=watchdog_timer, daemon=True, name="WatchdogTimer" + ) + watchdog.start() + return watchdog + + +def setup_force_exit() -> threading.Thread: + """Set up a force exit daemon thread""" + + def force_exit() -> None: + time.sleep(1) + print("Force exit daemon thread invoked") + kill_process_tree(os.getpid()) + os._exit(1) + + daemon_thread = threading.Thread( + target=force_exit, daemon=True, name="ForceExitDaemon" + ) + daemon_thread.start() + return daemon_thread diff --git a/v1.5/lib/FastLED/ci/util/test_exceptions.py b/v1.5/lib/FastLED/ci/util/test_exceptions.py new file mode 100644 index 0000000..221e447 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/test_exceptions.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Custom exceptions for test failures that need to bubble up to callers.""" + +import subprocess +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class TestFailureInfo: + """Information about a single test failure""" + + test_name: str + command: str + return_code: int + output: str + error_type: str = "test_failure" + + +class FastLEDTestException(Exception): + """Base exception for FastLED test failures""" + + def __init__(self, message: str, failures: Optional[List[TestFailureInfo]] = None): + super().__init__(message) + self.failures = failures or [] + self.message = message + + def add_failure(self, failure: TestFailureInfo) -> None: + """Add a test failure to this exception""" + self.failures.append(failure) + + def has_failures(self) -> bool: + """Check if this exception contains any failures""" + return len(self.failures) > 0 + + def get_failure_summary(self) -> str: + """Get a summary of all failures""" + if not self.failures: + return self.message + + summary = [self.message] + summary.append(f"\nFailed tests ({len(self.failures)}):") + for failure in self.failures: + summary.append( + f" - {failure.test_name}: {failure.error_type} (exit code {failure.return_code})" + ) + + return "\n".join(summary) + + def get_detailed_failure_info(self) -> str: + """Get detailed information about all failures""" + if not self.failures: + return self.message + + details = [self.message] + details.append(f"\n{'=' * 50}") + details.append("DETAILED FAILURE INFORMATION") + details.append(f"{'=' * 50}") + + for i, failure in enumerate(self.failures, 1): + cmd_str: str = ( + subprocess.list2cmdline(failure.command) + if isinstance(failure.command, list) + else failure.command + ) + assert isinstance(cmd_str, str) + details.append(f"\n{i}. {failure.test_name}") + details.append(f" Command: {cmd_str}") + details.append(f" Error Type: {failure.error_type}") + details.append(f" Exit Code: {failure.return_code}") + details.append(f" Output:") + # Indent the output + for line in failure.output.split("\n"): + details.append(f" {line}") + + return "\n".join(details) + + +class CompilationFailedException(FastLEDTestException): + """Exception for compilation failures""" + + def __init__( + self, + message: str = "Compilation failed", + failures: Optional[List[TestFailureInfo]] = None, + ): + super().__init__(message, failures) + + +class TestExecutionFailedException(FastLEDTestException): + """Exception for test execution failures""" + + def __init__( + self, + message: str = "Test execution failed", + failures: Optional[List[TestFailureInfo]] = None, + ): + super().__init__(message, failures) + + +class TestTimeoutException(FastLEDTestException): + """Exception for test timeouts""" + + def __init__( + self, + message: str = "Test execution timed out", + failures: Optional[List[TestFailureInfo]] = None, + ): + super().__init__(message, failures) diff --git a/v1.5/lib/FastLED/ci/util/test_runner.py b/v1.5/lib/FastLED/ci/util/test_runner.py new file mode 100644 index 0000000..16684a4 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/test_runner.py @@ -0,0 +1,1457 @@ +#!/usr/bin/env python3 +""" +WARNING: sys.stdout.flush() causes blocking issues on Windows with QEMU/subprocess processes! +Use conditional flushing: `if os.name != 'nt': sys.stdout.flush()` to avoid Windows blocking +while maintaining real-time output visibility on Unix systems. +""" + +import _thread +import os +import queue +import re +import subprocess +import sys +import threading +import time +import traceback +from dataclasses import dataclass +from pathlib import Path +from queue import Queue +from typing import Any, Callable, List, Optional, Pattern, Protocol, cast + +from typeguard import typechecked + +from ci.util.running_process import EndOfStream, RunningProcess +from ci.util.test_exceptions import ( + TestExecutionFailedException, + TestFailureInfo, + TestTimeoutException, +) +from ci.util.test_types import ( + TestArgs, + TestCategories, + TestResult, + TestResultType, + TestSuiteResult, + determine_test_categories, +) + + +_IS_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" +_TIMEOUT = 600 if _IS_GITHUB_ACTIONS else 240 +_GLOBAL_TIMEOUT = 600 if _IS_GITHUB_ACTIONS else 300 + +# Abort threshold for total failures across all processes (unit + examples) +MAX_FAILURES_BEFORE_ABORT = 3 + + +def extract_error_snippet(accumulated_output: list[str], context_lines: int = 5) -> str: + """ + Extract relevant error snippets from process output. + + Searches for lines containing "error" (case insensitive) and extracts + a small context window around the first few error occurrences. + + Args: + accumulated_output: List of output lines from the process + context_lines: Number of lines to capture before/after each error line (default: 5) + + Returns: + Formatted string containing error snippets with minimal context + """ + if not accumulated_output: + return "No output captured" + + error_snippets: list[str] = [] + error_pattern = re.compile(r"error", re.IGNORECASE) + + # Find all lines that contain "error" (case insensitive) + error_line_indices: list[int] = [] + for i, line in enumerate(accumulated_output): + if error_pattern.search(line): + error_line_indices.append(i) + + if not error_line_indices: + # No specific errors found, return last 10 lines which might contain useful info + max_lines = min(10, len(accumulated_output)) + return ( + "No 'error' keyword found. Last " + + str(max_lines) + + " lines:\n" + + "\n".join(accumulated_output[-max_lines:]) + ) + + # Extract context around first 5 errors (increased from 2 for better visibility) + max_errors_to_show = 5 + for i, error_idx in enumerate(error_line_indices[:max_errors_to_show]): + # Calculate context window (5 lines before to 5 lines after the error) + start_idx = max(0, error_idx - context_lines) + end_idx = min(len(accumulated_output), error_idx + context_lines + 1) + + snippet_lines: list[str] = [] + + for j in range(start_idx, end_idx): + line_marker = "➤ " if j == error_idx else " " # Mark the actual error line + snippet_lines.append(f"{line_marker}{accumulated_output[j]}") + + error_snippets.append("\n".join(snippet_lines)) + + # Add summary if there are more errors - but show the actual error lines + if len(error_line_indices) > max_errors_to_show: + remaining_errors = error_line_indices[max_errors_to_show:] + additional_error_lines: list[str] = [] + for error_idx in remaining_errors[:3]: # Show up to 3 more error lines + additional_error_lines.append(f"➤ {accumulated_output[error_idx]}") + + if additional_error_lines: + error_snippets.append("Additional errors found:") + error_snippets.append("\n".join(additional_error_lines)) + + if len(remaining_errors) > 3: + error_snippets.append( + f"... and {len(remaining_errors) - 3} more error(s) found" + ) + + return "\n\n".join(error_snippets) + + +@dataclass +class ProcessTiming: + """Information about a completed process execution for timing summary""" + + name: str + duration: float + command: str + + +@dataclass +class ProcessState: + """Tracks the state of an individual process during parallel execution""" + + process: RunningProcess + last_activity_time: float + command: str + + +@typechecked +class TestOutputFormatter: + """Formats test output in a consistent way""" + + def __init__(self, verbose: bool = False): + self.verbose = verbose + self.current_suite: Optional[TestSuiteResult] = None + self._start_time = time.time() + + def start_suite(self, name: str) -> None: + """Start a new test suite""" + self.current_suite = TestSuiteResult( + name=name, results=[], start_time=time.time() + ) + + def end_suite(self, passed: bool = True) -> None: + """End the current test suite""" + if self.current_suite: + self.current_suite.end_time = time.time() + self.current_suite.passed = passed + + def add_result(self, result: TestResult) -> None: + """Add a test result to the current suite""" + if self.current_suite: + self.current_suite.results.append(result) + self._format_result(result) + + def _format_result(self, result: TestResult) -> None: + """Format and display a test result""" + # Only show if verbose or important + if not (self.verbose or self._should_display(result)): + return + + # Format timestamp + delta = result.timestamp - self._start_time + + # Format message based on type + if result.type == TestResultType.SUCCESS: + color = "\033[92m" # Green + elif result.type == TestResultType.ERROR: + color = "\033[91m" # Red + elif result.type == TestResultType.WARNING: + color = "\033[93m" # Yellow + else: + color = "\033[0m" # Reset + + # Build message - only include timestamp, not command name + msg = f"{delta:.2f} " + msg += f"{color}{result.message}\033[0m" + + # Print with indentation + print(f" {msg}") + + def _should_display(self, result: TestResult) -> bool: + """Determine if a result should be displayed""" + # Always show errors + if result.type == TestResultType.ERROR: + return True + + # Always show final success/completion messages + if any( + marker in result.message + for marker in ["### SUCCESS", "### ERROR", "Test execution complete:"] + ): + return True + + # Show warnings in verbose mode + if result.type == TestResultType.WARNING and self.verbose: + return True + + # Show info/debug only in verbose mode + return self.verbose and result.type in [ + TestResultType.INFO, + TestResultType.DEBUG, + ] + + +@typechecked +@dataclass +class TestProcessConfig: + """Configuration for a test process""" + + command: str | list[str] + echo: bool = True + auto_run: bool = False + timeout: int | None = None + enable_stack_trace: bool = True + description: str = "" + parallel_safe: bool = True # Whether this process can run in parallel with others + output_filter: Callable[[str], bool] | None = ( + None # Filter function for output lines + ) + + +class ProcessOutputHandler: + """Handles capturing and displaying process output with structured results""" + + def __init__(self, verbose: bool = False): + self.formatter = TestOutputFormatter(verbose=verbose) + self.current_command: str | None = None + self.header_printed = False + + def handle_output_line(self, line: str, process_name: str) -> None: + """Process a line of output and convert it to structured test results""" + # Start new test suite if command changes + if process_name != self.current_command: + if self.current_command: + self.formatter.end_suite() + self.formatter.start_suite(process_name) + self.current_command = process_name + self.header_printed = True + self.formatter.add_result( + TestResult( + type=TestResultType.INFO, + message=f"=== [{process_name}] ===", + test_name=process_name, + ) + ) + + # Convert line to appropriate test result + result = self._parse_line_to_result(line, process_name) + if result: + self.formatter.add_result(result) + + def _parse_line_to_result( + self, line: str, process_name: str + ) -> Optional[TestResult]: + """Parse a line of output into a structured test result""" + # Skip empty lines + if not line.strip(): + return None + + # Skip test output noise + if any( + noise in line + for noise in [ + "doctest version is", + 'run with "--help"', + "assertions:", + "test cases:", + "MESSAGE:", + "TEST CASE:", + "Test passed", + "Test execution", + "Test completed", + "Running test:", + "Process completed:", + "Command completed:", + "Command output:", + "Exit code:", + "All parallel tests", + "JSON parsing failed", + "readFrameAt failed", + "ByteStreamMemory", + "C:\\Users\\", + "\\dev\\fastled\\", + "\\tests\\", + "\\src\\", + "test line_simplification.exe", + "test noise_hires.exe", + "test mutex.exe", + "test rbtree.exe", + "test priority_queue.exe", + "test json_roundtrip.exe", + "test malloc_hooks.exe", + "test rectangular_buffer.exe", + "test ostream.exe", + "test active_strip_data_json.exe", + "test noise_range.exe", + "test json.exe", + "test point.exe", + "test screenmap.exe", + "test task.exe", + "test queue.exe", + "test promise.exe", + "test shared_ptr.exe", + "test screenmap_serialization.exe", + "test strstream.exe", + "test slice.exe", + "test hsv_conversion_accuracy.exe", + "test tile2x2.exe", + "test tuple.exe", + "test raster.exe", + "test transform.exe", + "test transition_ramp.exe", + "test thread_local.exe", + "test set_inlined.exe", + "test vector.exe", + "test strip_id_map.exe", + "test weak_ptr.exe", + "test variant.exe", + "test type_traits.exe", + "test splat.exe", + "test hsv16.exe", + "test ui_help.exe", + "test unordered_set.exe", + "test traverse_grid.exe", + "test ui_title_bug.exe", + "test videofx_wrapper.exe", + "test ui.exe", + "test video.exe", + "test xypath.exe", + ] + ): + return None + + # Success messages + if "### SUCCESS" in line: + return TestResult( + type=TestResultType.SUCCESS, message=line, test_name=process_name + ) + + # Error messages + if any( + marker in line + for marker in [ + "### ERROR", + "FAILED", + "ERROR", + "Crash", + "Test FAILED", + "Compilation failed", + "Build failed", + ] + ): + return TestResult( + type=TestResultType.ERROR, message=line, test_name=process_name + ) + + # Warning messages + if any(marker in line.lower() for marker in ["warning:", "note:"]): + return TestResult( + type=TestResultType.WARNING, message=line, test_name=process_name + ) + + # Test completion messages + if "Test execution complete:" in line: + return TestResult( + type=TestResultType.INFO, message=line, test_name=process_name + ) + + # Default to info level for other lines + return TestResult( + type=TestResultType.INFO, message=line, test_name=process_name + ) + + +class ReconfigurableIO(Protocol): + def reconfigure(self, *, encoding: str, errors: str) -> None: ... + + +def create_namespace_check_process(enable_stack_trace: bool) -> RunningProcess: + """Create a namespace check process without starting it""" + return RunningProcess( + "uv run python ci/tests/no_using_namespace_fl_in_headers.py", + shell=True, + auto_run=False, # Don't auto-start - will be started in parallel later + enable_stack_trace=enable_stack_trace, + ) + + +def create_unit_test_process( + args: TestArgs, enable_stack_trace: bool +) -> RunningProcess: + """Create a unit test process without starting it""" + # First compile the tests + compile_cmd: list[str] = [ + "uv", + "run", + "python", + "-m", + "ci.compiler.cpp_test_run", + "--compile-only", + ] + if args.test: + compile_cmd.extend(["--test", args.test]) + if args.clean: + compile_cmd.append("--clean") + if args.verbose: + compile_cmd.append("--verbose") + if args.show_compile: + compile_cmd.append("--show-compile") + if args.show_link: + compile_cmd.append("--show-link") + if args.check: + compile_cmd.append("--check") + + if args.clang: + compile_cmd.append("--clang") + if args.gcc: + compile_cmd.append("--gcc") + if args.no_unity: + compile_cmd.append("--no-unity") + if args.no_pch: + compile_cmd.append("--no-pch") + if args.debug: + compile_cmd.append("--debug") + + # subprocess.run(compile_cmd, check=True) + + # Then run the tests using our new test runner + test_cmd = ["uv", "run", "python", "-m", "ci.run_tests"] + if args.test: + test_cmd.extend(["--test", str(args.test)]) + if args.verbose: + test_cmd.append("--verbose") + + both_cmds: list[str] = [] + both_cmds.extend(compile_cmd) + both_cmds.extend(["&&"]) + both_cmds.extend(test_cmd) + cmd_str = subprocess.list2cmdline(both_cmds) + + return RunningProcess( + cmd_str, + enable_stack_trace=enable_stack_trace, + timeout=_TIMEOUT, # 2 minutes timeout + auto_run=True, + ) + + +def create_examples_test_process( + args: TestArgs, enable_stack_trace: bool +) -> RunningProcess: + """Create an examples test process without starting it""" + cmd = ["uv", "run", "python", "-u", "ci/compiler/test_example_compilation.py"] + if args.examples is not None: + cmd.extend(args.examples) + if args.clean: + cmd.append("--clean") + if args.no_pch: + cmd.append("--no-pch") + if args.unity: + cmd.append("--unity") + if args.full and args.examples is not None: + cmd.append("--full") + elif args.examples is not None: + # Auto-enable full mode for examples to include execution + cmd.append("--full") + if args.no_parallel: + cmd.append("--no-parallel") + if args.verbose: + cmd.append("--verbose") + + # Use longer timeout for no-parallel mode since sequential compilation takes much longer + timeout = ( + 1800 if args.no_parallel else 600 + ) # 30 minutes for sequential, 10 minutes for parallel + + cmd_str = subprocess.list2cmdline(cmd) + + return RunningProcess( + cmd_str, auto_run=False, enable_stack_trace=enable_stack_trace, timeout=timeout + ) + + +def create_python_test_process( + enable_stack_trace: bool, full_tests: bool = False +) -> RunningProcess: + """Create a Python test process without starting it""" + # Use list format for better environment handling + cmd = [ + "uv", + "run", + "pytest", + "-s", # Don't capture stdout/stderr + "-v", # Verbose output + "--tb=short", # Shorter traceback format + "--durations=0", # Show all durations + "ci/tests", # Test directory + ] + + # If not running full tests, exclude tests marked with @pytest.mark.full + if not full_tests: + cmd.extend(["-m", "not full"]) + + cmd_str = subprocess.list2cmdline(cmd) + + return RunningProcess( + cmd_str, + auto_run=False, # Don't auto-start - will be started in parallel later + enable_stack_trace=False, # Always disable stack traces for Python tests + timeout=_TIMEOUT, # 2 minute timeout for Python tests + ) + + +def create_integration_test_process( + args: TestArgs, enable_stack_trace: bool +) -> RunningProcess: + """Create an integration test process without starting it""" + cmd = ["uv", "run", "pytest", "-s", "ci/test_integration", "-xvs", "--durations=0"] + if args.examples is not None: + # When --examples --full is specified, only run example-related integration tests + cmd.extend(["-k", "TestFullProgramLinking"]) + if args.verbose: + cmd.append("-v") + return RunningProcess(cmd, auto_run=False, enable_stack_trace=enable_stack_trace) + + +def create_compile_uno_test_process(enable_stack_trace: bool = True) -> RunningProcess: + """Create a process to compile the uno tests without starting it""" + cmd = [ + "uv", + "run", + "python", + "-m", + "ci.ci-compile", + "uno", + "--examples", + "Blink", + "--no-interactive", + ] + return RunningProcess(cmd, auto_run=False, enable_stack_trace=enable_stack_trace) + + +def get_cpp_test_processes( + args: TestArgs, test_categories: TestCategories, enable_stack_trace: bool +) -> list[RunningProcess]: + """Return all processes needed for C++ tests""" + processes: list[RunningProcess] = [] + + # Always include namespace check + processes.append(create_namespace_check_process(enable_stack_trace)) + + if test_categories.unit: + processes.append(create_unit_test_process(args, enable_stack_trace)) + + if test_categories.examples: + processes.append(create_examples_test_process(args, enable_stack_trace)) + + return processes + + +def get_python_test_processes( + enable_stack_trace: bool, full_tests: bool = False +) -> list[RunningProcess]: + """Return all processes needed for Python tests""" + return [ + create_python_test_process(False, full_tests) + ] # Disable stack trace for Python tests + + +def get_integration_test_processes( + args: TestArgs, enable_stack_trace: bool +) -> list[RunningProcess]: + """Return all processes needed for integration tests""" + return [create_integration_test_process(args, enable_stack_trace)] + + +def get_all_test_processes( + args: TestArgs, + test_categories: TestCategories, + enable_stack_trace: bool, + src_code_change: bool, +) -> list[RunningProcess]: + """Return all processes needed for all tests""" + processes: list[RunningProcess] = [] + + # Always include namespace check + processes.append(create_namespace_check_process(enable_stack_trace)) + + # Add test processes based on categories + if test_categories.unit: + processes.append(create_unit_test_process(args, enable_stack_trace)) + if test_categories.examples: + processes.append(create_examples_test_process(args, enable_stack_trace)) + if test_categories.py: + processes.append( + create_python_test_process(False) + ) # Disable stack trace for Python tests + if test_categories.integration: + processes.append(create_integration_test_process(args, enable_stack_trace)) + + # Add uno test process if source code changed + if src_code_change: + processes.append(create_compile_uno_test_process(enable_stack_trace)) + + return processes + + +def _extract_test_name(command: str | list[str]) -> str: + """Extract a human-readable test name from a command""" + if isinstance(command, list): + command = " ".join(command) + + # Extract test name patterns + if "--test " in command: + # Extract specific test name after --test flag + parts = command.split("--test ") + if len(parts) > 1: + test_name = parts[1].split()[0] + return test_name + elif ".exe" in command: + # Extract from executable name + for part in command.split(): + if part.endswith(".exe"): + return part.replace(".exe", "") + elif "python" in command and "-m " in command: + # Extract module name + parts = command.split("-m ") + if len(parts) > 1: + module = parts[1].split()[0] + return module.replace("ci.compiler.", "").replace("ci.", "") + elif "python" in command and command.endswith(".py"): + # Extract script name + for part in command.split(): + if part.endswith(".py"): + return part.split("/")[-1].replace(".py", "") + + # Fallback to first meaningful part of command + parts = command.split() + for part in parts[1:]: # Skip 'uv' or 'python' + if not part.startswith("-") and "python" not in part: + return part + + return "unknown_test" + + +def _get_friendly_test_name(command: str | list[str]) -> str: + """Extract a user-friendly test name for display in summary table""" + if isinstance(command, list): + command = " ".join(command) + + # Simplify common command patterns to friendly names + if "cpp_test_run" in command and "ci.run_tests" in command: + return "unit_tests" + elif "test_example_compilation.py" in command: + # Show script name plus example targets, e.g. "test_example_compilation.py Luminova" + try: + import os + + tokens = command.split() + # Find the script token and collect following non-flag args as examples + for i, tok in enumerate(tokens): + normalized = tok.strip('"') + if normalized.endswith("test_example_compilation.py"): + script_name = os.path.basename(normalized) + example_parts: list[str] = [] + for t in tokens[i + 1 :]: + if t.startswith("-"): + break + example_parts.append(t.strip('"')) + if example_parts: + return f"{script_name} {' '.join(example_parts)}" + return script_name + except Exception: + # Fall back to generic extraction on any unexpected parsing issue + pass + return _extract_test_name(command) + elif "pytest" in command and "ci/tests" in command: + return "python_tests" + elif "pytest" in command and "ci/test_integration" in command: + return "integration_tests" + elif "ci-compile" in command and "uno" in command: + return "uno_compilation" + else: + # Fallback to the existing extraction logic + return _extract_test_name(command) + + +def _format_timing_summary(process_timings: List[ProcessTiming]) -> str: + """Format a summary table of process execution times""" + if not process_timings: + return "" + + # Sort by duration (longest first) + sorted_timings = sorted(process_timings, key=lambda x: x.duration, reverse=True) + + # Calculate column widths + max_name_width = max(len(timing.name) for timing in sorted_timings) + max_name_width = max(max_name_width, len("Test Name")) # Ensure header fits + + # Create header + header = f"{'Test Name':<{max_name_width}} | {'Duration':>10}" + separator = f"{'-' * max_name_width}-+-{'-' * 10}" + + # Create rows + rows: list[str] = [] + for timing in sorted_timings: + duration_str = f"{timing.duration:.2f}s" + row = f"{timing.name:<{max_name_width}} | {duration_str:>10}" + rows.append(row) + + # Combine all parts + table_lines = ( + [ + "\nTest Execution Summary:", + separator, + header, + separator, + ] + + rows + + [separator] + ) + + return "\n".join(table_lines) + + +def _handle_process_completion( + proc_state: ProcessState, + active_processes: list[RunningProcess], + completed_timings: List[ProcessTiming], + last_activity_time: dict[RunningProcess, float], +) -> None: + """ + Handle completion of a single test process + + Args: + proc_state: State information for the completed process + active_processes: List of currently active processes + completed_timings: List to collect timing data + last_activity_time: Dictionary tracking activity times + + Raises: + TestExecutionFailedException: If process failed + """ + proc = proc_state.process + cmd = proc_state.command + if isinstance(cmd, list): + cmd = subprocess.list2cmdline(cmd) + + try: + returncode = proc.wait() + if returncode != 0: + test_name = _extract_test_name(cmd) + print(f"\nCommand failed: {cmd} with return code {returncode}") + print(f"\033[91m###### ERROR ######\033[0m") + print(f"Test failed: {test_name}") + + # Capture the actual output from the failed process + try: + actual_output = proc.stdout + if actual_output.strip(): + print(f"\n=== ACTUAL OUTPUT FROM FAILED PROCESS ===") + print(actual_output) + print(f"=== END OF OUTPUT ===") + else: + actual_output = "No output captured from failed process" + except Exception as e: + actual_output = f"Error capturing output: {e}" + + # Flush output for real-time visibility (but avoid on Windows due to blocking issues) + if os.name != "nt": # Only flush on non-Windows systems + sys.stdout.flush() + for p in active_processes: + if p != proc: + p.kill() + failure = TestFailureInfo( + test_name=test_name, + command=str(cmd), + return_code=returncode, + output=actual_output, + error_type="command_failure", + ) + raise TestExecutionFailedException("Test command failed", [failure]) + + active_processes.remove(proc) + if proc in last_activity_time: + del last_activity_time[proc] # Clean up tracking + print(f"Process completed: {cmd}") + + # Collect timing data for summary + if proc.duration is not None: + timing = ProcessTiming( + name=_get_friendly_test_name(cmd), + duration=proc.duration, + command=str(cmd), + ) + completed_timings.append(timing) + + # Flush output for real-time visibility (but avoid on Windows due to blocking issues) + if os.name != "nt": # Only flush on non-Windows systems + sys.stdout.flush() + + except Exception as e: + test_name = _extract_test_name(cmd) + print(f"\nError waiting for process: {cmd}") + print(f"Error: {e}") + print(f"\033[91m###### ERROR ######\033[0m") + print(f"Test error: {test_name}") + + # Try to capture any available output + try: + actual_output = proc.stdout + if actual_output.strip(): + print(f"\n=== PROCESS OUTPUT BEFORE ERROR ===") + print(actual_output) + print(f"=== END OF OUTPUT ===") + except Exception as output_error: + print(f"Could not capture process output: {output_error}") + + failures: list[TestFailureInfo] = [] + for p in active_processes: + p.kill() + # Try to capture output from this process too + try: + process_output = p.stdout if hasattr(p, "stdout") else str(e) + except Exception: + process_output = str(e) + + failures.append( + TestFailureInfo( + test_name=_extract_test_name(p.command), + command=str(p.command), + return_code=1, + output=process_output, + error_type="process_wait_error", + ) + ) + raise TestExecutionFailedException("Error waiting for process", failures) + + +@dataclass +class StuckProcessSignal: + """Signal from monitoring thread that a process is stuck""" + + process: RunningProcess + timeout_duration: float + + +class ProcessStuckMonitor: + """Manages individual monitoring threads for stuck process detection""" + + def __init__(self, stuck_process_timeout: float): + self.stuck_process_timeout = stuck_process_timeout + self.stuck_signals: Queue[StuckProcessSignal] = Queue() + self.monitoring_threads: dict[RunningProcess, threading.Thread] = {} + self.shutdown_event = threading.Event() + + def start_monitoring(self, process: RunningProcess) -> None: + """Start a monitoring thread for the given process""" + if process in self.monitoring_threads: + return # Already monitoring this process + + monitor_thread = threading.Thread( + target=self._monitor_process, + args=(process,), + name=f"StuckMonitor-{_extract_test_name(process.command)}", + daemon=True, + ) + self.monitoring_threads[process] = monitor_thread + monitor_thread.start() + + def stop_monitoring(self, process: RunningProcess) -> None: + """Stop monitoring a specific process""" + if process in self.monitoring_threads: + del self.monitoring_threads[process] + + def shutdown(self) -> None: + """Shutdown all monitoring threads""" + self.shutdown_event.set() + self.monitoring_threads.clear() + + def check_for_stuck_processes(self) -> list[StuckProcessSignal]: + """Check for any stuck process signals from monitoring threads""" + stuck_processes: list[StuckProcessSignal] = [] + try: + while True: + signal = self.stuck_signals.get_nowait() + stuck_processes.append(signal) + except queue.Empty: + pass + return stuck_processes + + def _monitor_process(self, process: RunningProcess) -> None: + """Monitor a single process for being stuck (runs in separate thread)""" + thread_id = threading.current_thread().ident + thread_name = threading.current_thread().name + + try: + last_activity_time = time.time() + + while not self.shutdown_event.is_set(): + if process.finished: + # Process completed normally, stop monitoring + return + + # Check if we have recent stdout activity + stdout_time = process.time_last_stdout_line() + if stdout_time is not None: + last_activity_time = stdout_time + + # Check if process is stuck + current_time = time.time() + if current_time - last_activity_time > self.stuck_process_timeout: + # Process is stuck, signal the main thread + signal = StuckProcessSignal( + process=process, timeout_duration=self.stuck_process_timeout + ) + self.stuck_signals.put(signal) + return + + # Sleep briefly before next check + time.sleep(1.0) # Check every second + + except KeyboardInterrupt: + print(f"🛑 Thread {thread_id} ({thread_name}) caught KeyboardInterrupt") + print(f"📍 Stack trace for thread {thread_id}:") + traceback.print_exc() + _thread.interrupt_main() + raise + except Exception as e: + print(f"❌ Thread {thread_id} ({thread_name}) unexpected error: {e}") + traceback.print_exc() + _thread.interrupt_main() + raise + + +def _handle_stuck_processes( + stuck_signals: list[StuckProcessSignal], + active_processes: list[RunningProcess], + failed_processes: list[str], + monitor: ProcessStuckMonitor, +) -> None: + """ + Handle stuck processes reported by monitoring threads + + Args: + stuck_signals: List of stuck process signals from monitoring threads + active_processes: List of currently active processes (modified in place) + failed_processes: List to track failed process commands + monitor: The process stuck monitor instance + """ + for signal in stuck_signals: + proc = signal.process + if proc in active_processes: + print( + f"\nProcess appears stuck (no output for {signal.timeout_duration}s): {proc.command}" + ) + print("Killing stuck process and its children...") + proc.kill() # This now kills the entire process tree + + # Track this as a failure + failed_processes.append(subprocess.list2cmdline(proc.command)) + + active_processes.remove(proc) + monitor.stop_monitoring(proc) + print(f"Killed stuck process: {proc.command}") + + +def _run_processes_parallel( + processes: list[RunningProcess], verbose: bool = False +) -> List[ProcessTiming]: + """ + DEPRECATED: Use RunningProcessGroup instead. + + This function has been replaced by RunningProcessGroup.run() + for better maintainability and consistency. + + Run multiple test processes in parallel and handle their output + + Args: + processes: List of RunningProcess objects to execute + + Returns: + List of ProcessTiming objects with execution times + """ + if not processes: + return [] + + # Create a shared output handler for formatting + output_handler = ProcessOutputHandler(verbose=verbose) + + # Configure Windows console for UTF-8 output if needed + if os.name == "nt": # Windows + if hasattr(sys.stdout, "reconfigure"): + cast(ReconfigurableIO, sys.stdout).reconfigure( + encoding="utf-8", errors="replace" + ) + if hasattr(sys.stderr, "reconfigure"): + cast(ReconfigurableIO, sys.stderr).reconfigure( + encoding="utf-8", errors="replace" + ) + + # Start processes that aren't already running + for proc in processes: + cmd_str = proc.get_command_str() + if proc.proc is None: # Only start if not already running + proc.run() + print(f"Started: {cmd_str}") + else: + print(f"Process already running: {cmd_str}") + + # Monitor all processes for output and completion + active_processes = processes.copy() + start_time = time.time() + + runner_timeouts: list[int] = [p.timeout for p in processes if p.timeout is not None] + global_timeout: int | None = None + if runner_timeouts: + global_timeout = max(runner_timeouts) + 60 # Add 1 minute buffer + + # Track last activity time for each process to detect stuck processes + last_activity_time = {proc: time.time() for proc in active_processes} + stuck_process_timeout = _GLOBAL_TIMEOUT + + # Track failed processes for proper error reporting + failed_processes: list[str] = [] # Processes killed due to timeout/stuck + exit_failed_processes: list[ + tuple[RunningProcess, int] + ] = [] # Processes that failed with non-zero exit code + + # Track completed processes for timing summary + completed_timings: List[ProcessTiming] = [] + + # Create thread-based stuck process monitor + stuck_monitor = ProcessStuckMonitor(stuck_process_timeout) + + try: + # Start monitoring threads for each process + for proc in active_processes: + stuck_monitor.start_monitoring(proc) + + def time_expired() -> bool: + if global_timeout is None: + return False + assert global_timeout is not None + return time.time() - start_time > global_timeout + + while active_processes: + # Check global timeout + if time_expired(): + assert global_timeout is not None + print(f"\nGlobal timeout reached after {global_timeout} seconds") + print("\033[91m###### ERROR ######\033[0m") + print("Tests failed due to global timeout") + failures: list[TestFailureInfo] = [] + for p in active_processes: + failed_processes.append( + subprocess.list2cmdline(p.command) + ) # Track all active processes as failed + p.kill() + failures.append( + TestFailureInfo( + test_name=_extract_test_name(p.command), + command=str(p.command), + return_code=1, + output="Process killed due to global timeout", + error_type="global_timeout", + ) + ) + raise TestTimeoutException("Global timeout reached", failures) + + # Check for stuck processes (using threaded monitoring) + stuck_signals = stuck_monitor.check_for_stuck_processes() + if stuck_signals: + _handle_stuck_processes( + stuck_signals, active_processes, failed_processes, stuck_monitor + ) + + # Early abort if failure threshold reached via stuck processes + if ( + len(exit_failed_processes) + len(failed_processes) + ) >= MAX_FAILURES_BEFORE_ABORT: + print( + f"\nExceeded failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining tests." + ) + # Kill any remaining active processes + for p in active_processes: + p.kill() + # Build detailed failures + failures: list[TestFailureInfo] = [] + for proc, exit_code in exit_failed_processes: + error_snippet = extract_error_snippet(proc.accumulated_output) + failures.append( + TestFailureInfo( + test_name=_extract_test_name(proc.command), + command=str(proc.command), + return_code=exit_code, + output=error_snippet, + error_type="exit_failure", + ) + ) + for cmd in failed_processes: + cmd = subprocess.list2cmdline(cmd) + failures.append( + TestFailureInfo( + test_name=_extract_test_name(cmd), + command=str(cmd), + return_code=1, + output="Process was killed due to timeout/stuck detection", + error_type="killed_process", + ) + ) + raise TestExecutionFailedException( + "Exceeded failure threshold", failures + ) + + # Process each active test individually + # Iterate backwards to safely remove processes from the list + any_activity = False + for i in range(len(active_processes) - 1, -1, -1): + proc = active_processes[i] + + if verbose: + with proc.line_iter(timeout=60) as line_iter: + for line in line_iter: + print(line) + + # Check if process has finished + if proc.finished: + # Get the exit code to check for failure + exit_code = proc.wait() + + # Process completed, remove from active list + active_processes.remove(proc) + # Stop monitoring this process + stuck_monitor.stop_monitoring(proc) + + # Check for non-zero exit code (failure) + if exit_code != 0: + print( + f"Process failed with exit code {exit_code}: {proc.command}" + ) + exit_failed_processes.append((proc, exit_code)) + # Early abort if we reached the failure threshold + if ( + len(exit_failed_processes) + len(failed_processes) + ) >= MAX_FAILURES_BEFORE_ABORT: + print( + f"\nExceeded failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining tests." + ) + # Kill remaining active processes + for p in active_processes: + if p is not proc: + p.kill() + # Prepare failures with snippets + failures: list[TestFailureInfo] = [] + for p, code in exit_failed_processes: + error_snippet = extract_error_snippet( + p.accumulated_output + ) + failures.append( + TestFailureInfo( + test_name=_extract_test_name(p.command), + command=str(p.command), + return_code=code, + output=error_snippet, + error_type="exit_failure", + ) + ) + for cmd in failed_processes: + cmd_str = subprocess.list2cmdline(cmd) + failures.append( + TestFailureInfo( + test_name=_extract_test_name(cmd_str), + command=cmd_str, + return_code=1, + output="Process was killed due to timeout/stuck detection", + error_type="killed_process", + ) + ) + raise TestExecutionFailedException( + "Exceeded failure threshold", failures + ) + any_activity = True + continue + + # Update timing information + # Calculate duration properly - if process duration is None, calculate it manually + if proc.duration is not None: + duration = proc.duration + elif proc.start_time is not None: + # Calculate duration from start time to now + duration = time.time() - proc.start_time + else: + duration = 0.0 + + timing = ProcessTiming( + name=_get_friendly_test_name(proc.command), + command=subprocess.list2cmdline(proc.command), + duration=duration, + ) + completed_timings.append(timing) + print(f"Process completed: {proc.get_command_str()}") + any_activity = True + continue + + # Update last activity time if we have stdout activity + stdout_time = proc.time_last_stdout_line() + if stdout_time is not None: + last_activity_time[proc] = stdout_time + any_activity = True + + # Only sleep if no activity was detected, and use a shorter sleep + # This prevents excessive CPU usage while maintaining responsiveness + if not any_activity: + time.sleep(0.01) # 10ms sleep only when no activity + + # Check for processes that failed with non-zero exit codes + if exit_failed_processes: + print(f"\n\033[91m###### ERROR ######\033[0m") + print( + f"Tests failed due to {len(exit_failed_processes)} process(es) with non-zero exit codes:" + ) + for proc, exit_code in exit_failed_processes: + print(f" - {proc.command} (exit code {exit_code})") + failures: list[TestFailureInfo] = [] + for proc, exit_code in exit_failed_processes: + # Extract error snippet from process output + error_snippet = extract_error_snippet(proc.accumulated_output) + + failures.append( + TestFailureInfo( + test_name=_extract_test_name(proc.command), + command=str(proc.command), + return_code=exit_code, + output=error_snippet, + error_type="exit_failure", + ) + ) + raise TestExecutionFailedException("Tests failed", failures) + + # Check for failed processes - CRITICAL FIX + if failed_processes: + print(f"\n\033[91m###### ERROR ######\033[0m") + print(f"Tests failed due to {len(failed_processes)} killed process(es):") + for cmd in failed_processes: + print(f" - {cmd}") + print("Processes were killed due to timeout/stuck detection") + failures: list[TestFailureInfo] = [] + for cmd in failed_processes: + failures.append( + TestFailureInfo( + test_name=_extract_test_name(cmd), + command=str(cmd), + return_code=1, + output="Process was killed due to timeout/stuck detection", + error_type="killed_process", + ) + ) + raise TestExecutionFailedException("Processes were killed", failures) + + print("\nAll parallel tests completed successfully") + return completed_timings + + finally: + # Always shutdown stuck monitoring threads, even on exception + stuck_monitor.shutdown() + + +def run_test_processes( + processes: list[RunningProcess], parallel: bool = True, verbose: bool = False +) -> List[ProcessTiming]: + """ + Run multiple test processes using RunningProcessGroup + + Args: + processes: List of RunningProcess objects to execute + parallel: Whether to run processes in parallel or sequentially (ignored if NO_PARALLEL is set) + verbose: Whether to show all output + + Returns: + List of ProcessTiming objects with execution times + """ + from ci.util.running_process_group import ( + ExecutionMode, + ProcessExecutionConfig, + RunningProcessGroup, + ) + + start_time = time.time() + + # Force sequential execution if NO_PARALLEL is set + if os.environ.get("NO_PARALLEL"): + parallel = False + + if not processes: + print("\033[92m###### SUCCESS ######\033[0m") + print("No tests to run") + return [] + + try: + # Configure execution mode + execution_mode = ( + ExecutionMode.PARALLEL if parallel else ExecutionMode.SEQUENTIAL + ) + + config = ProcessExecutionConfig( + execution_mode=execution_mode, + verbose=verbose, + max_failures_before_abort=MAX_FAILURES_BEFORE_ABORT, + enable_stuck_detection=True, + stuck_timeout_seconds=_GLOBAL_TIMEOUT, + live_updates=True, # Enable real-time display + display_type="auto", # Auto-detect best display format + ) + + # Create and run process group + group = RunningProcessGroup( + processes=processes, config=config, name="TestProcesses" + ) + + # Start real-time display if we have processes and live updates enabled + display_thread = None + if len(processes) > 0 and config.live_updates and not verbose: + # Only show live display if not in verbose mode (verbose already shows all output) + try: + from ci.util.process_status_display import display_process_status + + display_thread = display_process_status( + group, + display_type=config.display_type, + update_interval=config.update_interval, + ) + except ImportError: + pass # Fall back to normal execution if display not available + except Exception: + pass # Fall back to normal execution on any display error + + timings = group.run() + + # Stop display thread if it was started + if display_thread: + # Give it a moment to show final status + time.sleep(0.5) + + # Success message for sequential execution + if not parallel: + elapsed = time.time() - start_time + print(f"\033[92m### SUCCESS ({elapsed:.2f}s) ###\033[0m") + + return timings + + except (TestExecutionFailedException, TestTimeoutException) as e: + # Tests failed - print detailed info and re-raise for proper handling + print("\n" + "=" * 60) + print("FASTLED TEST RUNNER FAILURE DETAILS") + print("=" * 60) + print(e.get_detailed_failure_info()) + print("=" * 60) + raise + except SystemExit as e: + # Tests failed - extract command name from the error + if e.code != 0: + print("\033[91m###### ERROR ######\033[0m") + print(f"Tests failed with exit code {e.code}") + raise + + +def runner(args: TestArgs, src_code_change: bool = True) -> None: + """ + Main test runner function that determines what to run and executes tests + + Args: + args: Parsed command line arguments + src_code_change: Whether source code has changed since last run + """ + print(f"[TEST_RUNNER] Starting runner function") + print(f"[TEST_RUNNER] Args: {args}") + print(f"[TEST_RUNNER] Source code changed: {src_code_change}") + try: + # Determine test categories + test_categories = determine_test_categories(args) + enable_stack_trace = not args.no_stack_trace + + # Build up unified list of all processes to run + processes: list[RunningProcess] = [] + + # Always start with namespace check + processes.append(create_namespace_check_process(enable_stack_trace)) + + # Add unit tests if needed + if test_categories.unit or test_categories.unit_only: + processes.append(create_unit_test_process(args, enable_stack_trace)) + + # Add integration tests if needed + if test_categories.integration or test_categories.integration_only: + processes.append(create_integration_test_process(args, enable_stack_trace)) + + # Add uno compilation test if source changed + if src_code_change and not test_categories.py_only: + processes.append(create_compile_uno_test_process(enable_stack_trace)) + + # Add Python tests if needed + if test_categories.py or test_categories.py_only: + # Pass full_tests=True if we're running integration tests or any form of full tests + full_tests = ( + test_categories.integration + or test_categories.integration_only + or args.full + ) + + processes.append( + create_python_test_process(False, full_tests) + ) # Disable stack trace for Python tests + + # Add example tests if needed + if test_categories.examples or test_categories.examples_only: + processes.append(create_examples_test_process(args, enable_stack_trace)) + + # Determine if we'll run in parallel + will_run_parallel = not bool(os.environ.get("NO_PARALLEL")) + + # Print summary of what we're about to run + execution_mode = "in parallel" if will_run_parallel else "sequentially" + + # Run processes (parallel unless NO_PARALLEL is set) + timings = run_test_processes( + processes, + parallel=will_run_parallel, + verbose=args.verbose, + ) + + # Display timing summary + if timings: + summary = _format_timing_summary(timings) + print(summary) + except (TestExecutionFailedException, TestTimeoutException) as e: + # Print summary and exit with proper code + print(f"\n\033[91m###### ERROR ######\033[0m") + print(f"Tests failed with {len(e.failures)} failure(s)") + + # Exit with appropriate code + if e.failures: + # Use the return code from the first failure, or 1 if none available + exit_code = ( + e.failures[0].return_code if e.failures[0].return_code != 0 else 1 + ) + else: + exit_code = 1 + sys.exit(exit_code) diff --git a/v1.5/lib/FastLED/ci/util/test_types.py b/v1.5/lib/FastLED/ci/util/test_types.py new file mode 100644 index 0000000..3635012 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/test_types.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +import hashlib +import json +import time +from dataclasses import dataclass +from enum import Enum, auto +from pathlib import Path +from typing import Any, Dict, List, Optional + +from typeguard import typechecked + + +class TestResultType(Enum): + """Type of test result message""" + + SUCCESS = auto() + ERROR = auto() + WARNING = auto() + INFO = auto() + DEBUG = auto() + + +@typechecked +@dataclass +class TestResult: + """Structured test result""" + + type: TestResultType + message: str + test_name: Optional[str] = None + details: Optional[Dict[str, Any]] = None + timestamp: float = 0.0 + + def __post_init__(self): + if not self.timestamp: + self.timestamp = time.time() + + +@typechecked +@dataclass +class TestSuiteResult: + """Results for a test suite""" + + name: str + results: List[TestResult] + start_time: float + end_time: Optional[float] = None + passed: bool = True + + @property + def duration(self) -> float: + """Get test duration in seconds""" + if self.end_time is None: + return 0.0 + return self.end_time - self.start_time + + +@typechecked +@dataclass +class TestArgs: + """Type-safe test arguments""" + + cpp: bool = False + unit: bool = False + py: bool = False + test: Optional[str] = None + clang: bool = False + gcc: bool = False + clean: bool = False + no_interactive: bool = False + interactive: bool = False + verbose: bool = False + show_compile: bool = False + show_link: bool = False + quick: bool = False + no_stack_trace: bool = False + check: bool = False + examples: Optional[list[str]] = None + no_pch: bool = False + unity: bool = False + no_unity: bool = False # Disable unity builds for cpp tests and examples + full: bool = False + + no_parallel: bool = False # Force sequential test execution + unity_chunks: int = 1 # Number of unity chunks for libfastled build + debug: bool = False # Enable debug mode for unit tests + qemu: Optional[list[str]] = None # Run examples in QEMU emulation + + +@typechecked +@dataclass +class TestCategories: + """Type-safe test category flags""" + + unit: bool + examples: bool + py: bool + integration: bool + unit_only: bool + examples_only: bool + py_only: bool + integration_only: bool + qemu_esp32s3: bool + qemu_esp32s3_only: bool + + def __post_init__(self): + # Type validation + for field_name in [ + "unit", + "examples", + "py", + "integration", + "unit_only", + "examples_only", + "py_only", + "integration_only", + "qemu_esp32s3", + "qemu_esp32s3_only", + ]: + value = getattr(self, field_name) + if not isinstance(value, bool): + raise TypeError(f"{field_name} must be bool, got {type(value)}") + + +@typechecked +@dataclass +class FingerprintResult: + """Type-safe fingerprint result""" + + hash: str + elapsed_seconds: Optional[str] = None + status: Optional[str] = None + + +def process_test_flags(args: TestArgs) -> TestArgs: + """Process and validate test execution flags""" + + # Check which specific test flags are provided + specific_flags = [ + args.unit, + args.examples is not None, + args.py, + args.full, + args.qemu is not None, + ] + specific_count = sum(bool(flag) for flag in specific_flags) + + # If --cpp is provided, default to --unit --examples (no Python) + if args.cpp and specific_count == 0: + args.unit = True + args.examples = [] # Empty list means run all examples + print("--cpp mode: Running unit tests and examples (Python tests suppressed)") + return args + + # If any specific flags are provided, ONLY run those (exclusive behavior) + if specific_count > 0: + # When specific flags are provided, disable everything else unless explicitly set + if not args.unit: + # Unit was not explicitly set, so disable it + pass # args.unit is already False + + # Auto-enable verbose mode for unit tests (disabled) + # if args.unit and not args.verbose: + # args.verbose = True + # print("Auto-enabled --verbose mode for unit tests") + if args.examples is None: + # Examples was not explicitly set, so disable it + pass # args.examples is already None + if not args.py: + # Python was not explicitly set, so disable it + pass # args.py is already False + if not args.full: + # Full was not explicitly set, so disable it + pass # args.full is already False + + # Log what's running + enabled_tests: list[str] = [] + if args.unit: + enabled_tests.append("unit tests") + if args.examples is not None: + enabled_tests.append("examples") + if args.py: + enabled_tests.append("Python tests") + if args.full: + if args.examples is not None: + enabled_tests.append("full example integration tests") + else: + enabled_tests.append("full integration tests") + + print(f"Specific test flags provided: Running only {', '.join(enabled_tests)}") + return args + + # If no specific flags, run everything (backward compatibility) + if specific_count == 0: + args.unit = True + args.examples = [] # Empty list means run all examples + args.py = True + print("No test flags specified: Running all tests (unit, examples, Python)") + + # Auto-enable verbose mode for unit tests (disabled) + # if args.unit and not args.verbose: + # args.verbose = True + # print("Auto-enabled --verbose mode for unit tests") + + return args + + return args + + +def determine_test_categories(args: TestArgs) -> TestCategories: + """Determine which test categories should run based on flags""" + unit_enabled = args.unit + examples_enabled = args.examples is not None + py_enabled = args.py + # Integration tests only run when --full is used alone (not with --examples) + integration_enabled = args.full and args.examples is None + qemu_esp32s3_enabled = args.qemu is not None + + return TestCategories( + unit=unit_enabled, + examples=examples_enabled, + py=py_enabled, + integration=integration_enabled, + qemu_esp32s3=qemu_esp32s3_enabled, + unit_only=unit_enabled + and not examples_enabled + and not py_enabled + and not integration_enabled + and not qemu_esp32s3_enabled, + examples_only=examples_enabled + and not unit_enabled + and not py_enabled + and not integration_enabled + and not qemu_esp32s3_enabled, + py_only=py_enabled + and not unit_enabled + and not examples_enabled + and not integration_enabled + and not qemu_esp32s3_enabled, + integration_only=integration_enabled + and not unit_enabled + and not examples_enabled + and not py_enabled + and not qemu_esp32s3_enabled, + qemu_esp32s3_only=qemu_esp32s3_enabled + and not unit_enabled + and not examples_enabled + and not py_enabled + and not integration_enabled, + ) + + +def fingerprint_code_base( + start_directory: Path, glob: str = "**/*.h,**/*.cpp,**/*.hpp" +) -> FingerprintResult: + """ + Create a fingerprint of the code base by hashing file contents. + + Args: + start_directory: The root directory to start scanning from + glob: Comma-separated list of glob patterns to match files + + Returns: + A FingerprintResult with hash and optional status + """ + try: + hasher = hashlib.sha256() + patterns = glob.split(",") + + # Get all matching files + all_files: list[Path] = [] + for pattern in patterns: + pattern = pattern.strip() + all_files.extend(sorted(start_directory.glob(pattern))) + + # Sort files for consistent ordering + all_files.sort() + + # Process each file + for file_path in all_files: + if file_path.is_file(): + # Add the relative path to the hash + rel_path = file_path.relative_to(start_directory) + hasher.update(str(rel_path).encode("utf-8")) + + # Add the file content to the hash + try: + with open(file_path, "rb") as f: + # Read in chunks to handle large files + for chunk in iter(lambda: f.read(4096), b""): + hasher.update(chunk) + except Exception as e: + # If we can't read the file, include the error in the hash + hasher.update(f"ERROR:{str(e)}".encode("utf-8")) + + return FingerprintResult(hash=hasher.hexdigest()) + except Exception as e: + return FingerprintResult(hash="", status=f"error: {str(e)}") + + +def calculate_fingerprint(root_dir: Path | None = None) -> FingerprintResult: + """ + Calculate the code base fingerprint. + + Args: + root_dir: The root directory to start scanning from. If None, uses src directory. + + Returns: + The fingerprint result + """ + if root_dir is None: + root_dir = Path.cwd() / "src" + + start_time = time.time() + # Compute the fingerprint + result = fingerprint_code_base(root_dir) + elapsed_time = time.time() - start_time + # Add timing information to the result + result.elapsed_seconds = f"{elapsed_time:.2f}" + + return result diff --git a/v1.5/lib/FastLED/ci/util/tools.py b/v1.5/lib/FastLED/ci/util/tools.py new file mode 100644 index 0000000..79c30c2 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/tools.py @@ -0,0 +1,211 @@ +# pyright: reportUnknownMemberType=false +""" +Tools for working with build info and tool paths. +""" + +import json +import os +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict + +from ci.util.paths import BUILD + + +@dataclass +class Tools: + as_path: Path + ld_path: Path + objcopy_path: Path + objdump_path: Path + cpp_filt_path: Path + nm_path: Path + + +def load_tools(build_info_path: Path) -> Tools: + build_info: Dict[str, Any] = json.loads(build_info_path.read_text()) + board_info: Dict[str, Any] = build_info[next(iter(build_info))] + aliases: Dict[str, str] = board_info["aliases"] + as_path = Path(aliases["as"]) + ld_path = Path(aliases["ld"]) + objcopy_path = Path(aliases["objcopy"]) + objdump_path = Path(aliases["objdump"]) + cpp_filt_path = Path(aliases["c++filt"]) + nm_path = Path(aliases["nm"]) + if sys.platform == "win32": + as_path = as_path.with_suffix(".exe") + ld_path = ld_path.with_suffix(".exe") + objcopy_path = objcopy_path.with_suffix(".exe") + objdump_path = objdump_path.with_suffix(".exe") + cpp_filt_path = cpp_filt_path.with_suffix(".exe") + nm_path = nm_path.with_suffix(".exe") + out = Tools(as_path, ld_path, objcopy_path, objdump_path, cpp_filt_path, nm_path) + tools = [as_path, ld_path, objcopy_path, objdump_path, cpp_filt_path, nm_path] + for tool in tools: + if not tool.exists(): + raise FileNotFoundError(f"Tool not found: {tool}") + return out + + +def _list_builds() -> list[Path]: + str_paths = os.listdir(BUILD) + paths = [BUILD / p for p in str_paths] + dirs = [p for p in paths if p.is_dir()] + return dirs + + +def _check_build(build: Path) -> bool: + # 1. should contain a build_info.json file + # 2. should contain a .pio/build directory + has_build_info = (build / "build_info.json").exists() + has_pio_build = (build / ".pio" / "build").exists() + return has_build_info and has_pio_build + + +def _prompt_build() -> Path: + builds = _list_builds() + if not builds: + print("Error: No builds found", file=sys.stderr) + sys.exit(1) + print("Select a build:") + for i, build in enumerate(builds): + print(f" [{i}]: {build}") + while True: + try: + which = int(input("Enter the number of the build to use: ")) + if 0 <= which < len(builds): + valid = _check_build(BUILD / builds[which]) + if valid: + return BUILD / builds[which] + print("Error: Invalid build", file=sys.stderr) + else: + print("Error: Invalid selection", file=sys.stderr) + continue + except ValueError: + print("Error: Invalid input", file=sys.stderr) + continue + + +def _prompt_object_file(build: Path) -> Path: + # Look for object files in .pio/build directory + build_dir = build / ".pio" / "build" + object_files: list[Path] = [] + + # Walk through build directory to find .o files + for root, _, files in os.walk(build_dir): + for file in files: + if file.endswith(".o") and "FrameworkArduino" not in file: + full_path = Path(root) / file + if "FrameworkArduino" not in full_path.parts: + object_files.append(full_path) + + if not object_files: + print("Error: No object files found", file=sys.stderr) + sys.exit(1) + + print("\nSelect an object file:") + for i, obj_file in enumerate(object_files): + print(f" [{i}]: {obj_file.relative_to(build_dir)}") + + while True: + try: + which = int(input("Enter the number of the object file to use: ")) + if 0 <= which < len(object_files): + return object_files[which] + print("Error: Invalid selection", file=sys.stderr) + except ValueError: + print("Error: Invalid input", file=sys.stderr) + continue + + +def cli() -> None: + import argparse + + parser = argparse.ArgumentParser( + description="Dump object file information using build tools" + ) + + parser.add_argument( + "build_path", + type=Path, + nargs="?", + help="Path to build directory containing build info JSON file", + ) + + parser.add_argument( + "--symbols", action="store_true", help="Dump symbol table using nm" + ) + parser.add_argument( + "--disassemble", action="store_true", help="Dump disassembly using objdump" + ) + + args = parser.parse_args() + build_path = args.build_path + symbols = args.symbols + disassemble = args.disassemble + + # Check if object file was provided and exists + if build_path is None: + build_path = _prompt_build() + else: + if not _check_build(build_path): + print("Error: Invalid build directory", file=sys.stderr) + sys.exit(1) + + assert build_path is not None + assert build_path + + build_info_path = build_path / "build_info.json" + assert build_info_path.exists(), f"File not found: {build_info_path}" + + tools = load_tools(build_info_path) + + if not symbols and not disassemble: + while True: + print( + "Error: Please specify at least one action to perform", file=sys.stderr + ) + action = input( + "Enter 's' to dump symbols, 'd' to disassemble, or 'q' to quit: " + ) + if action == "s": + symbols = True + break + elif action == "d": + disassemble = True + break + elif action == "q": + sys.exit(0) + else: + print("Error: Invalid action", file=sys.stderr) + + object_file = _prompt_object_file(build_path) + if symbols: + import subprocess + + cmd_str = subprocess.list2cmdline( + [str(tools.objdump_path), str(object_file), "--syms"] + ) + print(f"Running command: {cmd_str}") + subprocess.run([str(tools.objdump_path), str(object_file)]) + + if disassemble: + import subprocess + + cmd_str = subprocess.list2cmdline( + [str(tools.objdump_path), "-d", str(object_file)] + ) + print(f"Running command: {cmd_str}") + subprocess.run([str(tools.objdump_path), "-d", str(object_file)]) + + if not (symbols or disassemble): + parser.print_help() + + +if __name__ == "__main__": + try: + cli() + except KeyboardInterrupt: + print("Exiting...") + sys.exit(1) diff --git a/v1.5/lib/FastLED/ci/util/url_utils.py b/v1.5/lib/FastLED/ci/util/url_utils.py new file mode 100644 index 0000000..16d70e8 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/url_utils.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +""" +URL utility functions for PlatformIO and other tools. + +This module provides utilities for working with URLs, particularly for +sanitizing URLs to create filesystem-safe path names. +""" + +import hashlib +from pathlib import Path + + +def sanitize_url_for_path(url: str) -> Path: + """ + Sanitize URL to create a valid filesystem path name. + Simply strips protocol and replaces invalid filesystem characters. + + Args: + url: URL to sanitize + + Returns: + Sanitized Path suitable for use as a filesystem path component + + Example: + >>> sanitize_url_for_path("https://github.com/owner/repo/releases/download/v1.0.0/file.zip") + Path('github_com_owner_repo_releases_download_v1_0_0_file_zip') + """ + # Remove protocol (http://, https://, ftp://, etc.) + if "://" in url: + url = url.split("://", 1)[1] + + # Replace invalid filesystem characters with underscores + # Invalid characters: / \ : * ? " < > | + invalid_chars = ["/", "\\", ":", "*", "?", '"', "<", ">", "|"] + result = url + for char in invalid_chars: + result = result.replace(char, "_") + + # Replace dots with underscores to avoid issues with file extensions + result = result.replace(".", "_") + + # Replace spaces and dashes with underscores for consistency + result = result.replace(" ", "_").replace("-", "_") + + # Remove any remaining non-alphanumeric characters except underscores + result = "".join(c if c.isalnum() or c == "_" else "_" for c in result) + + # Remove multiple consecutive underscores + while "__" in result: + result = result.replace("__", "_") + + # Remove leading/trailing underscores + result = result.strip("_") + + # Ensure reasonable length (max 100 chars) + if len(result) > 100: + # Keep first 70 chars and add a short hash + result = result[:70] + "_" + hashlib.sha256(url.encode()).hexdigest()[:8] + + return Path(result) diff --git a/v1.5/lib/FastLED/ci/util/xcache.py b/v1.5/lib/FastLED/ci/util/xcache.py new file mode 100644 index 0000000..d012610 --- /dev/null +++ b/v1.5/lib/FastLED/ci/util/xcache.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +""" +xcache.py - Enhanced sccache wrapper with response file support + +This trampoline wrapper handles the ESP32S3 sccache problem where long command lines +use response files (@tmpfile.tmp) that sccache doesn't understand. It creates +temporary wrapper scripts that act as compiler aliases. + +WARNING: Never use sys.stdout.flush() in this file! +It causes blocking issues on Windows that hang subprocess processes. +Python's default buffering behavior works correctly across platforms. + +Usage: + xcache.py [args...] + +When xcache detects response file arguments (@file.tmp), it: +1. Creates a temporary wrapper script that acts as the compiler +2. The wrapper script internally calls: sccache "$@" +3. Executes the original command with response files intact +4. The system handles response file expansion normally + +This solves the ESP32S3 issue where commands are too long for direct execution +but need caching support. +""" + +import os +import shutil +import stat +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional + + +# os.environ["XCACHE_DEBUG"] = "1" + + +@dataclass +class XCacheConfig: + """Configuration for xcache wrapper.""" + + sccache_path: str + compiler_path: str + temp_dir: Path + debug: bool = False + + +def find_sccache() -> Optional[str]: + """Find sccache executable in PATH.""" + sccache_path = shutil.which("sccache") + if sccache_path: + return sccache_path + + # Check common locations only if not found in PATH + common_paths = [ + "/usr/local/bin/sccache", + "/usr/bin/sccache", + "/opt/local/bin/sccache", + os.path.expanduser("~/.cargo/bin/sccache"), + ] + + for path in common_paths: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + + return None + + +def detect_response_files(args: List[str]) -> List[str]: + """Detect response file arguments (@file.tmp) in command line.""" + response_files: List[str] = [] + for arg in args: + if arg.startswith("@") and len(arg) > 1: + response_file = arg[1:] + if os.path.isfile(response_file): + response_files.append(response_file) + return response_files + + +def create_compiler_wrapper_script(config: XCacheConfig) -> Path: + """Create temporary wrapper script that acts as a compiler alias.""" + + # Determine platform-appropriate script type + is_windows = os.name == "nt" + script_suffix = ".bat" if is_windows else ".sh" + + # Create temporary script file + script_fd, script_path = tempfile.mkstemp( + suffix=script_suffix, prefix="xcache_compiler_", dir=config.temp_dir + ) + + try: + if is_windows: + # Create Windows batch file + wrapper_content = f'''@echo off +REM Temporary xcache compiler wrapper script +REM Acts as alias for: sccache + +if /i "{config.debug}"=="true" ( + echo XCACHE: Wrapper executing: "{config.sccache_path}" "{config.compiler_path}" %* >&2 +) + +REM Execute sccache with the actual compiler and all arguments (including response files) +"{config.sccache_path}" "{config.compiler_path}" %* +''' + else: + # Create Unix shell script + wrapper_content = f'''#!/bin/bash +# Temporary xcache compiler wrapper script +# Acts as alias for: sccache + +if [ "{config.debug}" = "true" ]; then + echo "XCACHE: Wrapper executing: {config.sccache_path} {config.compiler_path} $@" >&2 +fi + +# Execute sccache with the actual compiler and all arguments (including response files) +exec "{config.sccache_path}" "{config.compiler_path}" "$@" +''' + + # Write wrapper script + with os.fdopen(script_fd, "w", encoding="utf-8") as f: + f.write(wrapper_content) + + # Make script executable (Unix only) + script_path_obj = Path(script_path) + if not is_windows: + script_path_obj.chmod(script_path_obj.stat().st_mode | stat.S_IEXEC) + + return script_path_obj + + except Exception: + # Clean up on error + try: + os.close(script_fd) + os.unlink(script_path) + except Exception: + pass + raise + + +def execute_direct(config: XCacheConfig, args: List[str]) -> int: + """Execute sccache directly without response file handling.""" + command = [config.sccache_path, config.compiler_path] + args + + if config.debug: + print(f"XCACHE: Direct execution: {' '.join(command)}", file=sys.stderr) + + try: + # Use Popen to manually pump stdout/stderr and prevent hanging + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout as requested + text=True, + bufsize=1, # Line buffered + universal_newlines=True, + ) + + # Manually pump output until process finishes + while True: + output = process.stdout.readline() + if output == "" and process.poll() is not None: + break + if output: + print(output.rstrip()) # Print to stdout, remove trailing newlines + + # Wait for process to complete and get return code + return_code = process.wait() + return return_code + + except FileNotFoundError as e: + print(f"XCACHE ERROR: Command not found: {e}") + return 127 + except Exception as e: + print(f"XCACHE ERROR: Execution failed: {e}") + return 1 + + +def execute_with_wrapper(config: XCacheConfig, args: List[str]) -> int: + """Execute using wrapper script for response file handling.""" + response_files = detect_response_files(args) + + if not response_files: + # No response files, use direct execution + return execute_direct(config, args) + + if config.debug: + print(f"XCACHE: Detected response files: {response_files}", file=sys.stderr) + + # Create compiler wrapper script (acts as alias for sccache + compiler) + wrapper_script = None + try: + wrapper_script = create_compiler_wrapper_script(config) + + if config.debug: + print( + f"XCACHE: Created compiler wrapper: {wrapper_script}", file=sys.stderr + ) + # Show wrapper script content for debugging + try: + with open(wrapper_script, "r") as f: + content = f.read() + print(f"XCACHE: Wrapper script content:", file=sys.stderr) + for i, line in enumerate(content.split("\n"), 1): + print(f"XCACHE: {i}: {line}", file=sys.stderr) + except Exception as e: + print(f"XCACHE: Could not read wrapper script: {e}", file=sys.stderr) + + # Execute the original command but replace the compiler with our wrapper + # The wrapper script will handle: sccache "$@" + # Response files are passed through and handled normally by the system + command = [str(wrapper_script)] + args + + if config.debug: + print( + f"XCACHE: Executing with wrapper: {' '.join(command)}", file=sys.stderr + ) + print(f"XCACHE: Wrapper script path: {wrapper_script}", file=sys.stderr) + print( + f"XCACHE: Wrapper script exists: {wrapper_script.exists()}", + file=sys.stderr, + ) + if wrapper_script.exists(): + print( + f"XCACHE: Wrapper script executable: {os.access(wrapper_script, os.X_OK)}", + file=sys.stderr, + ) + + # Use Popen to manually pump stdout/stderr and prevent hanging + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout as requested + text=True, + bufsize=1, # Line buffered + universal_newlines=True, + ) + + # Manually pump output until process finishes + while True: + output = process.stdout.readline() + if output == "" and process.poll() is not None: + break + if output: + print(output.rstrip()) # Print to stdout, remove trailing newlines + + # Wait for process to complete and get return code + return_code = process.wait() + return return_code + + except Exception as e: + print(f"XCACHE ERROR: Wrapper execution failed: {e}", file=sys.stderr) + return 1 + finally: + # Clean up wrapper script + if wrapper_script and wrapper_script.exists(): + try: + wrapper_script.unlink() + except Exception: + pass + + +def main() -> int: + """Main xcache entry point.""" + + # Parse command line + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [args...]", file=sys.stderr) + print("", file=sys.stderr) + print( + "xcache is an enhanced sccache wrapper with response file support.", + file=sys.stderr, + ) + print( + "It handles ESP32S3 long command lines that use @response.tmp files.", + file=sys.stderr, + ) + return 1 + + compiler_path = sys.argv[1] + compiler_args = sys.argv[2:] + + # Check for debug mode + debug = os.environ.get("XCACHE_DEBUG", "").lower() in ("1", "true", "yes") + + # Some ESP-IDF build steps (e.g., linker script generation) invoke the + # compiler purely as a preprocessor (e.g. `-E -P`) on linker scripts. + # These operations are not real compilations and often confuse sccache, + # so bypass the cache entirely in this situation. + if "-E" in compiler_args and "-P" in compiler_args: + cmd = [compiler_path] + compiler_args + return subprocess.call(cmd) + + # Find sccache + sccache_path = find_sccache() + if not sccache_path: + print("XCACHE ERROR: sccache not found in PATH", file=sys.stderr) + print("Please install sccache or ensure it's in your PATH", file=sys.stderr) + return 1 + + # Set up temporary directory + temp_dir = Path(tempfile.gettempdir()) / "xcache" + temp_dir.mkdir(exist_ok=True) + + # Configure xcache + config = XCacheConfig( + sccache_path=sccache_path, + compiler_path=compiler_path, + temp_dir=temp_dir, + debug=debug, + ) + + if debug: + print(f"XCACHE: sccache={sccache_path}", file=sys.stderr) + print(f"XCACHE: compiler={compiler_path}", file=sys.stderr) + print(f"XCACHE: args={compiler_args}", file=sys.stderr) + print(f"XCACHE: temp_dir={temp_dir}", file=sys.stderr) + + # Execute with wrapper handling + return execute_with_wrapper(config, compiler_args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/wasm_compile.py b/v1.5/lib/FastLED/ci/wasm_compile.py new file mode 100644 index 0000000..467b424 --- /dev/null +++ b/v1.5/lib/FastLED/ci/wasm_compile.py @@ -0,0 +1,53 @@ +import argparse +import subprocess +import sys +from typing import List, Tuple + + +def parse_args() -> Tuple[argparse.Namespace, list[str]]: + parser = argparse.ArgumentParser(description="Compile wasm") + parser.add_argument( + "sketch_dir", + nargs="?", + default="examples/wasm", + help="The directory of the sketch to compile", + ) + # return parser.parse_args() + known_args, unknown_args = parser.parse_known_args() + # if -b or --build is in the unknown_args, remove it + if "--build" in unknown_args: + print("WARNING: --build is no longer supported. It will be ignored.") + unknown_args.remove("--build") + if "-b" in unknown_args: + print("WARNING: -b is no longer supported. It will be ignored.") + unknown_args.remove("-b") + return known_args, unknown_args + + +def run_command(cmd_list: List[str]) -> int: + """Run a command and return its exit code.""" + cmd_str = subprocess.list2cmdline(cmd_list) + print(f"Running command: {cmd_str}") + rtn = subprocess.call(cmd_list) + if rtn != 0: + print(f"ERROR: Command {cmd_str} failed with return code {rtn}") + return rtn + + +def main() -> int: + args, unknown_args = parse_args() + + # First run the build command + build_cmd = ["fastled", args.sketch_dir] + unknown_args + build_result = run_command(build_cmd) + + # Then run the compile command + compile_cmd = ["fastled", args.sketch_dir, "--just-compile"] + unknown_args + compile_result = run_command(compile_cmd) + + # Return non-zero if either command failed + return build_result if build_result != 0 else compile_result + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v1.5/lib/FastLED/ci/wasm_test.py b/v1.5/lib/FastLED/ci/wasm_test.py new file mode 100644 index 0000000..d9bcc27 --- /dev/null +++ b/v1.5/lib/FastLED/ci/wasm_test.py @@ -0,0 +1,109 @@ +import asyncio +import os +import sys +import time +from pathlib import Path + +from playwright.async_api import ConsoleMessage, async_playwright # type: ignore + + +HERE = Path(__file__).parent +PROJECT_ROOT = HERE.parent + + +# Ensure Playwright browsers are installed +def install_playwright_browsers(): + print("Installing Playwright browsers...") + try: + # Simulate the `playwright install` command + os.system(f"{sys.executable} -m playwright install chromium") + print("Playwright browsers installed successfully.") + except Exception as e: + print(f"Failed to install Playwright browsers: {e}", file=sys.stderr) + sys.exit(1) + + +# Start an HTTP server on the dynamic port +def start_http_server(port: int, directory: Path): + from fastled import Test # type: ignore + + server_process = Test.spawn_http_server( + directory=directory, port=port, open_browser=False + ) + return server_process + + +async def main() -> None: + install_playwright_browsers() + # Find an available port + port = ( + 8080 # Todo, figure out why the http server ignores any port other than 8080. + ) + print(f"Using port: {port}") + + # Start the HTTP server + os.chdir(str(PROJECT_ROOT)) + directory = Path("examples/wasm/fastled_js") + server_process = start_http_server(port=port, directory=directory) + + try: + # Give the server some time to start + time.sleep(2) + + # Use Playwright to test the server + async with async_playwright() as p: + browser = await p.chromium.launch() + page = await browser.new_page() + + try: + await page.goto(f"http://localhost:{port}", timeout=30000) + + # Listen for console messages + def console_log_handler(msg: ConsoleMessage) -> None: + if "INVALID_OPERATION" in msg.text: + print( + "INVALID_OPERATION detected in console log", file=sys.stderr + ) + raise Exception("INVALID_OPERATION detected in console log") + + page.on("console", console_log_handler) + + # Evaluate and monitor window.frameCallCount + await page.evaluate( + """ + window.frameCallCount = 0; + globalThis.FastLED_onFrame = (jsonStr) => { + console.log('FastLED_onFrame called with:', jsonStr); + window.frameCallCount++; + }; + """ + ) + await page.wait_for_timeout(5000) + + call_count = await page.evaluate("window.frameCallCount") + if call_count > 0: + print( + f"Success: FastLED.js was initialized and FastLED_onFrame was called {call_count} times" + ) + else: + print( + "Error: FastLED.js had something go wrong and FastLED_onFrame was not called within 5 seconds", + file=sys.stderr, + ) + raise Exception("FastLED.js failed to initialize") + + except Exception as e: + print(f"An error occurred: {e}", file=sys.stderr) + raise Exception(f"An error occurred: {e}") from e + + finally: + await browser.close() + + finally: + # Terminate the server process + server_process.terminate() + + +# Run the main function +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/v1.5/lib/FastLED/clean b/v1.5/lib/FastLED/clean new file mode 100644 index 0000000..a5a78a6 --- /dev/null +++ b/v1.5/lib/FastLED/clean @@ -0,0 +1,47 @@ +#!/bin/bash +# Ensure UTF-8 everywhere (console and Python), including on Windows msys/cygwin +case "$OSTYPE" in + msys*|cygwin*) + if command -v chcp.com >/dev/null 2>&1; then + chcp.com 65001 >NUL 2>NUL || chcp.com 65001 >/dev/null 2>&1 + fi + ;; + *) + : + ;; +esac +export LANG=C.UTF-8 +export LC_ALL=C.UTF-8 +export PYTHONUTF8=1 +export PYTHONIOENCODING=UTF-8 + +set -x + +# cd to the directory of the script +cd "$(dirname "$0")" + +rm -rf .venv +rm -rf .build +rm -rf .pio +rm -rf .pio_cache +rm -rf ci/tmp +rm -rf tests/.build + +rm -rf .*_cache +rm -rf __pycache__ +rm -rf .tools + +# JavaScript linting cleanup +rm -rf .js-tools + +rm -rf ci/__pycache__ +rm -rf ci/.*_cache + + + +# remove any CMakeCache.txt files +find . -name "CMakeCache.txt" -type f -delete +rm -f uv.lock + +# Remove generated compile_commands.json +rm -f compile_commands.json diff --git a/v1.5/lib/FastLED/code_of_conduct.md b/v1.5/lib/FastLED/code_of_conduct.md new file mode 100644 index 0000000..441741c --- /dev/null +++ b/v1.5/lib/FastLED/code_of_conduct.md @@ -0,0 +1,134 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting +* "Trolling" and excessive complaints without the due benefit of contributing code. + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders via a bug report with the title [CODE OF CONDUCT] as the beginning text. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + diff --git a/v1.5/lib/FastLED/compile b/v1.5/lib/FastLED/compile new file mode 100644 index 0000000..c681d26 --- /dev/null +++ b/v1.5/lib/FastLED/compile @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +# Function to find Python executable +find_python() { + if command -v python3 &> /dev/null; then + echo "python3" + elif command -v python &> /dev/null; then + echo "python" + else + echo "Python not found. Please install Python 3." + exit 1 + fi +} + +# Check if uv is installed, if not, install it +if ! command -v uv &> /dev/null; then + echo "uv command not found. Installing uv..." + PYTHON=$(find_python) + $PYTHON -m pip install uv +fi + +cd "$(dirname "$0")" +# if .venv not found +if [ ! -d .venv ]; then + # create virtual environment + ./install +fi + +interactive_stmt="" +# if no arguments +if [ $# -eq 0 ]; then + # set interactive statement + interactive_stmt="--interactive" +fi + +echo -e "\nRunning uv run ci/ci-compile.py $interactive_stmt $@\n" +uv run ci/ci-compile.py $interactive_stmt "$@" diff --git a/v1.5/lib/FastLED/compile.bat b/v1.5/lib/FastLED/compile.bat new file mode 100644 index 0000000..b3fb25a --- /dev/null +++ b/v1.5/lib/FastLED/compile.bat @@ -0,0 +1,41 @@ +@echo off +setlocal + +rem Function to find Python executable +where python >nul 2>nul +if %errorlevel% equ 0 ( + set "PYTHON=python" +) else ( + where python3 >nul 2>nul + if %errorlevel% equ 0 ( + set "PYTHON=python3" + ) else ( + echo Python not found. Please install Python 3. + exit /b 1 + ) +) + +rem Check if uv is installed, if not, install it +where uv >nul 2>nul +if %errorlevel% neq 0 ( + echo "uv" command not found. Please install "uv" by running "pip install uv" and try again. + exit /b 1 +) + +rem Change to the directory of the batch file +cd /d "%~dp0" + +rem Check if .venv directory exists +if not exist .venv ( + rem Create virtual environment + call install.bat +) + +set "interactive_stmt=" +rem Check if no arguments were provided +if "%~1"=="" ( + set "interactive_stmt=--interactive" +) + +rem Run the Python script +.venv\Scripts\python.exe ci\ci-compile.py %interactive_stmt% %* diff --git a/v1.5/lib/FastLED/component.mk b/v1.5/lib/FastLED/component.mk new file mode 100644 index 0000000..874ca9b --- /dev/null +++ b/v1.5/lib/FastLED/component.mk @@ -0,0 +1,2 @@ +COMPONENT_ADD_INCLUDEDIRS := ./src src/platforms/esp/32 +COMPONENT_SRCDIRS := ./src src/platforms/esp/32 diff --git a/v1.5/lib/FastLED/cool_projects.md b/v1.5/lib/FastLED/cool_projects.md new file mode 100644 index 0000000..82dd4fa --- /dev/null +++ b/v1.5/lib/FastLED/cool_projects.md @@ -0,0 +1,34 @@ + * Advanced Color Gradient using online version of FastLED. + * https://wokwi.com/projects/285170662915441160 + * LedMapper tool for irregular shapes + * https://github.com/jasoncoon/led-mapper + * list of projects on reddit: + * https://www.reddit.com/r/FastLED/wiki/index/user_examples/ + * mesh networked esp32 with mutli wifi connections for redundancy + + * https://github.com/Souravgoswami/Arduino-FastLED-Cool-Effects + * FastLED-IR: https://github.com/marcmerlin/FastLED-IR + * https://github.com/marcmerlin/NeoMatrix-FastLED-IR?tab=readme-ov-file + + * Tree IR: + * https://www.evilgeniuslabs.org/tree-v2 + + * Esp32 server for fastled + * https://github.com/jasoncoon/esp32-fastled-webserver + + * Strip tease - cool fx for strips + * https://github.com/lpaolini/Striptease?tab=readme-ov-file + + +* Soulematelights: + * https://editor.soulmatelights.com/gallery + + +* https://github.com/marcmerlin/FastLED_NeoMatrix_SmartMatrix_LEDMatrix_GFX_Demos/blob/master/LEDMatrix/Table_Mark_Estes/Table_Mark_Estes.ino + + +* AMAZING processing.js artist + * https://x.com/Hau_kun + +* llm-min.txt + * https://github.com/marv1nnnnn/llm-min.txt \ No newline at end of file diff --git a/v1.5/lib/FastLED/debug_test.html b/v1.5/lib/FastLED/debug_test.html new file mode 100644 index 0000000..4c16ca4 --- /dev/null +++ b/v1.5/lib/FastLED/debug_test.html @@ -0,0 +1,220 @@ + + + + FastLED Pure JavaScript Architecture Debug Test + + + +

FastLED Pure JavaScript Architecture Debug Test

+ +
+

Module Loading Status

+
Loading...
+
+ +
+

Debug Controls

+ + + + +
+ +
+

Debug Console Output

+
+
+ + + + diff --git a/v1.5/lib/FastLED/dev.sh b/v1.5/lib/FastLED/dev.sh new file mode 100644 index 0000000..e7a9f2e --- /dev/null +++ b/v1.5/lib/FastLED/dev.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +uv run dev/dev.py \ No newline at end of file diff --git a/v1.5/lib/FastLED/dev/dev.ino b/v1.5/lib/FastLED/dev/dev.ino new file mode 100644 index 0000000..401ef48 --- /dev/null +++ b/v1.5/lib/FastLED/dev/dev.ino @@ -0,0 +1,17 @@ + + +#if defined(ESP32) + +#include "esp_log.h" + +// use gcc intialize constructor +// to set log level to ESP_LOG_VERBOSE +// before setup() is called +__attribute__((constructor)) +void on_startup() { + esp_log_level_set("*", ESP_LOG_VERBOSE); // set all components to ERROR level +} + +#endif // ESP32 + +#include "../examples/FestivalStick/FestivalStick.ino" diff --git a/v1.5/lib/FastLED/docs/Doxyfile b/v1.5/lib/FastLED/docs/Doxyfile new file mode 100644 index 0000000..05424f3 --- /dev/null +++ b/v1.5/lib/FastLED/docs/Doxyfile @@ -0,0 +1,2976 @@ +# Doxyfile 1.13.2 + +# This file describes the settings to be used by the documentation system +# Doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use Doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use Doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = FastLED + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = 3.10.3 + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewers a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +# PROJECT_LOGO = include/fastled_logo.png + +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where Doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = ../docs + +# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this +# option can be useful when feeding Doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise cause +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. +# The default value is: NO. + +CREATE_SUBDIRS = YES + +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# number of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + +# If the ALLOW_UNICODE_NAMES tag is set to YES, Doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by Doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, Doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, Doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = YES + +# If the INLINE_INHERITED_MEMB tag is set to YES, Doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, Doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which Doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where Doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = /home/runner/work/FastLED/FastLED/src/ + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, Doxygen will generate much shorter (but +# less readable) file names. This can be useful if your file system doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen will interpret the +# first line (until the first dot, question mark or exclamation mark) of a +# Javadoc-style comment as the brief description. If set to NO, the Javadoc- +# style will behave just like regular Qt-style comments (thus requiring an +# explicit @brief command for a brief description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = YES + +# If the JAVADOC_BANNER tag is set to YES then Doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by Doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will interpret the first +# line (until the first dot, question mark or exclamation mark) of a Qt-style +# comment as the brief description. If set to NO, the Qt-style will behave just +# like regular Qt-style comments (thus requiring an explicit \brief command for +# a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and Doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# Doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as Doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then Doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = YES + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:^^" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) + +ALIASES = "htmlcolorblock{1}=<div style='background:#\1;width:4em;height:4em;'></div>" + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by Doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make Doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by Doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = ino=c + +# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by Doxygen, so you can +# mix Doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 6. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 10 + +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + +# When enabled Doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. Words listed in the +# AUTOLINK_IGNORE_WORDS tag are excluded from automatic linking. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# This tag specifies a list of words that, when matching the start of a word in +# the documentation, will suppress auto links generation, if it is enabled via +# AUTOLINK_SUPPORT. This list does not affect affect links explicitly created +# using \# or the \link or commands. +# This tag requires that the tag AUTOLINK_SUPPORT is set to YES. + +AUTOLINK_IGNORE_WORDS = + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let Doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also makes the inheritance and +# collaboration diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software) sources only. Doxygen will parse +# them like normal C++ but will assume all classes use public instead of private +# inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# Doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then Doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = YES + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = YES + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, Doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# Doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run Doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 5 + +# The NUM_PROC_THREADS specifies the number of threads Doxygen is allowed to use +# during processing. When set to 0 Doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 6 + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = YES + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, Doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = YES + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = YES + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = YES + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_UNDOC_NAMESPACES tag is set to YES, Doxygen will hide all +# undocumented namespaces that are normally visible in the namespace hierarchy. +# If set to NO, these namespaces will be included in the various overviews. This +# option has no effect if EXTRACT_ALL is enabled. +# The default value is: YES. + +HIDE_UNDOC_NAMESPACES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = YES + +# With the correct setting of option CASE_SENSE_NAMES Doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and macOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then Doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then Doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then Doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then Doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then Doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = YES + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then Doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = YES + +# If the SORT_GROUP_NAMES tag is set to YES then Doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = YES + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = YES + +# If the STRICT_PROTO_MATCHING option is enabled and Doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING Doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = YES + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# Doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by Doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by Doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents Doxygen's defaults, run Doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. +# +# Note that if you run Doxygen from a directory containing a file called +# DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +# The EXTERNAL_TOOL_PATH tag can be used to extend the search path (PATH +# environment variable) so that external tools such as latex and gs can be +# found. +# Note: Directories specified with EXTERNAL_TOOL_PATH are added in front of the +# path already specified by the PATH variable, and are added in the order +# specified. +# Note: This option is particularly useful for macOS version 14 (Sonoma) and +# higher, when running Doxygen from Doxywizard, because in this case any user- +# defined changes to the PATH are ignored. A typical example on macOS is to set +# EXTERNAL_TOOL_PATH = /Library/TeX/texbin /usr/local/bin +# together with the standard path, the full search path used by doxygen when +# launching external tools will then become +# PATH=/Library/TeX/texbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + +EXTERNAL_TOOL_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by Doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by Doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then Doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, Doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# If WARN_IF_INCOMPLETE_DOC is set to YES, Doxygen will warn about incomplete +# function parameter documentation. If set to NO, Doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, Doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, Doxygen will warn about +# undocumented enumeration values. If set to NO, Doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + +# If WARN_LAYOUT_FILE option is set to YES, Doxygen will warn about issues found +# while parsing the user defined layout file, such as missing or wrong elements. +# See also LAYOUT_FILE for details. If set to NO, problems with the layout file +# will be suppressed. +# The default value is: YES. + +WARN_LAYOUT_FILE = YES + +# If the WARN_AS_ERROR tag is set to YES then Doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then Doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the Doxygen process Doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then Doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined Doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that Doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of Doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = ../src \ + ../examples \ + ../README.md \ + ../PORTING.md + +# This tag can be used to specify the character encoding of the source files +# that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# This tag can be used to specify the character encoding of the source files +# that Doxygen parses. The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). +# See also: INPUT_ENCODING for further information on supported encodings. + +INPUT_FILE_ENCODING = + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by Doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, +# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to +# be provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice \ + *.ino + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which Doxygen is +# run. + +EXCLUDE = ../src/platforms + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# ANamespace::AClass, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = ../examples + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = *.ino \ + *.cpp \ + *.h \ + *.hpp + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = YES + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that Doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that Doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by Doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by Doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the Doxygen output. + +USE_MDFILE_AS_MAINPAGE = ../README.md + +# If the IMPLICIT_DIR_DOCS tag is set to YES, any README.md file found in sub- +# directories of the project's root, is used as the documentation for that sub- +# directory, except when the README.md starts with a \dir, \page or \mainpage +# command. If set to NO, the README.md file needs to start with an explicit \dir +# command in order to be used as directory documentation. +# The default value is: YES. + +IMPLICIT_DIR_DOCS = YES + +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# multi-line macros, enums or list initialized variables directly into the +# documentation. +# The default value is: NO. + +INLINE_SOURCES = YES + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct Doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of Doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by Doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then Doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then Doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which Doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then Doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by Doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, Doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank Doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that Doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that Doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of Doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = include/header.html + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank Doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that Doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = include/footer.html + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank Doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that Doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by Doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = style.css + +#HTML_EXTRA_STYLESHEET = include/fastled-docs.css \ +# doxygen-awesome-css/doxygen-awesome.css \ +# doxygen-awesome-css/doxygen-awesome-sidebar-only.css \ +# doxygen-awesome-css/doxygen-awesome-sidebar-only-darkmode-toggle.css + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +# HTML_EXTRA_FILES = doxygen-awesome-css/doxygen-awesome-darkmode-toggle.js + +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generates light mode output, DARK always +# generates dark mode output, AUTO_LIGHT automatically sets the mode according +# to the user preference, uses light mode if no preference is set (the default), +# AUTO_DARK automatically sets the mode according to the user preference, uses +# dark mode if no preference is set and TOGGLE allows a user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = AUTO_LIGHT + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use gray-scales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = YES + +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then Doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# Doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, Doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then Doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline (the HTML help workshop was already many +# years in maintenance mode). You can download the HTML help workshop from the +# web archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by Doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# Doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty Doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by Doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = YES + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# Doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# When the SHOW_ENUM_VALUES tag is set doxygen will show the specified +# enumeration values besides the enumeration mnemonics. +# The default value is: NO. + +SHOW_ENUM_VALUES = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, Doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the OBFUSCATE_EMAILS tag is set to YES, Doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, Doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# Doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for MathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2 + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with JavaScript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled Doxygen will generate a search box for +# the HTML output. The underlying search engine uses JavaScript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the JavaScript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /