diff --git a/.gitignore b/.gitignore
index c7642effc..2f102171c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,5 +26,6 @@ sdkconfig.board.*.dev
.caveman.json
.ai/mcp
+.clinerules/
__pycache__
\ No newline at end of file
diff --git a/Devices/lilygo-tembed-cc1101-plus/CMakeLists.txt b/Devices/lilygo-tembed-cc1101-plus/CMakeLists.txt
new file mode 100644
index 000000000..b2d9c4492
--- /dev/null
+++ b/Devices/lilygo-tembed-cc1101-plus/CMakeLists.txt
@@ -0,0 +1,6 @@
+file(GLOB_RECURSE SOURCE_FILES source/*.c*)
+
+idf_component_register(
+ SRCS ${SOURCE_FILES}
+ REQUIRES TactilityKernel Tactility
+)
diff --git a/Devices/lilygo-tembed-cc1101-plus/LICENSE-Apache-2.0.md b/Devices/lilygo-tembed-cc1101-plus/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Devices/lilygo-tembed-cc1101-plus/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Devices/lilygo-tembed-cc1101-plus/device.properties b/Devices/lilygo-tembed-cc1101-plus/device.properties
new file mode 100644
index 000000000..0c46c00e6
--- /dev/null
+++ b/Devices/lilygo-tembed-cc1101-plus/device.properties
@@ -0,0 +1,28 @@
+general.vendor=LilyGO
+general.name=T-Embed CC1101 Plus
+
+apps.launcherAppId=tactility.launcher
+
+hardware.target=ESP32S3
+hardware.flashSize=16MB
+hardware.spiRam=true
+hardware.spiRamMode=OCT
+hardware.spiRamSpeed=80M
+hardware.spiRamXipDisabled=true
+hardware.tinyUsbMsc=true
+hardware.esptoolFlashFreq=80M
+hardware.bluetooth=true
+
+storage.userDataLocation=SD
+
+display.size=1.9"
+display.shape=rectangle
+display.dpi=191
+
+lvgl.colorDepth=16
+lvgl.fontSize=16
+lvgl.uiDensity=compact
+
+sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y
+
+
diff --git a/Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts b/Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts
new file mode 100644
index 000000000..30cc27b51
--- /dev/null
+++ b/Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts
@@ -0,0 +1,186 @@
+/dts-v1/;
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Reference:
+// https://wiki.lilygo.cc/products/t-embed-series/t-embed-cc1101/
+// https://github.com/Xinyuan-LilyGO/T-Embed-CC1101
+// https://lilygo.cc/products/t-embed-cc1101-plus
+/ {
+ compatible = "root";
+ model = "T-Embed CC1101 Plus";
+
+ adc0 {
+ compatible = "espressif,esp32-adc-oneshot";
+ unit-id = ;
+ clk-src = ;
+ channels = ;
+ };
+
+ battery-sense {
+ compatible = "battery-sense";
+ io-channel = <&adc0 0>;
+ reference-voltage-mv = <3300>;
+ multiplier = <2110>;
+ };
+
+ wifi0 {
+ compatible = "espressif,esp32-wifi-pinned";
+ };
+
+ ble0 {
+ compatible = "espressif,esp32-ble";
+ };
+
+ gpio0 {
+ compatible = "espressif,esp32-gpio";
+ gpio-count = <49>;
+ };
+
+ // Required to enable peripherals like BQ25896
+ peripheral-power-hog {
+ compatible = "gpio-hog";
+ pin = <&gpio0 15 GPIO_FLAG_NONE>;
+ mode = ;
+ };
+
+ // Speaker I2S (MAX98357A amplifier)
+ i2s0 {
+ compatible = "espressif,esp32-i2s";
+ port = ;
+ pin-bclk = <&gpio0 46 GPIO_FLAG_NONE>;
+ pin-ws = <&gpio0 40 GPIO_FLAG_NONE>;
+ pin-data-out = <&gpio0 7 GPIO_FLAG_NONE>;
+ pin-data-in = <&gpio0 42 GPIO_FLAG_NONE>;
+ pin-mclk = <&gpio0 39 GPIO_FLAG_NONE>;
+ };
+
+ // MAX98357A class-D speaker amplifier. No I2C.
+ speaker0 {
+ compatible = "maxim,max98357a";
+ i2s = <&i2s0>;
+ };
+
+ mic0 {
+ compatible = "generic,spm1423";
+ i2s = <&i2s0>;
+ channels = <1>;
+ };
+
+ // WS2812 RGB LED Ring
+ // TODO - Sync on/off with DisplayIdle?
+ ledstrip0 {
+ compatible = "espressif,esp32-led-strip";
+ pin-data = <&gpio0 14 GPIO_FLAG_NONE>;
+ led-count = <8>;
+ };
+
+ i2c_internal: i2c0 {
+ compatible = "espressif,esp32-i2c-master";
+ port = ;
+ clock-frequency = <100000>;
+ pin-sda = <&gpio0 8 GPIO_FLAG_NONE>;
+ pin-scl = <&gpio0 18 GPIO_FLAG_NONE>;
+
+ bq27220 {
+ compatible = "ti,bq27220";
+ reg = <0x55>;
+ };
+
+ bq25896 {
+ compatible = "ti,bq25896";
+ reg = <0x6B>;
+ };
+
+ // TODO - Add PN532 NFC - IRQ GPIO 17, RES GPIO 45, address 0x24 - driver needed
+ };
+
+ display_backlight_pwm {
+ compatible = "espressif,esp32-pwm-ledc";
+ pin = <&gpio0 21 GPIO_FLAG_NONE>;
+ period-ns = <33333>;
+ ledc-timer = <0>;
+ ledc-channel = <0>;
+ };
+
+ display_backlight {
+ compatible = "pwm-backlight";
+ // Off by default so dispaly power-on won't show the screen from before the last power loss.
+ // The display backlight is turned on during the boot process.
+ status = "disabled";
+ pwm = <&display_backlight_pwm>;
+ };
+
+ spi0 {
+ compatible = "espressif,esp32-spi";
+ host = ;
+ cs-gpios = <&gpio0 41 GPIO_FLAG_NONE>, // Display
+ <&gpio0 13 GPIO_FLAG_NONE>, // SD card
+ <&gpio0 12 GPIO_FLAG_NONE>, // Radio - CC1101
+ <&gpio0 44 GPIO_FLAG_NONE>; // Radio - nRF24L01
+ pin-mosi = <&gpio0 9 GPIO_FLAG_NONE>;
+ pin-miso = <&gpio0 10 GPIO_FLAG_NONE>;
+ pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>;
+
+ display@0 {
+ compatible = "sitronix,st7789";
+ horizontal-resolution = <320>;
+ vertical-resolution = <170>;
+ gap-x = <35>;
+ swap-xy;
+ mirror-y;
+ invert-color;
+ pixel-clock-hz = <80000000>;
+ pin-dc = <&gpio0 16 GPIO_FLAG_NONE>;
+ pin-reset = <&gpio0 40 GPIO_FLAG_NONE>;
+ backlight = <&display_backlight>;
+ };
+
+ sdcard@1 {
+ compatible = "espressif,esp32-sdspi";
+ status = "disabled";
+ frequency-khz = <20000>;
+ };
+
+ // TODO - Add CC1101 Radio - CS GPIO 12, SCK GPIO 11, MOSI GPIO 9, MISO GPIO 10, IO2 GPIO 38, IO0 GPIO 3, SW1 GPIO 47, SW0 GPIO 48 - driver needed
+ // TODO - Add nRF24L01 Radio - CS GPIO 44, CE GPIO 43, IQR -1/NC, SCK GPIO 11, MOSI GPIO 9, MISO GPIO 10 - driver needed
+ };
+
+ encoder {
+ compatible = "tactility,gpio-encoder";
+ pin-a = <&gpio0 5 GPIO_FLAG_NONE>;
+ pin-b = <&gpio0 4 GPIO_FLAG_NONE>;
+ pin-enter = <&gpio0 0 GPIO_FLAG_NONE>;
+ pulses-per-detent = <2>;
+ pending-capacity = <8>;
+ };
+
+ usbdevice0 {
+ compatible = "espressif,esp32-usbdevice";
+
+ usbdevicemsc0 {
+ compatible = "espressif,esp32-usbdevice-msc";
+ };
+ };
+};
diff --git a/Devices/lilygo-tembed-cc1101-plus/module.yaml b/Devices/lilygo-tembed-cc1101-plus/module.yaml
new file mode 100644
index 000000000..00e9ad69b
--- /dev/null
+++ b/Devices/lilygo-tembed-cc1101-plus/module.yaml
@@ -0,0 +1,11 @@
+dependencies:
+ - Platforms/platform-esp32
+ - Drivers/bq27220-module
+ - Drivers/bq25896-module
+ - Drivers/gpio-encoder-module
+ - Drivers/st7789-module
+ - Drivers/dummy-i2s-amp-module
+ - Drivers/pdm-mic-module
+ - Drivers/audio-stream-module
+ - Drivers/esp32-led-strip-module
+dts: lilygo,tembed-cc1101-plus.dts
diff --git a/Devices/lilygo-tembed-cc1101-plus/source/module.cpp b/Devices/lilygo-tembed-cc1101-plus/source/module.cpp
new file mode 100644
index 000000000..97e9b00c1
--- /dev/null
+++ b/Devices/lilygo-tembed-cc1101-plus/source/module.cpp
@@ -0,0 +1,49 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+constexpr auto* TAG = "tembed-cc1101-plus";
+
+extern "C" {
+
+static void on_boot_completed(SystemEvent* event, void* context) {
+ (void)event;
+ (void)context;
+ Device* device = nullptr;
+ if (device_get_first_by_type(&LED_STRIP_TYPE, &device) != ERROR_NONE) {
+ LOG_W(TAG, "No LED strip found during boot restore");
+ return;
+ }
+
+ const auto settings = tt::settings::ledstrip::loadOrGetDefault(device->name);
+ const error_t result = tt::settings::ledstrip::apply(device, settings);
+ if (result != ERROR_NONE) {
+ LOG_E(TAG, "Failed to restore LED strip %s: %d", device->name, result);
+ } else {
+ LOG_I(TAG, "Restored LED strip %s", device->name);
+ }
+ device_put(device);
+}
+
+static error_t start() {
+ return system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr);
+}
+
+static error_t stop() {
+ return system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed);
+}
+
+Module lilygo_tembed_cc1101_plus_module = {
+ .name = "lilygo-tembed-cc1101-plus",
+ .start = start,
+ .stop = stop,
+ .drivers = nullptr,
+ .symbols = nullptr,
+ .internal = nullptr
+};
+
+}
diff --git a/Drivers/esp32-led-strip-module/CMakeLists.txt b/Drivers/esp32-led-strip-module/CMakeLists.txt
new file mode 100644
index 000000000..8e6a6d6eb
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.20)
+
+include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
+
+file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
+
+tactility_add_module(esp32-led-strip-module
+ SRCS ${SOURCE_FILES}
+ INCLUDE_DIRS include/
+ REQUIRES TactilityKernel platform-esp32 esp_driver_rmt driver
+)
\ No newline at end of file
diff --git a/Drivers/esp32-led-strip-module/LICENSE-Apache-2.0.md b/Drivers/esp32-led-strip-module/LICENSE-Apache-2.0.md
new file mode 100644
index 000000000..f5f4b8b5e
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/LICENSE-Apache-2.0.md
@@ -0,0 +1,195 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_<>_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/Drivers/esp32-led-strip-module/README.md b/Drivers/esp32-led-strip-module/README.md
new file mode 100644
index 000000000..c5479dbef
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/README.md
@@ -0,0 +1,88 @@
+# esp32-led-strip-module
+
+WS2812-compatible RGB LED strip driver for Tactility. It uses ESP-IDF's RMT peripheral to transmit pixel data and provides per-pixel and range color control, brightness adjustment, and gamma-corrected output.
+
+## Devicetree Configuration
+
+Declare the strip with the compatible string `"espressif,esp32-led-strip"`. `pin-data` references the GPIO device and pin used for the data signal; `led-count` is the number of LEDs on the strip.
+
+```dts
+ws2812 {
+ compatible = "espressif,esp32-led-strip";
+ pin-data = <&gpio0 14 GPIO_FLAG_NONE>;
+ led-count = <8>;
+};
+```
+
+The driver reads these properties into `LedStripConfig`.
+
+## API
+
+Include ``. The kernel API operates on a `struct Device *` and returns `error_t`; successful calls return `ERROR_NONE`.
+
+| Function | Description |
+|---|---|
+| `led_strip_set_single_led_color` | Set one LED using global brightness |
+| `led_strip_set_single_led_color_and_brightness` | Set one LED with an explicit brightness |
+| `led_strip_set_led_range_color` | Set a range from an array of `LedRgb` values using global brightness |
+| `led_strip_set_led_range_color_and_brightness` | Set a range from an array with explicit brightness |
+| `led_strip_fill_led_range_color` | Fill a range using global brightness |
+| `led_strip_fill_led_range_color_and_brightness` | Fill a range with explicit brightness |
+| `led_strip_set_brightness` | Set global brightness |
+| `led_strip_show` | Transmit buffered pixel values to the strip |
+| `led_strip_clear` | Turn off all LEDs |
+| `led_strip_get_length` | Get the configured LED count |
+
+LED indices and range offsets are zero-based. Color channels in `struct LedRgb` range from 0 to 255. Global and per-operation brightness also range from 0 (off) to 255 (full); the default global brightness is 255. Call `led_strip_show()` after setting colors to transmit the updated buffer. `led_strip_clear()` clears and refreshes the strip.
+
+## Usage Example
+
+```c
+#include
+
+void led_demo(struct Device *strip) {
+ uint16_t length = 0;
+ if (led_strip_get_length(strip, &length) != ERROR_NONE || length == 0) {
+ return;
+ }
+
+ led_strip_set_brightness(strip, 128); // About 50% brightness
+ led_strip_fill_led_range_color(strip, 0, length, LED_RGB_COLOR_GREEN);
+ led_strip_show(strip);
+
+ led_strip_set_single_led_color(strip, 0, LED_RGB_COLOR_RED);
+ led_strip_show(strip);
+
+ led_strip_clear(strip);
+}
+```
+
+## Using Literal RGB Values
+
+Color macros are optional. Initialize `struct LedRgb` directly when a color is calculated or loaded at runtime. This example assumes the strip has at least four LEDs.
+
+```c
+#include
+
+void show_custom_colors(struct Device *strip) {
+ const struct LedRgb colors[] = {
+ { .r = 255, .g = 32, .b = 0 },
+ { .r = 0, .g = 180, .b = 64 },
+ { .r = 24, .g = 48, .b = 255 },
+ { .r = 200, .g = 80, .b = 160 }
+ };
+
+ led_strip_set_led_range_color_and_brightness(strip, 0, 4, colors, 192);
+ led_strip_show(strip);
+}
+```
+
+## Color Presets
+
+The kernel header also provides `LED_RGB_COLOR_*` macros such as `LED_RGB_COLOR_RED`, `LED_RGB_COLOR_GREEN`, and `LED_RGB_COLOR_WHITE`, plus `LED_COLOR_RGB_CUSTOM(r, g, b)` for a custom color. These expand to `struct LedRgb` values.
+
+## Implementation Notes
+
+- The driver configures the ESP-IDF RMT backend for WS2812 RGB output in GRB component order.
+- An 8-bit gamma correction lookup table is applied after brightness scaling.
+- The devicetree compatible string and driver registration are `"espressif,esp32-led-strip"`; the kernel device type is `LED_STRIP_TYPE`.
diff --git a/Drivers/esp32-led-strip-module/bindings/espressif,esp32-led-strip.yaml b/Drivers/esp32-led-strip-module/bindings/espressif,esp32-led-strip.yaml
new file mode 100644
index 000000000..79b98f912
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/bindings/espressif,esp32-led-strip.yaml
@@ -0,0 +1,13 @@
+description: RGB LED strip. ESP-IDF's RMT peripheral to transmit pixel data to drive WS2812-compatible LEDs.
+
+compatible: "espressif,esp32-led-strip"
+
+properties:
+ pin-data:
+ type: phandles
+ required: true
+ description: GPIO pin for strip data output
+ led-count:
+ type: int
+ required: true
+ description: Number of LEDs in the strip
diff --git a/Drivers/esp32-led-strip-module/include/bindings/esp32_led_strip.h b/Drivers/esp32-led-strip-module/include/bindings/esp32_led_strip.h
new file mode 100644
index 000000000..b3be8cbc5
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/include/bindings/esp32_led_strip.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DEFINE_DEVICETREE(esp32_led_strip, struct LedStripConfig)
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp32-led-strip-module/include/drivers/esp32_led_strip.h b/Drivers/esp32-led-strip-module/include/drivers/esp32_led_strip.h
new file mode 100644
index 000000000..e4d0d3a81
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/include/drivers/esp32_led_strip.h
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+
+struct Device;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct LedStripConfig {
+ /** Data Pin */
+ struct GpioPinSpec pin_data;
+
+ /** LED Count */
+ uint16_t led_count;
+};
+
+/**
+ * @brief Sets the color of a single LED on the strip with global brightness.
+ * @param[in] device the LED strip device
+ * @param[in] index index of the LED in the strip (0-based)
+ * @param[in] color the color to set
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_set_single_led_color(struct Device* device, size_t index, const struct LedRgb color);
+
+/**
+ * @brief Sets the color of a single LED on the strip with an explicit brightness value.
+ * @param[in] device the LED strip device
+ * @param[in] index index of the LED in the strip (0-based)
+ * @param[in] color the color to set
+ * @param[in] brightness 0-255
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_set_single_led_color_and_brightness(struct Device* device, size_t index, const struct LedRgb color, uint8_t brightness);
+
+/**
+ * @brief Sets colors for a range of LEDs on the strip using the global brightness.
+ * @param[in] device the LED strip device
+ * @param[in] first starting LED index (0-based)
+ * @param[in] count number of LEDs to set
+ * @param[in] color array of colors to set
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_set_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb* color);
+
+/**
+ * @brief Sets colors for a range of LEDs on the strip with an explicit brightness value.
+ * @param[in] device the LED strip device
+ * @param[in] first starting LED index (0-based)
+ * @param[in] count number of LEDs to set
+ * @param[in] color array of colors to set
+ * @param[in] brightness 0-255
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_set_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb* color, uint8_t brightness);
+
+/**
+ * @brief Fills a range of LEDs with a single color using the global brightness.
+ * @param[in] device the LED strip device
+ * @param[in] first starting LED index (0-based)
+ * @param[in] count number of LEDs to fill
+ * @param[in] color the color to fill with
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_fill_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb color);
+
+/**
+ * @brief Fills a range of LEDs with a single color and an explicit brightness value.
+ * @param[in] device the LED strip device
+ * @param[in] first starting LED index (0-based)
+ * @param[in] count number of LEDs to fill
+ * @param[in] color the color to fill with
+ * @param[in] brightness 0-255
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_fill_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb color, uint8_t brightness);
+
+/**
+ * @brief Sets global brightness for the LED strip.
+ * @param[in] device the LED strip device
+ * @param[in] brightness 0-255
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_set_brightness(struct Device* device, uint8_t brightness);
+
+/**
+ * @brief Refreshes the strip to update the displayed colors.
+ * @param[in] device the LED strip device
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_show(struct Device* device);
+
+/**
+ * @brief Clears the strip to black.
+ * @param[in] device the LED strip device
+ * @retval ERROR_NONE when the operation was successful
+ */
+error_t esp32_led_strip_clear(struct Device* device);
+
+/**
+ * @brief Gets the length of a strip.
+ * @param[in] device the LED strip device
+ * @param[out] out_length receives the strip length
+ * @retval ERROR_NONE when the operation was successful
+ * @retval ERROR_INVALID_ARGUMENT if out of range
+ */
+error_t esp32_led_strip_get_length(struct Device* device, uint16_t* out_length);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp32-led-strip-module/include/esp32_led_strip_module.h b/Drivers/esp32-led-strip-module/include/esp32_led_strip_module.h
new file mode 100644
index 000000000..ad33cc90d
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/include/esp32_led_strip_module.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Module esp32_led_strip_module;
+
+#ifdef __cplusplus
+}
+#endif
\ No newline at end of file
diff --git a/Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt.h b/Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt.h
new file mode 100644
index 000000000..e0a600ec1
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt.h
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include
+#include "esp_err.h"
+#include "esp32_led_strip_types.h"
+#include "esp_idf_version.h"
+#include "driver/rmt_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief LED Strip RMT specific configuration
+ */
+typedef struct {
+ rmt_clock_source_t clk_src; /*!< RMT clock source */
+ uint32_t resolution_hz; /*!< RMT tick resolution, if set to zero, a default resolution (10MHz) will be applied */
+ size_t mem_block_symbols; /*!< How many RMT symbols can one RMT channel hold at one time. Set to 0 will fallback to use the default size. */
+ /*!< Extra RMT specific driver flags */
+ struct led_strip_rmt_extra_config {
+ uint32_t with_dma: 1; /*!< Use DMA to transmit data */
+ } flags; /*!< Extra driver flags */
+} led_strip_rmt_config_t;
+
+/**
+ * @brief Create LED strip based on RMT TX channel
+ *
+ * @param led_config LED strip configuration
+ * @param rmt_config RMT specific configuration
+ * @param ret_strip Returned LED strip handle
+ * @return
+ * - ESP_OK: create LED strip handle successfully
+ * - ESP_ERR_INVALID_ARG: create LED strip handle failed because of invalid argument
+ * - ESP_ERR_NO_MEM: create LED strip handle failed because of out of memory
+ * - ESP_FAIL: create LED strip handle failed because some other error
+ */
+esp_err_t led_strip_new_rmt_device(const led_strip_config_t *led_config, const led_strip_rmt_config_t *rmt_config, led_strip_handle_t *ret_strip);
+
+/**
+ * @brief Set RGB for a specific pixel
+ *
+ * @param strip: LED strip
+ * @param index: index of pixel to set
+ * @param red: red part of color
+ * @param green: green part of color
+ * @param blue: blue part of color
+ *
+ * @return
+ * - ESP_OK: Set RGB for a specific pixel successfully
+ * - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters
+ * - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred
+ */
+esp_err_t led_strip_rmt_set_pixel(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue);
+
+/**
+ * @brief Refresh memory colors to LEDs
+ *
+ * @param strip: LED strip
+ *
+ * @return
+ * - ESP_OK: Refresh successfully
+ * - ESP_FAIL: Refresh failed because some other error occurred
+ *
+ * @note:
+ * After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
+ */
+esp_err_t led_strip_rmt_refresh(led_strip_handle_t strip);
+
+/**
+ * @brief Clear LED strip (turn off all LEDs)
+ *
+ * @param strip: LED strip
+ *
+ * @return
+ * - ESP_OK: Clear LEDs successfully
+ * - ESP_FAIL: Clear LEDs failed because some other error occurred
+ */
+esp_err_t led_strip_rmt_clear(led_strip_handle_t strip);
+
+/**
+ * @brief Free LED strip resources
+ *
+ * @param strip: LED strip
+ *
+ * @return
+ * - ESP_OK: Free resources successfully
+ * - ESP_FAIL: Free resources failed because error occurred
+ */
+esp_err_t led_strip_rmt_del(led_strip_handle_t strip);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt_encoder.h b/Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt_encoder.h
new file mode 100644
index 000000000..9e62615f0
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt_encoder.h
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include
+#include "driver/rmt_encoder.h"
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Type of led strip encoder configuration
+ */
+typedef struct {
+ uint32_t resolution; /*!< Encoder resolution, in Hz */
+ led_model_t led_model; /*!< LED model */
+} led_strip_encoder_config_t;
+
+/**
+ * @brief Create RMT encoder for encoding LED strip pixels into RMT symbols
+ *
+ * @param[in] config Encoder configuration
+ * @param[out] ret_encoder Returned encoder handle
+ * @return
+ * - ESP_ERR_INVALID_ARG for any invalid arguments
+ * - ESP_ERR_NO_MEM out of memory when creating led strip encoder
+ * - ESP_OK if creating encoder successfully
+ */
+esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp32-led-strip-module/include/esp32_led_strip_types.h b/Drivers/esp32-led-strip-module/include/esp32_led_strip_types.h
new file mode 100644
index 000000000..1c61f40a8
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/include/esp32_led_strip_types.h
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Type of LED strip handle
+ */
+typedef struct led_strip_t led_strip_t;
+typedef led_strip_t *led_strip_handle_t;
+
+/**
+ * @brief LED strip model
+ * @note Different led model may have different timing parameters, so we need to distinguish them.
+ */
+typedef enum {
+ LED_MODEL_WS2812, /*!< LED strip model: WS2812 */
+ LED_MODEL_SK6812, /*!< LED strip model: SK6812 */
+ LED_MODEL_WS2811, /*!< LED strip model: WS2811 */
+ LED_MODEL_WS2816, /*!< LED strip model: WS2816 */
+ LED_MODEL_INVALID /*!< Invalid LED strip model */
+} led_model_t;
+
+/**
+ * @brief LED color component format
+ * @note The format is used to specify the order of color components in each pixel, also the number of color components.
+ */
+typedef union {
+ struct format_layout {
+ uint32_t r_pos: 2; /*!< Position of the red channel in the color order: 0~3 */
+ uint32_t g_pos: 2; /*!< Position of the green channel in the color order: 0~3 */
+ uint32_t b_pos: 2; /*!< Position of the blue channel in the color order: 0~3 */
+ uint32_t w_pos: 2; /*!< Position of the white channel in the color order: 0~3 */
+ uint32_t reserved: 19; /*!< Reserved */
+ uint32_t bytes_per_color: 2; /*!< Bytes per color component: 1 or 2. If set to 0, it will fallback to 1 */
+ uint32_t num_components: 3; /*!< Number of color components per pixel: 3 or 4. If set to 0, it will fallback to 3 */
+ } format; /*!< Format layout */
+ uint32_t format_id; /*!< Format ID */
+} led_color_component_format_t;
+
+/// Helper macros to set the color component format
+#define LED_STRIP_COLOR_COMPONENT_FMT_GRB (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 3}}
+#define LED_STRIP_COLOR_COMPONENT_FMT_GRB_16 (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 3}}
+#define LED_STRIP_COLOR_COMPONENT_FMT_GRBW (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 4}}
+#define LED_STRIP_COLOR_COMPONENT_FMT_GRBW_16 (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 4}}
+#define LED_STRIP_COLOR_COMPONENT_FMT_RGB (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 3}}
+#define LED_STRIP_COLOR_COMPONENT_FMT_RGB_16 (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 3}}
+#define LED_STRIP_COLOR_COMPONENT_FMT_RGBW (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 4}}
+#define LED_STRIP_COLOR_COMPONENT_FMT_RGBW_16 (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 4}}
+
+/**
+ * @brief LED Strip common configurations
+ * The common configurations are not specific to any backend peripheral.
+ */
+typedef struct {
+ int strip_gpio_num; /*!< GPIO number that used by LED strip */
+ uint32_t max_leds; /*!< Maximum number of LEDs that can be controlled in a single strip */
+ led_model_t led_model; /*!< Specifies the LED strip model (e.g., WS2812, SK6812) */
+ led_color_component_format_t color_component_format; /*!< Specifies the order of color components in each pixel.
+ Use helper macros like `LED_STRIP_COLOR_COMPONENT_FMT_GRB` to set the format */
+ /*!< LED strip extra driver flags */
+ struct led_strip_extra_flags {
+ uint32_t invert_out: 1; /*!< Invert output signal */
+ } flags; /*!< Extra driver flags */
+} led_strip_config_t;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/Drivers/esp32-led-strip-module/module.yaml b/Drivers/esp32-led-strip-module/module.yaml
new file mode 100644
index 000000000..a07d6f334
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/module.yaml
@@ -0,0 +1,3 @@
+dependencies:
+ - TactilityKernel
+bindings: bindings
diff --git a/Drivers/esp32-led-strip-module/source/esp32_led_strip.cpp b/Drivers/esp32-led-strip-module/source/esp32_led_strip.cpp
new file mode 100644
index 000000000..e7f82bbb2
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/source/esp32_led_strip.cpp
@@ -0,0 +1,258 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+constexpr auto* TAG = "Esp32LedStrip";
+
+#define GET_CONFIG(device) (static_cast((device)->config))
+#define GET_INTERNAL(device) (static_cast(device_get_driver_data(device)))
+
+struct LedStripInternal {
+ led_strip_handle_t strip;
+ GpioPinSpec data_pin;
+ uint16_t led_count;
+ uint8_t global_brightness;
+};
+
+// Gamma correction lookup table for 8-bit brightness/color scaling
+static const uint8_t gamma8[] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3,
+ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7,
+ 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11, 11, 12, 12, 13,
+ 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20,
+ 21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 27, 28, 29, 29, 30,
+ 31, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 42,
+ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,
+ 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75,
+ 76, 77, 78, 79, 81, 82, 83, 84, 86, 87, 88, 90, 91, 92, 94, 95,
+ 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 111, 113, 114, 116, 117, 119,
+ 121, 122, 124, 125, 127, 129, 130, 132, 134, 135, 137, 139, 141, 142, 144, 146,
+ 148, 150, 151, 153, 155, 157, 159, 161, 163, 165, 166, 168, 170, 172, 174, 176,
+ 178, 180, 182, 184, 186, 188, 191, 193, 195, 197, 199, 201, 204, 206, 208, 210,
+ 213, 215, 217, 220, 222, 224, 227, 229, 231, 234, 236, 239, 241, 244, 246, 249,
+ 251, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
+};
+
+static error_t create_strip(const LedStripConfig* source, led_strip_handle_t* out_strip) {
+ led_strip_config_t strip_config_internal = {
+ .strip_gpio_num = static_cast(source->pin_data.pin),
+ .max_leds = source->led_count,
+ .led_model = LED_MODEL_WS2812,
+ .color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB,
+ .flags = {
+ .invert_out = false,
+ }
+ };
+
+ led_strip_rmt_config_t rmt_config = {
+ .clk_src = RMT_CLK_SRC_DEFAULT,
+ .resolution_hz = 10 * 1000 * 1000,
+ .mem_block_symbols = 0,
+ .flags = {
+ .with_dma = 0,
+ }
+ };
+
+ led_strip_handle_t strip = nullptr;
+ esp_err_t err = led_strip_new_rmt_device(&strip_config_internal, &rmt_config, &strip);
+ if (err != ESP_OK) {
+ return ERROR_RESOURCE;
+ }
+
+ *out_strip = strip;
+ return ERROR_NONE;
+}
+
+static error_t start(Device* device) {
+ const auto* config = GET_CONFIG(device);
+
+ auto* internal = static_cast(std::calloc(1, sizeof(LedStripInternal)));
+ if (internal == nullptr) {
+ return ERROR_OUT_OF_MEMORY;
+ }
+
+ internal->data_pin = static_cast(config->pin_data);
+ internal->led_count = config->led_count;
+ internal->global_brightness = 255;
+
+ led_strip_handle_t strip = nullptr;
+ if (create_strip(config, &strip) != ERROR_NONE) {
+ std::free(internal);
+ return ERROR_RESOURCE;
+ }
+
+ internal->strip = strip;
+
+ device_set_driver_data(device, internal);
+
+ return ERROR_NONE;
+}
+
+static error_t stop(Device* device) {
+ auto* internal = GET_INTERNAL(device);
+
+ led_strip_rmt_del(internal->strip);
+
+ std::free(internal);
+ device_set_driver_data(device, nullptr);
+ return ERROR_NONE;
+}
+
+static error_t apply_single(Device* device, size_t index, const LedRgb& color, uint8_t brightness) {
+ auto* internal = GET_INTERNAL(device);
+
+ if (index >= internal->led_count) return ERROR_INVALID_ARGUMENT;
+
+ uint32_t sr = (color.r * brightness) / 255;
+ uint32_t sg = (color.g * brightness) / 255;
+ uint32_t sb = (color.b * brightness) / 255;
+
+ esp_err_t err = led_strip_rmt_set_pixel(internal->strip, index, gamma8[sr], gamma8[sg], gamma8[sb]);
+ if (err != ESP_OK) {
+ return ERROR_RESOURCE;
+ }
+ return ERROR_NONE;
+}
+
+static error_t apply_range(Device* device, size_t first, size_t count, const LedRgb* colors, uint8_t brightness) {
+ auto* internal = GET_INTERNAL(device);
+
+ if (first > internal->led_count) return ERROR_INVALID_ARGUMENT;
+ if (count > internal->led_count - first) return ERROR_INVALID_ARGUMENT;
+ if (colors == nullptr) return ERROR_INVALID_ARGUMENT;
+
+ for (size_t i = 0; i < count; i++) {
+ uint32_t sr = (colors[i].r * brightness) / 255;
+ uint32_t sg = (colors[i].g * brightness) / 255;
+ uint32_t sb = (colors[i].b * brightness) / 255;
+
+ esp_err_t err = led_strip_rmt_set_pixel(internal->strip, first + i, gamma8[sr], gamma8[sg], gamma8[sb]);
+ if (err != ESP_OK) {
+ return ERROR_RESOURCE;
+ }
+ }
+ return ERROR_NONE;
+}
+
+static error_t apply_fill(Device* device, size_t first, size_t count, const LedRgb& color, uint8_t brightness)
+{
+ auto* internal = GET_INTERNAL(device);
+ if (first > internal->led_count) return ERROR_INVALID_ARGUMENT;
+ if (count > internal->led_count - first) return ERROR_INVALID_ARGUMENT;
+
+ for (size_t i = 0; i < count; i++) {
+ uint32_t sr = (color.r * brightness) / 255;
+ uint32_t sg = (color.g * brightness) / 255;
+ uint32_t sb = (color.b * brightness) / 255;
+
+ esp_err_t err = led_strip_rmt_set_pixel(internal->strip, first + i, gamma8[sr], gamma8[sg], gamma8[sb]);
+ if (err != ESP_OK) {
+ return ERROR_RESOURCE;
+ }
+ }
+ return ERROR_NONE;
+}
+
+extern "C" {
+
+error_t esp32_led_strip_set_single_led_color(Device* device, size_t index, const LedRgb color) {
+ auto* internal = GET_INTERNAL(device);
+ return apply_single(device, index, color, internal->global_brightness);
+}
+
+error_t esp32_led_strip_set_single_led_color_and_brightness(Device* device, size_t index, const LedRgb color, uint8_t brightness) {
+ return apply_single(device, index, color, brightness);
+}
+
+error_t esp32_led_strip_set_led_range_color(Device* device, size_t first, size_t count, const LedRgb* color) {
+ auto* internal = GET_INTERNAL(device);
+ return apply_range(device, first, count, color, internal->global_brightness);
+}
+
+error_t esp32_led_strip_set_led_range_color_and_brightness(Device* device, size_t first, size_t count, const LedRgb* color, uint8_t brightness) {
+ return apply_range(device, first, count, color, brightness);
+}
+
+error_t esp32_led_strip_fill_led_range_color(Device* device, size_t first, size_t count, const LedRgb color) {
+ auto* internal = GET_INTERNAL(device);
+ return apply_fill(device, first, count, color, internal->global_brightness);
+}
+
+error_t esp32_led_strip_fill_led_range_color_and_brightness(Device* device, size_t first, size_t count, const LedRgb color, uint8_t brightness) {
+ return apply_fill(device, first, count, color, brightness);
+}
+
+error_t esp32_led_strip_set_brightness(Device* device, uint8_t brightness) {
+ auto* internal = GET_INTERNAL(device);
+ internal->global_brightness = brightness;
+ return ERROR_NONE;
+}
+
+error_t esp32_led_strip_show(Device* device) {
+ auto* internal = GET_INTERNAL(device);
+ if (internal->strip != nullptr) {
+ esp_err_t err = led_strip_rmt_refresh(internal->strip);
+ if (err != ESP_OK) {
+ return ERROR_RESOURCE;
+ }
+ }
+ return ERROR_NONE;
+}
+
+error_t esp32_led_strip_clear(Device* device) {
+ auto* internal = GET_INTERNAL(device);
+ if (internal->strip != nullptr) {
+ esp_err_t err = led_strip_rmt_clear(internal->strip);
+ if (err != ESP_OK) {
+ return ERROR_RESOURCE;
+ }
+ }
+ return ERROR_NONE;
+}
+
+error_t esp32_led_strip_get_length(Device* device, uint16_t* out_length) {
+ auto* internal = GET_INTERNAL(device);
+ *out_length = internal->led_count;
+ return ERROR_NONE;
+}
+
+static constexpr LedStripApi LED_STRIP_API = {
+ .set_single_led_color = esp32_led_strip_set_single_led_color,
+ .set_single_led_color_and_brightness = esp32_led_strip_set_single_led_color_and_brightness,
+ .set_led_range_color = esp32_led_strip_set_led_range_color,
+ .set_led_range_color_and_brightness = esp32_led_strip_set_led_range_color_and_brightness,
+ .fill_led_range_color = esp32_led_strip_fill_led_range_color,
+ .fill_led_range_color_and_brightness = esp32_led_strip_fill_led_range_color_and_brightness,
+ .set_brightness = esp32_led_strip_set_brightness,
+ .show = esp32_led_strip_show,
+ .clear = esp32_led_strip_clear,
+ .get_length = esp32_led_strip_get_length,
+};
+
+extern Module esp32_led_strip_module;
+
+Driver esp32_led_strip_driver = {
+ .name = "esp32_led_strip",
+ .compatible = (const char*[]) { "espressif,esp32-led-strip", nullptr },
+ .start_device = start,
+ .stop_device = stop,
+ .probe = nullptr,
+ .api = &LED_STRIP_API,
+ .device_type = &LED_STRIP_TYPE,
+ .owner = &esp32_led_strip_module,
+ .internal = nullptr,
+};
+
+} // extern "C"
diff --git a/Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_dev.c b/Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_dev.c
new file mode 100644
index 000000000..70decdad6
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_dev.c
@@ -0,0 +1,170 @@
+/*
+ * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#include
+#include
+#include
+#include "esp_log.h"
+#include "esp_check.h"
+#include "driver/rmt_tx.h"
+#include "drivers/esp32_led_strip.h"
+#include "esp32_led_strip_rmt_encoder.h"
+#include "esp32_led_strip_rmt.h"
+
+#define LED_STRIP_RMT_DEFAULT_RESOLUTION 10000000 // 10MHz resolution
+#define LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE 4
+// the memory size of each RMT channel, in words (4 bytes)
+#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
+#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 64
+#else
+#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 48
+#endif
+
+static const char *TAG = "led_strip_rmt";
+
+typedef struct led_strip_t {
+ 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[];
+} led_strip_rmt_obj;
+
+esp_err_t led_strip_rmt_set_pixel(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
+{
+ led_strip_rmt_obj *rmt_strip = strip;
+ ESP_RETURN_ON_FALSE(index < rmt_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
+
+ struct format_layout format = rmt_strip->component_fmt.format;
+ uint32_t start = index * rmt_strip->bytes_per_pixel;
+ uint8_t *pixel_buf = rmt_strip->pixel_buf;
+ uint8_t pos_bytes = format.bytes_per_color;
+
+ for (uint8_t i = 0; i < format.bytes_per_color; i++) {
+ uint8_t color_shift = 8 * (format.bytes_per_color - 1 - i);
+ pixel_buf[start + format.r_pos * pos_bytes + i] = (red >> color_shift) & 0xFF;
+ pixel_buf[start + format.g_pos * pos_bytes + i] = (green >> color_shift) & 0xFF;
+ pixel_buf[start + format.b_pos * pos_bytes + i] = (blue >> color_shift) & 0xFF;
+ if (format.num_components > 3) {
+ pixel_buf[start + format.w_pos * pos_bytes + i] = 0;
+ }
+ }
+ return ESP_OK;
+}
+
+esp_err_t led_strip_rmt_refresh(led_strip_handle_t strip)
+{
+ led_strip_rmt_obj *rmt_strip = strip;
+ rmt_transmit_config_t tx_conf = {
+ .loop_count = 0,
+ };
+
+ ESP_RETURN_ON_ERROR(rmt_enable(rmt_strip->rmt_chan), TAG, "enable RMT channel failed");
+ ESP_RETURN_ON_ERROR(rmt_transmit(rmt_strip->rmt_chan, rmt_strip->strip_encoder, rmt_strip->pixel_buf,
+ rmt_strip->strip_len * rmt_strip->bytes_per_pixel, &tx_conf), TAG, "transmit pixels by RMT failed");
+ ESP_RETURN_ON_ERROR(rmt_tx_wait_all_done(rmt_strip->rmt_chan, -1), TAG, "flush RMT channel failed");
+ ESP_RETURN_ON_ERROR(rmt_disable(rmt_strip->rmt_chan), TAG, "disable RMT channel failed");
+ return ESP_OK;
+}
+
+esp_err_t led_strip_rmt_clear(led_strip_handle_t strip)
+{
+ led_strip_rmt_obj *rmt_strip = strip;
+ // Write zero to turn off all leds
+ memset(rmt_strip->pixel_buf, 0, rmt_strip->strip_len * rmt_strip->bytes_per_pixel);
+ return led_strip_rmt_refresh(strip);
+}
+
+esp_err_t led_strip_rmt_del(led_strip_handle_t strip)
+{
+ led_strip_rmt_obj *rmt_strip = strip;
+ ESP_RETURN_ON_ERROR(rmt_del_channel(rmt_strip->rmt_chan), TAG, "delete RMT channel failed");
+ ESP_RETURN_ON_ERROR(rmt_del_encoder(rmt_strip->strip_encoder), TAG, "delete strip encoder failed");
+ free(rmt_strip);
+ return ESP_OK;
+}
+
+esp_err_t led_strip_new_rmt_device(const led_strip_config_t *led_config, const led_strip_rmt_config_t *rmt_config, led_strip_handle_t *ret_strip)
+{
+ led_strip_rmt_obj *rmt_strip = NULL;
+ esp_err_t ret = ESP_OK;
+ ESP_GOTO_ON_FALSE(led_config && rmt_config && ret_strip, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+ led_color_component_format_t component_fmt = led_config->color_component_format;
+ // If R/G/B order is not specified, set default GRB order as fallback
+ if (component_fmt.format_id == 0) {
+ component_fmt = LED_STRIP_COLOR_COMPONENT_FMT_GRB;
+ }
+ if (led_config->led_model == LED_MODEL_WS2816) {
+ component_fmt.format.bytes_per_color = 2;
+ }
+ if (component_fmt.format.bytes_per_color == 0) {
+ component_fmt.format.bytes_per_color = 1;
+ }
+ // check the validation of the color component format
+ uint8_t mask = 0;
+ if (component_fmt.format.num_components == 3) {
+ mask = BIT(component_fmt.format.r_pos) | BIT(component_fmt.format.g_pos) | BIT(component_fmt.format.b_pos);
+ // Check for invalid values
+ ESP_RETURN_ON_FALSE(mask == 0x07, ESP_ERR_INVALID_ARG, TAG, "invalid order argument");
+ } else if (component_fmt.format.num_components == 4) {
+ mask = BIT(component_fmt.format.r_pos) | BIT(component_fmt.format.g_pos) | BIT(component_fmt.format.b_pos) | BIT(component_fmt.format.w_pos);
+ // Check for invalid values
+ ESP_RETURN_ON_FALSE(mask == 0x0F, ESP_ERR_INVALID_ARG, TAG, "invalid order argument");
+ } else {
+ ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_ARG, TAG, "invalid number of color components: %d", component_fmt.format.num_components);
+ }
+ uint8_t bytes_per_pixel = component_fmt.format.num_components;
+ if (component_fmt.format.bytes_per_color > 1) {
+ bytes_per_pixel *= component_fmt.format.bytes_per_color;
+ }
+ rmt_strip = calloc(1, sizeof(led_strip_rmt_obj) + led_config->max_leds * bytes_per_pixel);
+ ESP_GOTO_ON_FALSE(rmt_strip, ESP_ERR_NO_MEM, err, TAG, "no mem for rmt strip");
+ uint32_t resolution = rmt_config->resolution_hz ? rmt_config->resolution_hz : LED_STRIP_RMT_DEFAULT_RESOLUTION;
+
+ // for backward compatibility, if the user does not set the clk_src, use the default value
+ rmt_clock_source_t clk_src = RMT_CLK_SRC_DEFAULT;
+ if (rmt_config->clk_src) {
+ clk_src = rmt_config->clk_src;
+ }
+ size_t mem_block_symbols = LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS;
+ // override the default value if the user sets it
+ if (rmt_config->mem_block_symbols) {
+ mem_block_symbols = rmt_config->mem_block_symbols;
+ }
+ rmt_tx_channel_config_t rmt_chan_config = {
+ .clk_src = clk_src,
+ .gpio_num = led_config->strip_gpio_num,
+ .mem_block_symbols = mem_block_symbols,
+ .resolution_hz = resolution,
+ .trans_queue_depth = LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE,
+ .flags.with_dma = rmt_config->flags.with_dma,
+ .flags.invert_out = led_config->flags.invert_out,
+ };
+ ESP_GOTO_ON_ERROR(rmt_new_tx_channel(&rmt_chan_config, &rmt_strip->rmt_chan), err, TAG, "create RMT TX channel failed");
+
+ led_strip_encoder_config_t strip_encoder_conf = {
+ .resolution = resolution,
+ .led_model = led_config->led_model
+ };
+ ESP_GOTO_ON_ERROR(rmt_new_led_strip_encoder(&strip_encoder_conf, &rmt_strip->strip_encoder), err, TAG, "create LED strip encoder failed");
+
+ rmt_strip->component_fmt = component_fmt;
+ rmt_strip->bytes_per_pixel = bytes_per_pixel;
+ rmt_strip->strip_len = led_config->max_leds;
+ *ret_strip = rmt_strip;
+ return ESP_OK;
+err:
+ if (rmt_strip) {
+ if (rmt_strip->rmt_chan) {
+ rmt_del_channel(rmt_strip->rmt_chan);
+ }
+ if (rmt_strip->strip_encoder) {
+ rmt_del_encoder(rmt_strip->strip_encoder);
+ }
+ free(rmt_strip);
+ }
+ return ret;
+}
diff --git a/Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_encoder.c b/Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_encoder.c
new file mode 100644
index 000000000..b96186847
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_encoder.c
@@ -0,0 +1,194 @@
+/*
+ * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "sdkconfig.h"
+#include "esp_idf_version.h"
+#include "esp_check.h"
+#include "esp_attr.h"
+#include "esp32_led_strip_rmt_encoder.h"
+
+static const char *TAG = "led_rmt_encoder";
+
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0)
+#if CONFIG_RMT_ISR_IRAM_SAFE
+#define RMT_ENCODER_FUNC_ATTR IRAM_ATTR
+#else
+#define RMT_ENCODER_FUNC_ATTR
+#endif // CONFIG_RMT_ISR_IRAM_SAFE
+#endif // ESP_IDF_VERSION
+
+typedef struct {
+ rmt_encoder_t base;
+ rmt_encoder_t *bytes_encoder;
+ rmt_encoder_t *copy_encoder;
+ int state;
+ rmt_symbol_word_t reset_code;
+} rmt_led_strip_encoder_t;
+
+RMT_ENCODER_FUNC_ATTR
+static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
+{
+ rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
+ rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder;
+ rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder;
+ rmt_encode_state_t session_state = 0;
+ rmt_encode_state_t state = 0;
+ size_t encoded_symbols = 0;
+ switch (led_encoder->state) {
+ case 0: // send RGB data
+ encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state);
+ if (session_state & RMT_ENCODING_COMPLETE) {
+ led_encoder->state = 1; // switch to next state when current encoding session finished
+ }
+ if (session_state & RMT_ENCODING_MEM_FULL) {
+ state |= RMT_ENCODING_MEM_FULL;
+ goto out; // yield if there's no free space for encoding artifacts
+ }
+ // fall-through
+ case 1: // send reset code
+ encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code,
+ sizeof(led_encoder->reset_code), &session_state);
+ if (session_state & RMT_ENCODING_COMPLETE) {
+ led_encoder->state = 0; // back to the initial encoding session
+ state |= RMT_ENCODING_COMPLETE;
+ }
+ if (session_state & RMT_ENCODING_MEM_FULL) {
+ state |= RMT_ENCODING_MEM_FULL;
+ goto out; // yield if there's no free space for encoding artifacts
+ }
+ }
+out:
+ *ret_state = state;
+ return encoded_symbols;
+}
+
+static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder)
+{
+ rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
+ rmt_del_encoder(led_encoder->bytes_encoder);
+ rmt_del_encoder(led_encoder->copy_encoder);
+ free(led_encoder);
+ return ESP_OK;
+}
+
+RMT_ENCODER_FUNC_ATTR
+static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder)
+{
+ rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
+ rmt_encoder_reset(led_encoder->bytes_encoder);
+ rmt_encoder_reset(led_encoder->copy_encoder);
+ led_encoder->state = 0;
+ return ESP_OK;
+}
+
+esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
+{
+ esp_err_t ret = ESP_OK;
+ rmt_led_strip_encoder_t *led_encoder = NULL;
+ ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+ ESP_GOTO_ON_FALSE(config->led_model < LED_MODEL_INVALID, ESP_ERR_INVALID_ARG, err, TAG, "invalid led model");
+ led_encoder = calloc(1, sizeof(rmt_led_strip_encoder_t));
+ ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder");
+ led_encoder->base.encode = rmt_encode_led_strip;
+ led_encoder->base.del = rmt_del_led_strip_encoder;
+ led_encoder->base.reset = rmt_led_strip_encoder_reset;
+ rmt_bytes_encoder_config_t bytes_encoder_config;
+ uint32_t reset_ticks = config->resolution / 1000000 * 280 / 2; // reset code duration defaults to 280us to accommodate WS2812B-V5
+ if (config->led_model == LED_MODEL_SK6812) {
+ bytes_encoder_config = (rmt_bytes_encoder_config_t) {
+ .bit0 = {
+ .level0 = 1,
+ .duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
+ .level1 = 0,
+ .duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
+ },
+ .bit1 = {
+ .level0 = 1,
+ .duration0 = 0.6 * config->resolution / 1000000, // T1H=0.6us
+ .level1 = 0,
+ .duration1 = 0.6 * config->resolution / 1000000, // T1L=0.6us
+ },
+ .flags.msb_first = 1 // SK6812 transfer bit order: G7...G0R7...R0B7...B0(W7...W0)
+ };
+ } else if (config->led_model == LED_MODEL_WS2812) {
+ // different led strip might have its own timing requirements, following parameter is for WS2812
+ bytes_encoder_config = (rmt_bytes_encoder_config_t) {
+ .bit0 = {
+ .level0 = 1,
+ .duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
+ .level1 = 0,
+ .duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
+ },
+ .bit1 = {
+ .level0 = 1,
+ .duration0 = 0.9 * config->resolution / 1000000, // T1H=0.9us
+ .level1 = 0,
+ .duration1 = 0.3 * config->resolution / 1000000, // T1L=0.3us
+ },
+ .flags.msb_first = 1 // WS2812 transfer bit order: G7...G0R7...R0B7...B0
+ };
+ } else if (config->led_model == LED_MODEL_WS2811) {
+ // different led strip might have its own timing requirements, following parameter is for WS2811
+ bytes_encoder_config = (rmt_bytes_encoder_config_t) {
+ .bit0 = {
+ .level0 = 1,
+ .duration0 = 0.5 * config->resolution / 1000000., // T0H=0.5us
+ .level1 = 0,
+ .duration1 = 2.0 * config->resolution / 1000000., // T0L=2.0us
+ },
+ .bit1 = {
+ .level0 = 1,
+ .duration0 = 1.2 * config->resolution / 1000000., // T1H=1.2us
+ .level1 = 0,
+ .duration1 = 1.3 * config->resolution / 1000000., // T1L=1.3us
+ },
+ .flags.msb_first = 1
+ };
+ reset_ticks = config->resolution / 1000000 * 50 / 2; // divide by 2... signal is sent twice
+ } else if (config->led_model == LED_MODEL_WS2816) {
+ // different led strip might have its own timing requirements, following parameter is for WS2816
+ bytes_encoder_config = (rmt_bytes_encoder_config_t) {
+ .bit0 = {
+ .level0 = 1,
+ .duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
+ .level1 = 0,
+ .duration1 = 0.95 * config->resolution / 1000000, // T0L=0.95us
+ },
+ .bit1 = {
+ .level0 = 1,
+ .duration0 = 0.75 * config->resolution / 1000000, // T1H=0.75us
+ .level1 = 0,
+ .duration1 = 0.5 * config->resolution / 1000000, // T1L=0.5us
+ },
+ .flags.msb_first = 1
+ };
+ } else {
+ assert(false);
+ }
+ ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed");
+ rmt_copy_encoder_config_t copy_encoder_config = {};
+ ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(©_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed");
+
+ led_encoder->reset_code = (rmt_symbol_word_t) {
+ .level0 = 0,
+ .duration0 = reset_ticks,
+ .level1 = 0,
+ .duration1 = reset_ticks,
+ };
+ *ret_encoder = &led_encoder->base;
+ return ESP_OK;
+err:
+ if (led_encoder) {
+ if (led_encoder->bytes_encoder) {
+ rmt_del_encoder(led_encoder->bytes_encoder);
+ }
+ if (led_encoder->copy_encoder) {
+ rmt_del_encoder(led_encoder->copy_encoder);
+ }
+ free(led_encoder);
+ }
+ return ret;
+}
diff --git a/Drivers/esp32-led-strip-module/source/module.cpp b/Drivers/esp32-led-strip-module/source/module.cpp
new file mode 100644
index 000000000..05204f4ae
--- /dev/null
+++ b/Drivers/esp32-led-strip-module/source/module.cpp
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0
+#include
+#include
+
+extern "C" {
+
+extern Driver esp32_led_strip_driver;
+
+static Driver* const esp32_led_strip_drivers[] = {
+ &esp32_led_strip_driver,
+ nullptr
+};
+
+Module esp32_led_strip_module = {
+ .name = "esp32_led_strip",
+ .start = nullptr,
+ .stop = nullptr,
+ .drivers = esp32_led_strip_drivers,
+ .symbols = nullptr,
+ .internal = nullptr
+};
+
+} // extern "C"
diff --git a/Modules/lvgl-module/assets/generate-all.py b/Modules/lvgl-module/assets/generate-all.py
index 3a67c271c..6b19da426 100644
--- a/Modules/lvgl-module/assets/generate-all.py
+++ b/Modules/lvgl-module/assets/generate-all.py
@@ -114,6 +114,7 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam
"terminal",
"logo_dev",
"camera",
+ "lightstrip",
]
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
diff --git a/Modules/lvgl-module/include/lvgl/icons/shared.h b/Modules/lvgl-module/include/lvgl/icons/shared.h
index d2fcbd451..7c745beff 100644
--- a/Modules/lvgl-module/include/lvgl/icons/shared.h
+++ b/Modules/lvgl-module/include/lvgl/icons/shared.h
@@ -48,3 +48,4 @@
#define LVGL_ICON_SHARED_TERMINAL "\xEE\xAE\x8E"
#define LVGL_ICON_SHARED_LOGO_DEV "\xEE\xAB\x96"
#define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF"
+#define LVGL_ICON_SHARED_LIGHTSTRIP "\xF3\xBF\xBD\xB5"
diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c
index 8c0c11f52..0ca7af0fa 100644
--- a/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c
+++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_12.c
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 12 px
* Bpp: 2
- * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_12.c --force-fast-kern-format
+ * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_12.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -258,7 +258,13 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x80, 0x0, 0x9, 0x0, 0x70, 0x91, 0xcd,
0x9, 0xd, 0xc0, 0x90, 0xac, 0x4, 0x6, 0xd0,
0x0, 0x97, 0x0, 0x1c, 0x2c, 0x7, 0x0, 0x7f,
- 0x80
+ 0x80,
+
+ /* U+FFF75 "" */
+ 0xff, 0xff, 0xec, 0x41, 0x13, 0xd5, 0x55, 0x7a,
+ 0xaf, 0xef, 0xb, 0xd1, 0x3b, 0x81, 0x2e, 0xd5,
+ 0x7f, 0x5e, 0xaa, 0xab, 0xc4, 0x11, 0x3b, 0xff,
+ 0xff
};
@@ -313,7 +319,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 883, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2},
{.bitmap_index = 899, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2},
{.bitmap_index = 923, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = 1},
- {.bitmap_index = 946, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}
+ {.bitmap_index = 946, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1},
+ {.bitmap_index = 971, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}
};
/*---------------------
@@ -335,6 +342,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
+ },
+ {
+ .range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@@ -359,7 +370,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
- .cmap_num = 1,
+ .cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,
diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c
index dad2feff7..63d932133 100644
--- a/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c
+++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_16.c
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 16 px
* Bpp: 2
- * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_16.c --force-fast-kern-format
+ * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_16.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -361,7 +361,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xca, 0x0, 0x80, 0xc, 0xa0, 0x0, 0x0, 0xc7,
0x0, 0x0, 0x2c, 0x38, 0x0, 0x3, 0x40, 0xe0,
0x1, 0xe0, 0x3, 0xea, 0xf8, 0x0, 0x6, 0xf8,
- 0x0
+ 0x0,
+
+ /* U+FFF75 "" */
+ 0x55, 0x55, 0x55, 0xf, 0xff, 0xff, 0xfc, 0xd1,
+ 0x4, 0x4, 0xde, 0x55, 0x55, 0x5d, 0xff, 0xff,
+ 0xff, 0xd0, 0x7, 0xf4, 0xd, 0x1b, 0xe0, 0x21,
+ 0xd7, 0x90, 0x82, 0xf8, 0xe1, 0x7, 0xf8, 0xf,
+ 0xff, 0xff, 0xfd, 0xe5, 0x55, 0x55, 0xdd, 0x20,
+ 0x80, 0x8d, 0xb5, 0x55, 0x55, 0xd2, 0xff, 0xff,
+ 0xfd
};
@@ -416,7 +425,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1630, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2},
{.bitmap_index = 1666, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2},
{.bitmap_index = 1714, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1},
- {.bitmap_index = 1756, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}
+ {.bitmap_index = 1756, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1},
+ {.bitmap_index = 1805, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}
};
/*---------------------
@@ -438,6 +448,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
+ },
+ {
+ .range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@@ -462,7 +476,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
- .cmap_num = 1,
+ .cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,
diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c
index 11eb44f8f..d7525abc6 100644
--- a/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c
+++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_20.c
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 20 px
* Bpp: 2
- * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_20.c --force-fast-kern-format
+ * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_20.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -480,6 +480,19 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x7, 0x80, 0xf4, 0x0, 0x0, 0xf0, 0x3,
0xe0, 0x0, 0x7c, 0x0, 0xb, 0xea, 0xbf, 0x0,
0x0, 0x1b, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
+ 0x0,
+
+ /* U+FFF75 "" */
+ 0x2a, 0xaa, 0xaa, 0xa9, 0x3, 0xff, 0xff, 0xff,
+ 0xfc, 0x38, 0x0, 0x0, 0x1, 0xd3, 0x89, 0x7,
+ 0x3, 0xe, 0x38, 0x0, 0x0, 0x0, 0xe3, 0xff,
+ 0xff, 0xff, 0xfe, 0x2a, 0xab, 0xff, 0xab, 0xe0,
+ 0x1, 0xff, 0x44, 0xe, 0x6, 0xfe, 0x0, 0x82,
+ 0xd2, 0xf9, 0x2, 0x6, 0xf8, 0x38, 0x30, 0x1b,
+ 0xf8, 0x3, 0xea, 0xaf, 0xfe, 0xa9, 0x3f, 0xff,
+ 0xff, 0xff, 0xe3, 0x80, 0x0, 0x1, 0xe, 0x38,
+ 0x90, 0x20, 0x20, 0xe3, 0xea, 0xaa, 0xaa, 0xae,
+ 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x0, 0x0, 0x0,
0x0
};
@@ -535,7 +548,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 2555, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3},
{.bitmap_index = 2604, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3},
{.bitmap_index = 2674, .adv_w = 320, .box_w = 16, .box_h = 18, .ofs_x = 2, .ofs_y = 1},
- {.bitmap_index = 2746, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}
+ {.bitmap_index = 2746, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1},
+ {.bitmap_index = 2827, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}
};
/*---------------------
@@ -557,6 +571,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
+ },
+ {
+ .range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@@ -581,7 +599,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
- .cmap_num = 1,
+ .cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,
diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c
index 18ca9baeb..d7c544e2e 100644
--- a/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c
+++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_24.c
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 24 px
* Bpp: 2
- * Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_24.c --force-fast-kern-format
+ * Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_24.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -582,7 +582,22 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x0, 0x7c, 0x1f, 0x0, 0x0, 0x0, 0xf4,
0xb, 0xd0, 0x0, 0x7, 0xe0, 0x2, 0xfd, 0x0,
0x7f, 0x80, 0x0, 0x7f, 0xff, 0xfd, 0x0, 0x0,
- 0x6, 0xff, 0x90, 0x0
+ 0x6, 0xff, 0x90, 0x0,
+
+ /* U+FFF75 "" */
+ 0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff,
+ 0xff, 0xfe, 0xf0, 0x0, 0x0, 0x0, 0xf, 0xf0,
+ 0xd0, 0x28, 0x7, 0xf, 0xf0, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0x0, 0x2, 0xff, 0x80, 0xf,
+ 0x0, 0x7f, 0xf4, 0x28, 0xf, 0xb, 0xfe, 0x0,
+ 0x0, 0x2f, 0x7f, 0x90, 0x28, 0x7, 0xfd, 0xf8,
+ 0x34, 0x1, 0xbf, 0xe0, 0xf0, 0x10, 0x6f, 0xf9,
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xf0, 0x0, 0x0, 0x0, 0xf,
+ 0xf0, 0xd0, 0x28, 0x7, 0xf, 0xf0, 0x0, 0x0,
+ 0x0, 0xf, 0xbf, 0xff, 0xff, 0xff, 0xff, 0x2f,
+ 0xff, 0xff, 0xff, 0xff
};
@@ -637,7 +652,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 3288, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4},
{.bitmap_index = 3352, .adv_w = 384, .box_w = 22, .box_h = 16, .ofs_x = 1, .ofs_y = 4},
{.bitmap_index = 3440, .adv_w = 384, .box_w = 18, .box_h = 20, .ofs_x = 3, .ofs_y = 2},
- {.bitmap_index = 3530, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}
+ {.bitmap_index = 3530, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2},
+ {.bitmap_index = 3630, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}
};
/*---------------------
@@ -659,6 +675,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
+ },
+ {
+ .range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@@ -683,7 +703,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
- .cmap_num = 1,
+ .cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,
diff --git a/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c b/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c
index 90fd2664b..013b37479 100644
--- a/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c
+++ b/Modules/lvgl-module/source-fonts/material_symbols_shared_32.c
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 32 px
* Bpp: 2
- * Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_32.c --force-fast-kern-format
+ * Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_32.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -980,6 +980,33 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0xb, 0xff, 0xaa, 0xbf, 0xf4, 0x0, 0x0,
0x1, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
0x1b, 0xff, 0xe4, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0,
+
+ /* U+FFF75 "" */
+ 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x40, 0x3f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3e, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xbd, 0x3e, 0x7, 0x0, 0x1c,
+ 0x0, 0x70, 0x3e, 0x3e, 0xb, 0x0, 0x2c, 0x0,
+ 0x70, 0x3e, 0x3e, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x3e, 0x3f, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbe,
+ 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0,
+ 0x6, 0xff, 0xf8, 0x0, 0x7e, 0x0, 0x0, 0xbf,
+ 0xfe, 0x42, 0x40, 0x3e, 0x0, 0x1f, 0xff, 0xd0,
+ 0x3, 0x80, 0x3e, 0x2, 0xff, 0xf8, 0x1c, 0x0,
+ 0x1, 0xfd, 0xf, 0xfe, 0x40, 0x2c, 0x0, 0x6f,
+ 0xf8, 0x3f, 0xe0, 0xd0, 0x0, 0xb, 0xff, 0xe0,
+ 0x3e, 0x1, 0xd0, 0x2, 0xff, 0xfd, 0x0, 0x3f,
+ 0x0, 0x0, 0x7f, 0xff, 0x80, 0x0, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xbe, 0x3e, 0x1, 0x0, 0x4, 0x0,
+ 0x10, 0x3e, 0x3e, 0xb, 0x0, 0x2c, 0x0, 0xb4,
+ 0x3e, 0x3e, 0x1, 0x0, 0x4, 0x0, 0x10, 0x3e,
+ 0x3f, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbe, 0x1f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0
};
@@ -1035,7 +1062,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 6130, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5},
{.bitmap_index = 6251, .adv_w = 512, .box_w = 30, .box_h = 22, .ofs_x = 1, .ofs_y = 5},
{.bitmap_index = 6416, .adv_w = 512, .box_w = 25, .box_h = 28, .ofs_x = 4, .ofs_y = 2},
- {.bitmap_index = 6591, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}
+ {.bitmap_index = 6591, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2},
+ {.bitmap_index = 6787, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}
};
/*---------------------
@@ -1057,6 +1085,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
+ },
+ {
+ .range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
+ .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@@ -1081,7 +1113,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
- .cmap_num = 1,
+ .cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,
diff --git a/Modules/lvgl-module/source/widgets/toolbar.cpp b/Modules/lvgl-module/source/widgets/toolbar.cpp
index 5a223a83c..cac2e1167 100644
--- a/Modules/lvgl-module/source/widgets/toolbar.cpp
+++ b/Modules/lvgl-module/source/widgets/toolbar.cpp
@@ -59,6 +59,7 @@ typedef struct {
lv_obj_t* close_button_image;
lv_obj_t* action_container;
uint8_t action_count;
+ lv_event_cb_t nav_action_callback;
} Toolbar;
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
@@ -105,6 +106,7 @@ lv_obj_t* lvgl_toolbar_create(lv_obj_t* parent, const char* title) {
lv_obj_set_height(obj, toolbar_height);
auto* toolbar = reinterpret_cast(obj);
+ toolbar->nav_action_callback = nullptr;
lv_obj_set_width(obj, LV_PCT(100));
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(obj, 0, LV_STATE_DEFAULT);
@@ -176,7 +178,13 @@ void lvgl_toolbar_set_title(lv_obj_t* obj, const char* title) {
void lvgl_toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) {
auto* toolbar = reinterpret_cast(obj);
- lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
+ if (toolbar->nav_action_callback != nullptr) {
+ lv_obj_remove_event_cb(toolbar->close_button, toolbar->nav_action_callback);
+ }
+ toolbar->nav_action_callback = callback;
+ if (callback != nullptr) {
+ lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
+ }
lv_image_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE
}
diff --git a/Tactility/Include/Tactility/settings/LedStripSettings.h b/Tactility/Include/Tactility/settings/LedStripSettings.h
new file mode 100644
index 000000000..035e0eb35
--- /dev/null
+++ b/Tactility/Include/Tactility/settings/LedStripSettings.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include
+#include
+
+namespace tt::settings::ledstrip {
+
+enum class Pattern : uint8_t {
+ Solid,
+ Alternating,
+ Gradient,
+};
+
+struct LedStripSettings {
+ bool enabled = true;
+ Pattern pattern = Pattern::Alternating;
+ uint8_t brightness = 77;
+ uint8_t primaryPreset = 6;
+ uint8_t secondaryPreset = 7;
+ struct LedRgb primaryCustom { 255, 165, 0 }; //Orange
+ struct LedRgb secondaryCustom { 0, 0, 255 }; //Blue
+};
+
+bool load(const char* deviceName, LedStripSettings& settings);
+
+LedStripSettings loadOrGetDefault(const char* deviceName);
+
+LedStripSettings getDefault();
+
+bool save(const char* deviceName, const LedStripSettings& settings);
+
+error_t apply(struct Device* device, const LedStripSettings& settings);
+
+}
\ No newline at end of file
diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp
index ae52fadf2..e6bf9e71a 100644
--- a/Tactility/Source/Tactility.cpp
+++ b/Tactility/Source/Tactility.cpp
@@ -91,6 +91,7 @@
extern "C" Module tactility_audio_module;
#include
#include
+#include
#include
#include
#include
@@ -176,6 +177,7 @@ namespace app {
namespace imageviewer { extern const ::AppManifest manifest; }
namespace inputdialog { extern const ::AppManifest manifest; }
namespace launcher { extern const ::AppManifest manifest; }
+ namespace ledstripsettings { extern const ::AppManifest manifest; }
namespace localesettings { extern const ::AppManifest manifest; }
namespace notes { extern const ::AppManifest manifest; }
namespace power { extern const ::AppManifest manifest; }
@@ -237,6 +239,9 @@ static void registerInternalApps() {
if (device_exists_of_type(&DISPLAY_TYPE)) {
app_manager_add(&app::display::manifest);
}
+ if (device_exists_of_type(&LED_STRIP_TYPE)) {
+ app_manager_add(&app::ledstripsettings::manifest);
+ }
app_manager_add(&app::files::manifest);
app_manager_add(&app::fileselection::manifest);
app_manager_add(&app::i2cscanner::manifest);
diff --git a/Tactility/Source/app/ledstripsettings/LedStripSettings.cpp b/Tactility/Source/app/ledstripsettings/LedStripSettings.cpp
new file mode 100644
index 000000000..9f3bbb308
--- /dev/null
+++ b/Tactility/Source/app/ledstripsettings/LedStripSettings.cpp
@@ -0,0 +1,474 @@
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+namespace tt::app::ledstripsettings {
+
+extern const ::AppManifest manifest;
+
+constexpr auto* TAG = "LedStripSettings";
+constexpr uint8_t CUSTOM_PRESET = 12;
+constexpr const char* COLOR_NAMES[] = {
+ "White", "Green", "Red", "Blue",
+ "Yellow", "Orange", "Cyan", "Magenta",
+ "Purple", "Pink", "Teal", "Black"
+};
+
+namespace {
+
+struct Context {
+ uint32_t appInstanceId = 0;
+ Device* device = nullptr;
+ settings::ledstrip::LedStripSettings settings;
+ bool updated = false;
+ enum class Page : uint8_t { Main, Choices, CustomColor } page = Page::Main;
+ enum class ChoiceKind : uint8_t { None, Pattern, PrimaryColor, SecondaryColor } choiceKind = ChoiceKind::None;
+ bool editingSecondary = false;
+ lv_obj_t* toolbar = nullptr;
+ lv_obj_t* mainPanel = nullptr;
+ lv_obj_t* choicePanel = nullptr;
+ lv_obj_t* choiceList = nullptr;
+ lv_obj_t* customPanel = nullptr;
+ lv_obj_t* returnFocus = nullptr;
+ lv_obj_t* modeValue = nullptr;
+ lv_obj_t* primaryValue = nullptr;
+ lv_obj_t* secondaryRow = nullptr;
+ lv_obj_t* secondaryValue = nullptr;
+ lv_obj_t* primaryEditor = nullptr;
+ lv_obj_t* secondaryEditor = nullptr;
+ lv_obj_t* primarySliderBoxes[3] = {};
+ lv_obj_t* secondarySliderBoxes[3] = {};
+ lv_obj_t* brightnessSlider = nullptr;
+ lv_obj_t* brightnessValue = nullptr;
+};
+
+const char* patternName(settings::ledstrip::Pattern pattern) {
+ switch (pattern) {
+ case settings::ledstrip::Pattern::Solid: return "Solid";
+ case settings::ledstrip::Pattern::Alternating: return "Alternating";
+ case settings::ledstrip::Pattern::Gradient: return "Gradient";
+ }
+ return "Solid";
+}
+
+const char* presetName(uint8_t preset) {
+ if (preset == CUSTOM_PRESET) return "Custom";
+ return COLOR_NAMES[preset < CUSTOM_PRESET ? preset : 0];
+}
+
+void applyAndLog(Context& ctx) {
+ const error_t result = settings::ledstrip::apply(ctx.device, ctx.settings);
+ if (result != ERROR_NONE) {
+ LOG_W(TAG, "Failed to apply LED strip settings: %d", result);
+ }
+}
+
+void updateVisibility(Context& ctx) {
+ const bool hasSecondary = ctx.settings.pattern != settings::ledstrip::Pattern::Solid;
+ if (hasSecondary) {
+ lv_obj_remove_flag(ctx.secondaryRow, LV_OBJ_FLAG_HIDDEN);
+ } else {
+ lv_obj_add_flag(ctx.secondaryRow, LV_OBJ_FLAG_HIDDEN);
+ }
+}
+
+void onNavigationPressed(lv_event_t* event);
+
+void returnToMain(Context& ctx) {
+ ctx.page = Context::Page::Main;
+ ctx.choiceKind = Context::ChoiceKind::None;
+ lv_obj_add_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(ctx.customPanel, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_remove_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_update_layout(ctx.mainPanel);
+ lvgl_toolbar_set_title(ctx.toolbar, "LED Strip");
+ lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_CLOSE, onNavigationPressed, &ctx);
+ if (ctx.returnFocus != nullptr) {
+ lv_group_focus_obj(ctx.returnFocus);
+ }
+}
+
+void onNavigationPressed(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ if (ctx->page != Context::Page::Main) {
+ returnToMain(*ctx);
+ } else {
+ app_event_emit_close(ctx->appInstanceId);
+ }
+}
+
+void openCustomEditor(Context& ctx, bool secondary) {
+ ctx.page = Context::Page::CustomColor;
+ ctx.choiceKind = Context::ChoiceKind::None;
+ ctx.editingSecondary = secondary;
+ lv_obj_add_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_remove_flag(ctx.customPanel, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_add_flag(secondary ? ctx.primaryEditor : ctx.secondaryEditor, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_remove_flag(secondary ? ctx.secondaryEditor : ctx.primaryEditor, LV_OBJ_FLAG_HIDDEN);
+ lvgl_toolbar_set_title(ctx.toolbar, secondary ? "Custom second color" : "Custom color");
+ lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_LEFT, onNavigationPressed, &ctx);
+ lv_obj_update_layout(ctx.customPanel);
+ auto* sliderBox = secondary ? ctx.secondarySliderBoxes[0] : ctx.primarySliderBoxes[0];
+ lv_group_focus_obj(sliderBox);
+}
+
+size_t choiceCount(Context::ChoiceKind kind) {
+ return kind == Context::ChoiceKind::Pattern ? 3 : CUSTOM_PRESET + 1;
+}
+
+uint32_t currentChoice(const Context& ctx) {
+ switch (ctx.choiceKind) {
+ case Context::ChoiceKind::Pattern: return static_cast(ctx.settings.pattern);
+ case Context::ChoiceKind::PrimaryColor: return ctx.settings.primaryPreset;
+ case Context::ChoiceKind::SecondaryColor: return ctx.settings.secondaryPreset;
+ case Context::ChoiceKind::None: return 0;
+ }
+ return 0;
+}
+
+const char* choiceTitle(Context::ChoiceKind kind) {
+ switch (kind) {
+ case Context::ChoiceKind::Pattern: return "Choose mode";
+ case Context::ChoiceKind::PrimaryColor: return "Choose color";
+ case Context::ChoiceKind::SecondaryColor: return "Choose second color";
+ case Context::ChoiceKind::None: return "LED Strip";
+ }
+ return "LED Strip";
+}
+
+const char* choiceName(Context::ChoiceKind kind, size_t index) {
+ if (kind == Context::ChoiceKind::Pattern) {
+ return patternName(static_cast(index));
+ }
+ return index == CUSTOM_PRESET ? "Custom" : COLOR_NAMES[index];
+}
+
+void onChoiceSelected(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ const size_t selected = lv_obj_get_index(static_cast(lv_event_get_target(event)));
+ if (selected >= choiceCount(ctx->choiceKind)) return;
+
+ const auto choiceKind = ctx->choiceKind;
+ switch (ctx->choiceKind) {
+ case Context::ChoiceKind::Pattern:
+ ctx->settings.pattern = static_cast(selected);
+ break;
+ case Context::ChoiceKind::PrimaryColor:
+ ctx->settings.primaryPreset = static_cast(selected);
+ break;
+ case Context::ChoiceKind::SecondaryColor:
+ ctx->settings.secondaryPreset = static_cast(selected);
+ break;
+ case Context::ChoiceKind::None:
+ return;
+ }
+
+ ctx->updated = true;
+ lv_label_set_text(ctx->modeValue, patternName(ctx->settings.pattern));
+ lv_label_set_text(ctx->primaryValue, presetName(ctx->settings.primaryPreset));
+ lv_label_set_text(ctx->secondaryValue, presetName(ctx->settings.secondaryPreset));
+ updateVisibility(*ctx);
+ if (selected == CUSTOM_PRESET && choiceKind != Context::ChoiceKind::Pattern) {
+ openCustomEditor(*ctx, choiceKind == Context::ChoiceKind::SecondaryColor);
+ applyAndLog(*ctx);
+ return;
+ }
+ returnToMain(*ctx);
+ applyAndLog(*ctx);
+}
+
+void openChoice(Context& ctx, Context::ChoiceKind kind, lv_obj_t* returnFocus) {
+ ctx.page = Context::Page::Choices;
+ ctx.choiceKind = kind;
+ ctx.returnFocus = returnFocus;
+ lv_obj_clean(ctx.choiceList);
+
+ const size_t selected = currentChoice(ctx);
+ lv_obj_t* selectedButton = nullptr;
+ for (size_t index = 0; index < choiceCount(kind); index++) {
+ auto* button = lv_list_add_button(ctx.choiceList, index == selected ? LV_SYMBOL_OK : nullptr, choiceName(kind, index));
+ lv_obj_add_event_cb(button, onChoiceSelected, LV_EVENT_SHORT_CLICKED, &ctx);
+ if (index == selected) selectedButton = button;
+ }
+
+ lvgl_toolbar_set_title(ctx.toolbar, choiceTitle(kind));
+ lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_LEFT, onNavigationPressed, &ctx);
+ lv_obj_add_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_remove_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
+ if (selectedButton != nullptr) {
+ lv_group_focus_obj(selectedButton);
+ }
+}
+
+void onOutputChanged(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ auto* sw = static_cast(lv_event_get_target(event));
+ ctx->settings.enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
+ ctx->updated = true;
+ applyAndLog(*ctx);
+}
+
+void onPatternPressed(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ openChoice(*ctx, Context::ChoiceKind::Pattern, static_cast(lv_event_get_target(event)));
+}
+
+void onPrimaryColorPressed(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ openChoice(*ctx, Context::ChoiceKind::PrimaryColor, static_cast(lv_event_get_target(event)));
+}
+
+void onSecondaryColorPressed(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ openChoice(*ctx, Context::ChoiceKind::SecondaryColor, static_cast(lv_event_get_target(event)));
+}
+
+void setChannel(LedRgb& color, size_t channel, uint8_t value) {
+ switch (channel) {
+ case 0: color.r = value; break;
+ case 1: color.g = value; break;
+ case 2: color.b = value; break;
+ default: break;
+ }
+}
+
+void onColorSliderChanged(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ auto* target = static_cast(lv_event_get_target(event));
+ const uint8_t value = static_cast(lvgl_sliderbox_get_value(target));
+
+ for (size_t channel = 0; channel < 3; channel++) {
+ if (target == ctx->primarySliderBoxes[channel]) {
+ setChannel(ctx->settings.primaryCustom, channel, value);
+ ctx->updated = true;
+ applyAndLog(*ctx);
+ return;
+ }
+ if (target == ctx->secondarySliderBoxes[channel]) {
+ setChannel(ctx->settings.secondaryCustom, channel, value);
+ ctx->updated = true;
+ applyAndLog(*ctx);
+ return;
+ }
+ }
+}
+
+void onBrightnessChanged(lv_event_t* event) {
+ auto* ctx = static_cast(lv_event_get_user_data(event));
+ const auto value = static_cast(lvgl_sliderbox_get_value(static_cast(lv_event_get_target(event))));
+ ctx->settings.brightness = value;
+ lv_label_set_text_fmt(ctx->brightnessValue, "%u%%", (static_cast(value) * 100 + 127) / 255);
+ ctx->updated = true;
+ applyAndLog(*ctx);
+}
+
+lv_obj_t* createRow(lv_obj_t* parent, const char* title) {
+ auto* row = lv_obj_create(parent);
+ lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
+ lv_obj_set_style_pad_all(row, 2, LV_STATE_DEFAULT);
+ lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
+ auto* label = lv_label_create(row);
+ lv_label_set_text(label, title);
+ lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0);
+ return row;
+}
+
+void createRgbEditor(lv_obj_t* parent, const char* title, const LedRgb& color, lv_obj_t** sliderBoxes, Context* ctx, bool primary) {
+ auto* editor = lv_obj_create(parent);
+ lv_obj_set_width(editor, LV_PCT(100));
+ lv_obj_set_flex_flow(editor, LV_FLEX_FLOW_COLUMN);
+ lv_obj_set_style_pad_all(editor, 0, LV_STATE_DEFAULT);
+ lv_obj_set_style_border_width(editor, 0, LV_STATE_DEFAULT);
+ auto* heading = lv_label_create(editor);
+ lv_label_set_text(heading, title);
+
+ constexpr const char* CHANNELS[] = { "R", "G", "B" };
+ const uint8_t channels[] = { color.r, color.g, color.b };
+ for (size_t i = 0; i < 3; i++) {
+ auto* row = lv_obj_create(editor);
+ lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
+ lv_obj_set_style_pad_all(row, 1, LV_STATE_DEFAULT);
+ lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
+ auto* label = lv_label_create(row);
+ lv_label_set_text(label, CHANNELS[i]);
+ lv_obj_set_width(label, LV_PCT(12));
+ lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0);
+ sliderBoxes[i] = lvgl_sliderbox_create(row, 0, UINT8_MAX, 8, channels[i]);
+ lv_obj_set_width(sliderBoxes[i], LV_PCT(84));
+ lv_obj_align(sliderBoxes[i], LV_ALIGN_RIGHT_MID, 0, 0);
+ lvgl_sliderbox_add_value_changed_cb(sliderBoxes[i], onColorSliderChanged, ctx);
+ }
+ if (primary) {
+ ctx->primaryEditor = editor;
+ } else {
+ ctx->secondaryEditor = editor;
+ }
+}
+
+void createWidgets(lv_obj_t* parent, void* userData) {
+ auto* ctx = static_cast(userData);
+ lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
+ lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
+
+ ctx->toolbar = lvgl_toolbar_create(parent, "LED Strip");
+ lvgl_toolbar_set_nav_action(ctx->toolbar, LV_SYMBOL_CLOSE, onNavigationPressed, ctx);
+
+ ctx->mainPanel = lv_obj_create(parent);
+ lv_obj_set_width(ctx->mainPanel, LV_PCT(100));
+ lv_obj_set_flex_grow(ctx->mainPanel, 1);
+ lv_obj_set_flex_flow(ctx->mainPanel, LV_FLEX_FLOW_COLUMN);
+ lv_obj_set_style_pad_row(ctx->mainPanel, 2, LV_STATE_DEFAULT);
+ lv_obj_add_flag(ctx->mainPanel, LV_OBJ_FLAG_SCROLLABLE);
+
+ ctx->customPanel = lv_obj_create(parent);
+ lv_obj_set_width(ctx->customPanel, LV_PCT(100));
+ lv_obj_set_flex_grow(ctx->customPanel, 1);
+ lv_obj_set_flex_flow(ctx->customPanel, LV_FLEX_FLOW_COLUMN);
+ lv_obj_add_flag(ctx->customPanel, LV_OBJ_FLAG_HIDDEN);
+
+ auto* outputRow = createRow(ctx->mainPanel, "Output");
+ auto* outputSwitch = lv_switch_create(outputRow);
+ if (ctx->settings.enabled) lv_obj_add_state(outputSwitch, LV_STATE_CHECKED);
+ lv_obj_align(outputSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
+ lv_obj_add_event_cb(outputSwitch, onOutputChanged, LV_EVENT_VALUE_CHANGED, ctx);
+
+ auto* modeRow = createRow(ctx->mainPanel, "Mode");
+ auto* modeButton = lv_button_create(modeRow);
+ lv_obj_set_width(modeButton, LV_PCT(62));
+ lv_obj_align(modeButton, LV_ALIGN_RIGHT_MID, 0, 0);
+ ctx->modeValue = lv_label_create(modeButton);
+ lv_label_set_text(ctx->modeValue, patternName(ctx->settings.pattern));
+ lv_obj_center(ctx->modeValue);
+ lv_obj_add_event_cb(modeButton, onPatternPressed, LV_EVENT_SHORT_CLICKED, ctx);
+
+ auto* primaryRow = createRow(ctx->mainPanel, "Color");
+ auto* primaryButton = lv_button_create(primaryRow);
+ lv_obj_set_width(primaryButton, LV_PCT(62));
+ lv_obj_align(primaryButton, LV_ALIGN_RIGHT_MID, 0, 0);
+ ctx->primaryValue = lv_label_create(primaryButton);
+ lv_label_set_text(ctx->primaryValue, presetName(ctx->settings.primaryPreset));
+ lv_obj_center(ctx->primaryValue);
+ lv_obj_add_event_cb(primaryButton, onPrimaryColorPressed, LV_EVENT_SHORT_CLICKED, ctx);
+
+ createRgbEditor(ctx->customPanel, "Red / Green / Blue", ctx->settings.primaryCustom, ctx->primarySliderBoxes, ctx, true);
+ lv_obj_add_flag(ctx->primaryEditor, LV_OBJ_FLAG_HIDDEN);
+
+ ctx->secondaryRow = createRow(ctx->mainPanel, "Second color");
+ auto* secondaryButton = lv_button_create(ctx->secondaryRow);
+ lv_obj_set_width(secondaryButton, LV_PCT(62));
+ lv_obj_align(secondaryButton, LV_ALIGN_RIGHT_MID, 0, 0);
+ ctx->secondaryValue = lv_label_create(secondaryButton);
+ lv_label_set_text(ctx->secondaryValue, presetName(ctx->settings.secondaryPreset));
+ lv_obj_center(ctx->secondaryValue);
+ lv_obj_add_event_cb(secondaryButton, onSecondaryColorPressed, LV_EVENT_SHORT_CLICKED, ctx);
+
+ createRgbEditor(ctx->customPanel, "Red / Green / Blue", ctx->settings.secondaryCustom, ctx->secondarySliderBoxes, ctx, false);
+ lv_obj_add_flag(ctx->secondaryEditor, LV_OBJ_FLAG_HIDDEN);
+
+ auto* brightnessRow = createRow(ctx->mainPanel, "Brightness");
+ ctx->brightnessValue = lv_label_create(brightnessRow);
+ lv_label_set_text_fmt(ctx->brightnessValue, "%u%%", (static_cast(ctx->settings.brightness) * 100 + 127) / 255);
+ lv_obj_align(ctx->brightnessValue, LV_ALIGN_RIGHT_MID, 0, 0);
+ ctx->brightnessSlider = lvgl_sliderbox_create(ctx->mainPanel, 0, UINT8_MAX, 17, ctx->settings.brightness);
+ lv_obj_set_width(ctx->brightnessSlider, LV_PCT(100));
+ lvgl_sliderbox_add_value_changed_cb(ctx->brightnessSlider, onBrightnessChanged, ctx);
+
+ ctx->choicePanel = lv_obj_create(parent);
+ lv_obj_set_width(ctx->choicePanel, LV_PCT(100));
+ lv_obj_set_flex_grow(ctx->choicePanel, 1);
+ lv_obj_set_flex_flow(ctx->choicePanel, LV_FLEX_FLOW_COLUMN);
+ lv_obj_add_flag(ctx->choicePanel, LV_OBJ_FLAG_HIDDEN);
+ ctx->choiceList = lv_list_create(ctx->choicePanel);
+ lv_obj_set_width(ctx->choiceList, LV_PCT(100));
+ lv_obj_set_flex_grow(ctx->choiceList, 1);
+
+ updateVisibility(*ctx);
+}
+
+void persistIfUpdated(const Context& ctx) {
+ if (!ctx.updated) return;
+ const auto settingsToSave = ctx.settings;
+ const std::string deviceName = ctx.device->name;
+ getMainDispatcher().dispatch([settingsToSave, deviceName] {
+ if (!settings::ledstrip::save(deviceName.c_str(), settingsToSave)) {
+ LOG_W(TAG, "Failed to save LED strip settings");
+ }
+ });
+}
+
+int32_t appMain(int argc, char* argv[]) {
+ Device* device = nullptr;
+ if (device_get_first_by_type(&LED_STRIP_TYPE, &device) != ERROR_NONE) {
+ return 0;
+ }
+
+ Context ctx {};
+ ctx.appInstanceId = app_scheduler_current_app_id();
+ ctx.device = device;
+ ctx.settings = settings::ledstrip::loadOrGetDefault(device->name);
+ uint16_t ledCount = 0;
+ if (led_strip_get_length(device, &ledCount) != ERROR_NONE || ledCount == 0) {
+ device_put(device);
+ return 0;
+ }
+ applyAndLog(ctx);
+
+ TaskEventGroup eventGroup {};
+ task_event_group_construct(&eventGroup);
+ AppEventSubscription subscription {};
+ if (app_event_subscribe(&subscription, &eventGroup) != ERROR_NONE) {
+ task_event_group_destruct(&eventGroup);
+ device_put(device);
+ return 0;
+ }
+
+ const WindowId window = window_manager_create(ctx.appInstanceId, createWidgets, &ctx);
+ bool shouldClose = false;
+ while (!shouldClose) {
+ task_event_group_wait_any(&eventGroup, nullptr, portMAX_DELAY);
+ AppEvent event {};
+ while (app_event_poll(&subscription, &event) == ERROR_NONE) {
+ if (event.type == APP_EVENT_CLOSE) {
+ persistIfUpdated(ctx);
+ shouldClose = true;
+ break;
+ }
+ }
+ }
+
+ window_manager_remove(window);
+ app_event_unsubscribe(&subscription);
+ task_event_group_destruct(&eventGroup);
+ device_put(device);
+ return 0;
+}
+
+}
+
+extern const ::AppManifest manifest = {
+ .id = "tactility.ledstripsettings",
+ .name = "LED Strip",
+ .category = APP_CATEGORY_SETTINGS,
+ .location = { APP_LOCATION_MEMORY, reinterpret_cast(appMain) },
+ .flags = 0,
+ .stack = {}
+};
+
+}
\ No newline at end of file
diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp
index 94ffc6ca4..ccda269e2 100644
--- a/Tactility/Source/app/settings/Settings.cpp
+++ b/Tactility/Source/app/settings/Settings.cpp
@@ -43,6 +43,7 @@ constexpr IconEntry ICONS[] = {
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
+ {"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
diff --git a/Tactility/Source/settings/LedStripSettings.cpp b/Tactility/Source/settings/LedStripSettings.cpp
new file mode 100644
index 000000000..86d98c85e
--- /dev/null
+++ b/Tactility/Source/settings/LedStripSettings.cpp
@@ -0,0 +1,194 @@
+#include
+
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include