Преглед на файлове

Merge pull request #202 from wizmo2/led_visu-v4.3

Support for WS2812 RGB LED Strip v4.3
philippe44 преди 1 година
родител
ревизия
af8db69030

+ 14 - 0
README.md

@@ -307,6 +307,20 @@ See [set_GPIO](#set-gpio) for how to set the green and red LEDs. In addition, th
 ```
 NB: For well-known configuration, GPIO affected to green and red LED cannot be changed but brightness option applies
 
+### LED Strip
+One LED strip with up to 255 addressable LEDs can be configured to offer enhanced visualizations.  The LED strip can also be controlled remotely though the LMS server (using the CLI interface).  Currently only WS2812B LEDs are supported.  Set the LED Strip configuration (or NVS led_vu_config) to `WS2812,length=<n>,gpio=<gpio>, where <n> is the number of leds in the strip (1..255), and <gpio> is the data pin.`  
+
+The latest LMS plugin update is required to set the visualizer mode and brightness, in the ESP32 settings page for the player.  The plugin also adds the following CLI command options
+```
+<playerid> led_visual [<mode>] [brightness(1-255)]
+  Toggles or selects the visulaizer mode.
+  The visualizer brighness can be controled using the optional <brighness> tag.
+
+<playerid> dmx <R,G,B|R,G,B,R,G,B ... R,G,B> [<offset>]
+  Sets the LED at position "offset" to any RGB color where "R"(red),"G"(green), and "B"(blue) are values from 0(off) to 255(max brightness).
+  Add additional RGB values to the delimited string to set multiple LEDs. 
+```
+
 ### Rotary Encoder
 One rotary encoder is supported, quadrature shift with press. Such encoders usually have 2 pins for encoders (A and B), and common C that must be set to ground and an optional SW pin for press. A, B and SW must be pulled up, so automatic pull-up is provided by ESP32, but you can add your own resistors. A bit of filtering on A and B (~470nF) helps for debouncing which is not made by software. 
 

+ 11 - 0
components/led_strip/CMakeLists.txt

@@ -0,0 +1,11 @@
+
+idf_component_register(SRC_DIRS .
+						INCLUDE_DIRS .
+						REQUIRES platform_config tools esp_common
+						PRIV_REQUIRES services freertos driver           
+)
+
+set_source_files_properties(led_strip.c
+    PROPERTIES COMPILE_FLAGS
+   -Wno-format-overflow
+)

+ 202 - 0
components/led_strip/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   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.

+ 409 - 0
components/led_strip/led_strip.c

@@ -0,0 +1,409 @@
+/*  ----------------------------------------------------------------------------
+    File: led_strip.c
+    Author(s):  Lucas Bruder <LBruder@me.com>
+    Date Created: 11/23/2016
+    Last modified: 11/26/2016
+
+    Updated: C. Rohs  - The update thread now
+    only runs when signalled. The double buffer code was modified to copy on show
+    instead of the ping pong buffer that destroyed the buffers contents.
+
+    The current code is not thread safe, but is more performant, and the thread
+    safety does not matter the was it is currently used.
+
+    Description: LED Library for driving various led strips on ESP32.
+
+    This library uses double buffering to display the LEDs.
+    ------------------------------------------------------------------------- */
+
+#include "led_strip.h"
+#include "freertos/task.h"
+
+#include <string.h>
+
+#define LED_STRIP_TASK_SIZE             (1024)
+#define LED_STRIP_TASK_PRIORITY         (configMAX_PRIORITIES - 1)
+
+#define LED_STRIP_REFRESH_PERIOD_MS     (30U) // TODO: add as parameter to led_strip_init
+
+#define LED_STRIP_NUM_RMT_ITEMS_PER_LED (24U) // Assumes 24 bit color for each led
+
+// RMT Clock source is @ 80 MHz. Dividing it by 8 gives us 10 MHz frequency, or 100ns period.
+#define LED_STRIP_RMT_CLK_DIV (8)
+
+/****************************
+        WS2812 Timing
+ ****************************/
+#define LED_STRIP_RMT_TICKS_BIT_1_HIGH_WS2812 9 // 900ns (900ns +/- 150ns per datasheet)
+#define LED_STRIP_RMT_TICKS_BIT_1_LOW_WS2812  3 // 300ns (350ns +/- 150ns per datasheet)
+#define LED_STRIP_RMT_TICKS_BIT_0_HIGH_WS2812 3 // 300ns (350ns +/- 150ns per datasheet)
+#define LED_STRIP_RMT_TICKS_BIT_0_LOW_WS2812  9 // 900ns (900ns +/- 150ns per datasheet)
+
+/****************************
+        SK6812 Timing
+ ****************************/
+#define LED_STRIP_RMT_TICKS_BIT_1_HIGH_SK6812 6
+#define LED_STRIP_RMT_TICKS_BIT_1_LOW_SK6812  6
+#define LED_STRIP_RMT_TICKS_BIT_0_HIGH_SK6812 3
+#define LED_STRIP_RMT_TICKS_BIT_0_LOW_SK6812  9
+
+/****************************
+        APA106 Timing
+ ****************************/
+#define LED_STRIP_RMT_TICKS_BIT_1_HIGH_APA106 14 // 1.36us +/- 150ns per datasheet
+#define LED_STRIP_RMT_TICKS_BIT_1_LOW_APA106   3 // 350ns +/- 150ns per datasheet
+#define LED_STRIP_RMT_TICKS_BIT_0_HIGH_APA106  3 // 350ns +/- 150ns per datasheet
+#define LED_STRIP_RMT_TICKS_BIT_0_LOW_APA106  14 // 1.36us +/- 150ns per datasheet
+
+// Function pointer for generating waveforms based on different LED drivers
+typedef void (*led_fill_rmt_items_fn)(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length);
+
+static inline void led_strip_fill_item_level(rmt_item32_t* item, int high_ticks, int low_ticks)
+{
+    item->level0 = 1;
+    item->duration0 = high_ticks;
+    item->level1 = 0;
+    item->duration1 = low_ticks;
+}
+
+static inline void led_strip_rmt_bit_1_sk6812(rmt_item32_t* item)
+{
+    led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_1_HIGH_SK6812, LED_STRIP_RMT_TICKS_BIT_1_LOW_SK6812);
+}
+
+static inline void led_strip_rmt_bit_0_sk6812(rmt_item32_t* item)
+{
+    led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_0_HIGH_SK6812, LED_STRIP_RMT_TICKS_BIT_0_LOW_SK6812);
+}
+
+static void led_strip_fill_rmt_items_sk6812(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length)
+{
+    uint32_t rmt_items_index = 0;
+    for (uint32_t led_index = 0; led_index < led_strip_length; led_index++) {
+        struct led_color_t led_color = led_strip_buf[led_index];
+
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.green >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_sk6812(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_sk6812(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.red >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_sk6812(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_sk6812(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.blue >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_sk6812(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_sk6812(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+    }
+}
+
+static inline void led_strip_rmt_bit_1_ws2812(rmt_item32_t* item)
+{
+    led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_1_HIGH_WS2812, LED_STRIP_RMT_TICKS_BIT_1_LOW_WS2812);
+}
+
+static inline void led_strip_rmt_bit_0_ws2812(rmt_item32_t* item)
+{
+    led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_0_HIGH_WS2812, LED_STRIP_RMT_TICKS_BIT_0_LOW_WS2812);
+}
+
+static void led_strip_fill_rmt_items_ws2812(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length)
+{
+    uint32_t rmt_items_index = 0;
+    for (uint32_t led_index = 0; led_index < led_strip_length; led_index++) {
+        struct led_color_t led_color = led_strip_buf[led_index];
+
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.green >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_ws2812(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_ws2812(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.red >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_ws2812(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_ws2812(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.blue >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_ws2812(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_ws2812(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+    }
+}
+
+static inline void led_strip_rmt_bit_1_apa106(rmt_item32_t* item)
+{
+    led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_1_HIGH_APA106, LED_STRIP_RMT_TICKS_BIT_1_LOW_APA106);
+}
+
+static inline void led_strip_rmt_bit_0_apa106(rmt_item32_t* item)
+{
+    led_strip_fill_item_level(item, LED_STRIP_RMT_TICKS_BIT_0_HIGH_APA106, LED_STRIP_RMT_TICKS_BIT_0_LOW_APA106);
+}
+
+static void led_strip_fill_rmt_items_apa106(struct led_color_t *led_strip_buf, rmt_item32_t *rmt_items, uint32_t led_strip_length)
+{
+    uint32_t rmt_items_index = 0;
+    for (uint32_t led_index = 0; led_index < led_strip_length; led_index++) {
+        struct led_color_t led_color = led_strip_buf[led_index];
+
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.red >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_apa106(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_apa106(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.green >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_apa106(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_apa106(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+        for (uint8_t bit = 8; bit != 0; bit--) {
+            uint8_t bit_set = (led_color.blue >> (bit - 1)) & 1;
+            if(bit_set) {
+                led_strip_rmt_bit_1_apa106(&(rmt_items[rmt_items_index]));
+            } else {
+                led_strip_rmt_bit_0_apa106(&(rmt_items[rmt_items_index]));
+            }
+            rmt_items_index++;
+        }
+    }
+}
+
+static void led_strip_task(void *arg)
+{
+    struct led_strip_t *led_strip = (struct led_strip_t *)arg;
+    led_fill_rmt_items_fn led_make_waveform = NULL;
+
+    size_t num_items_malloc = (LED_STRIP_NUM_RMT_ITEMS_PER_LED * led_strip->led_strip_length);
+    rmt_item32_t *rmt_items = (rmt_item32_t*) malloc(sizeof(rmt_item32_t) * num_items_malloc);
+    if (!rmt_items) {
+        vTaskDelete(NULL);
+    }
+
+    switch (led_strip->rgb_led_type) {
+        case RGB_LED_TYPE_WS2812:
+            led_make_waveform = led_strip_fill_rmt_items_ws2812;
+            break;
+
+        case RGB_LED_TYPE_SK6812:
+            led_make_waveform = led_strip_fill_rmt_items_sk6812;
+            break;
+
+        case RGB_LED_TYPE_APA106:
+            led_make_waveform = led_strip_fill_rmt_items_apa106;
+            break;
+
+        default:
+            // Will avoid keeping it point to NULL
+            led_make_waveform = led_strip_fill_rmt_items_ws2812;
+            break;
+    };
+
+    for(;;) {
+        rmt_wait_tx_done(led_strip->rmt_channel, portMAX_DELAY);
+        vTaskDelay(LED_STRIP_REFRESH_PERIOD_MS / portTICK_PERIOD_MS);
+
+        xSemaphoreTake(led_strip->access_semaphore, portMAX_DELAY);
+
+        led_make_waveform(led_strip->led_strip_working,
+                          rmt_items,
+                          led_strip->led_strip_length);
+        rmt_write_items(led_strip->rmt_channel,
+                        rmt_items,
+                        num_items_malloc,
+                        false);
+    }
+
+    if (rmt_items) {
+        free(rmt_items);
+    }
+    vTaskDelete(NULL);
+}
+
+static bool led_strip_init_rmt(struct led_strip_t *led_strip)
+{
+    rmt_config_t rmt_cfg = {
+        .rmt_mode = RMT_MODE_TX,
+        .channel = led_strip->rmt_channel,
+        .clk_div = LED_STRIP_RMT_CLK_DIV,
+        .gpio_num = led_strip->gpio,
+        .mem_block_num = 1,
+        .tx_config = {
+            .loop_en = false,
+            .carrier_freq_hz = 100, // Not used, but has to be set to avoid divide by 0 err
+            .carrier_duty_percent = 50,
+            .carrier_level = RMT_CARRIER_LEVEL_LOW,
+            .carrier_en = false,
+            .idle_level = RMT_IDLE_LEVEL_LOW,
+            .idle_output_en = true,
+        }
+    };
+
+    esp_err_t cfg_ok = rmt_config(&rmt_cfg);
+    if (cfg_ok != ESP_OK) {
+        return false;
+    }
+    esp_err_t install_ok = rmt_driver_install(rmt_cfg.channel, 0, 0);
+    if (install_ok != ESP_OK) {
+        return false;
+    }
+
+    return true;
+}
+
+bool led_strip_init(struct led_strip_t *led_strip)
+{
+    static EXT_RAM_ATTR TaskHandle_t task_created;
+    static DRAM_ATTR StaticTask_t xTaskBuffer __attribute__ ((aligned (4)));
+    static EXT_RAM_ATTR StackType_t xStack[LED_STRIP_TASK_SIZE] __attribute__ ((aligned (4)));
+
+    if ((led_strip == NULL) ||
+        (led_strip->rmt_channel >= RMT_CHANNEL_MAX) ||
+        (led_strip->gpio > GPIO_NUM_33) || 
+        (led_strip->led_strip_working == NULL) ||
+        (led_strip->led_strip_showing == NULL) ||
+        (led_strip->led_strip_length == 0) ||
+        (led_strip->access_semaphore == NULL)) {
+        return false;
+    }
+
+    if(led_strip->led_strip_working == led_strip->led_strip_showing) {
+        return false;
+    }
+
+    memset(led_strip->led_strip_working, 0, sizeof(struct led_color_t) * led_strip->led_strip_length);
+    memset(led_strip->led_strip_showing, 0, sizeof(struct led_color_t) * led_strip->led_strip_length);
+
+    bool init_rmt = led_strip_init_rmt(led_strip);
+    if (!init_rmt) {
+        return false;
+    }
+
+    xSemaphoreGive(led_strip->access_semaphore);
+    task_created = xTaskCreateStatic(led_strip_task,
+                                          "led_strip_task",
+                                          LED_STRIP_TASK_SIZE,
+                                          led_strip,
+                                          LED_STRIP_TASK_PRIORITY,
+                                          xStack, &xTaskBuffer);
+
+    if (!task_created) {
+        return false;
+    }
+
+    return true;
+}
+
+bool led_strip_set_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color)
+{
+    bool set_led_success = true;
+
+    if ((!led_strip) || (!color) || (pixel_num > led_strip->led_strip_length)) {
+        return false;
+    }
+
+    led_strip->led_strip_working[pixel_num] = *color;
+
+    return set_led_success;
+}
+
+bool led_strip_set_pixel_rgb(struct led_strip_t *led_strip, uint32_t pixel_num, uint8_t red, uint8_t green, uint8_t blue)
+{
+    bool set_led_success = true;
+
+    if ((!led_strip) || (pixel_num > led_strip->led_strip_length)) {
+        return false;
+    }
+
+    led_strip->led_strip_working[pixel_num].red   = red;
+    led_strip->led_strip_working[pixel_num].green = green;
+    led_strip->led_strip_working[pixel_num].blue  = blue;
+
+    return set_led_success;
+}
+
+bool led_strip_get_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color)
+{
+    bool get_success = true;
+
+    if ((!led_strip) ||
+        (pixel_num > led_strip->led_strip_length) ||
+        (!color)) {
+        color = NULL;
+        return false;
+    }
+
+    *color = led_strip->led_strip_working[pixel_num];
+
+    return get_success;
+}
+
+/**
+ * Updates the led buffer to be shown
+ */
+bool led_strip_show(struct led_strip_t *led_strip)
+{
+    bool success = true;
+
+    if (!led_strip) {
+        return false;
+    }
+    /* copy the current buffer for display */
+    memcpy(led_strip->led_strip_showing,led_strip->led_strip_working, sizeof(struct led_color_t) * led_strip->led_strip_length);
+
+    xSemaphoreGive(led_strip->access_semaphore);
+
+    return success;
+}
+
+/**
+ * Clears the LED strip
+ */
+bool led_strip_clear(struct led_strip_t *led_strip)
+{
+    bool success = true;
+    if (!led_strip) {
+        return false;
+    }
+
+    memset(led_strip->led_strip_working,
+           0,
+           sizeof(struct led_color_t) * led_strip->led_strip_length);
+
+    return success;
+}

+ 95 - 0
components/led_strip/led_strip.h

@@ -0,0 +1,95 @@
+/*  ---------------------------------------------------------------------------
+    File: led_strip.h
+    Author(s):  Lucas Bruder <LBruder@me.com>
+    Date Created: 11/23/2016
+    Last modified: 11/26/2016
+
+    Description: 
+    This library can drive led strips through the RMT module on the ESP32.
+    ------------------------------------------------------------------------ */
+
+#ifndef LED_STRIP_H
+#define LED_STRIP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <driver/rmt.h>
+#include <driver/gpio.h>
+#include "freertos/semphr.h"
+#include <stddef.h>
+
+enum rgb_led_type_t {
+    RGB_LED_TYPE_WS2812 = 0,
+    RGB_LED_TYPE_SK6812 = 1,
+    RGB_LED_TYPE_APA106 = 2,
+
+    RGB_LED_TYPE_MAX,
+};
+
+/**
+ * RGB LED colors
+ */
+struct led_color_t {
+    uint8_t red;
+    uint8_t green;
+    uint8_t blue;
+};
+
+struct led_strip_t {
+    const enum rgb_led_type_t rgb_led_type;
+    uint32_t led_strip_length;
+
+    // RMT peripheral settings
+    rmt_channel_t rmt_channel;
+
+    /*
+     * Interrupt table is located in soc.h
+     * As of 11/27/16, reccomended interrupts are:
+     * 9, 12, 13, 17, 18, 19, 20, 21 or 23
+     * Ensure that the same interrupt number isn't used twice
+     * across all libraries
+     */
+    int rmt_interrupt_num;
+
+    gpio_num_t gpio; // Must be less than GPIO_NUM_33
+
+    struct led_color_t *led_strip_working;
+    struct led_color_t *led_strip_showing;
+
+    SemaphoreHandle_t access_semaphore;
+};
+
+bool led_strip_init(struct led_strip_t *led_strip);
+
+/**
+ * Sets the pixel at pixel_num to color.
+ */
+bool led_strip_set_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color);
+bool led_strip_set_pixel_rgb(struct led_strip_t *led_strip, uint32_t pixel_num, uint8_t red, uint8_t green, uint8_t blue);
+/**
+ * Get the pixel color at pixel_num for the led strip that is currently being shown! 
+ * NOTE: If you call set_pixel_color then get_pixel_color for the same pixel_num, you will not 
+ * get back the same pixel value. This gets you the color of the pixel currently being shown, not the one
+ * being updated
+ *
+ * If there is an invalid argument, color will point to NULL and this function will return false.
+ */
+bool led_strip_get_pixel_color(struct led_strip_t *led_strip, uint32_t pixel_num, struct led_color_t *color);
+
+/**
+ * Updates the led buffer to be shown using double buffering.
+ */
+bool led_strip_show(struct led_strip_t *led_strip);
+
+/**
+ * Clears the LED strip.
+ */
+bool led_strip_clear(struct led_strip_t *led_strip);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // LED_STRIP_H

+ 365 - 0
components/led_strip/led_vu.c

@@ -0,0 +1,365 @@
+/* 
+ *  Control of LED strip within squeezelite-esp32 
+ *     
+ *  (c) Wizmo 2021
+ *
+ *  Loosely based on code by 
+ *     Chuck Rohs 2020, chuck@zethus.ca
+ * 
+ *  This software is released under the MIT License.
+ *  https://opensource.org/licenses/MIT
+ *
+ * ToDo:
+ * Driver does support other led device. Maybe look at supporting in future. 
+ * The VU refresh rate has been decreaced (100->75) to optimize animation of spin dial.  Could make
+ *   configurable like text scrolling (or use the same value) 
+ * Look at reserving a status led within the effects.  (may require nvs setting for center or end position)
+ * Artwork function, but not released as very buggy and not really practical
+ */
+
+#include <ctype.h>
+#include <math.h>
+#include "esp_log.h"
+
+#include "led_strip.h"
+#include "platform_config.h"
+#include "led_vu.h"
+
+static const char *TAG = "led_vu";
+
+#define LED_VU_STACK_SIZE 	(3*1024)
+#define LED_VU_RMT_INTR_NUM 20U
+
+#define LED_VU_PEAK_HOLD 6U
+
+#define LED_VU_DEFAULT_GPIO 22
+#define LED_VU_DEFAULT_LENGTH 19
+#define LED_VU_MAX_LENGTH 255
+
+#define max(a,b) (((a) > (b)) ? (a) : (b))
+
+struct led_strip_t* led_display = NULL;
+static struct led_strip_t  led_strip_config = {
+    .rgb_led_type      = RGB_LED_TYPE_WS2812,
+    .rmt_channel       = RMT_CHANNEL_1,
+    .rmt_interrupt_num = LED_VU_RMT_INTR_NUM,
+    .gpio              = GPIO_NUM_22,
+};
+
+static struct {
+    int gpio;
+    int length;
+    int vu_length;
+    int vu_start_l;
+    int vu_start_r;
+    int vu_odd;
+} strip;
+
+static int led_addr(int pos ) {
+    if (pos < 0) return pos + strip.length;
+    if (pos >= strip.length) return pos - strip.length;
+    return pos;
+}
+
+/****************************************************************************************
+ * Initialize the led vu strip if configured.
+ * 
+ */
+void led_vu_init()
+{
+    char* p;
+    char* config = config_alloc_get_str("led_vu_config", NULL, "N/A");
+
+    // Initialize led VU strip 
+    char* drivername = strcasestr(config, "WS2812");
+
+    if ((p = strcasestr(config, "length")) != NULL) {
+        strip.length = atoi(strchr(p, '=') + 1);
+    } // else 0
+    if ((p = strcasestr(config, "gpio")) != NULL) {
+        strip.gpio = atoi(strchr(p, '=') + 1);
+    } else {
+        strip.gpio = LED_VU_DEFAULT_GPIO;
+    }
+    // check for valid configuration
+    if (!drivername || !strip.gpio) {
+        ESP_LOGI(TAG, "led_vu configuration invalid");
+        goto done;
+    }
+   
+    if (strip.length > LED_VU_MAX_LENGTH) strip.length = LED_VU_MAX_LENGTH;
+    // initialize vu settings
+    //strip.vu_length = (strip.length % 2) ? strip.length / 2 : (strip.length  - 1) / 2;
+    strip.vu_length = (strip.length  - 1) / 2;
+    strip.vu_start_l  = strip.vu_length;
+    strip.vu_start_r = strip.vu_start_l + 1;
+    strip.vu_odd = strip.length - 1;
+
+    // create driver configuration
+    led_strip_config.access_semaphore = xSemaphoreCreateBinary();
+    led_strip_config.led_strip_length = strip.length;
+    led_strip_config.led_strip_working = heap_caps_malloc(strip.length * sizeof(struct led_color_t), MALLOC_CAP_8BIT);
+    led_strip_config.led_strip_showing = heap_caps_malloc(strip.length * sizeof(struct led_color_t), MALLOC_CAP_8BIT);
+    led_strip_config.gpio = strip.gpio;
+
+    // initialize driver 
+    bool led_init_ok = led_strip_init(&led_strip_config);
+    if (led_init_ok) {
+        led_display = &led_strip_config;
+        ESP_LOGI(TAG, "led_vu using gpio:%d length:%d", strip.gpio, strip.length);
+    } else {
+        ESP_LOGE(TAG, "led_vu init failed");
+        goto done;
+    }
+
+    // reserver max memory for remote management systems
+    rmt_set_mem_block_num(RMT_CHANNEL_1, 7);
+
+    led_vu_clear(led_display);
+
+    done:
+        free(config);
+        return;
+    }
+
+inline bool inRange(double x, double y, double z) {
+    return (x > y && x < z);
+}
+
+/****************************************************************************************
+ * Returns the led strip length
+ */
+uint16_t led_vu_string_length() {
+    if (!led_display) return 0;
+    return (uint16_t)strip.length;
+}
+
+/****************************************************************************************
+ * Turns all LEDs off (Black)
+ */
+void led_vu_clear() {
+    if (!led_display) return;
+    led_strip_clear(led_display);
+
+    led_strip_show(led_display);
+}
+
+/****************************************************************************************
+ * Sets all LEDs to one color
+ * r = red (0-255), g = green (0-255), b - blue (0-255)
+ *      note - all colors are adjusted for brightness
+ */
+void led_vu_color_all(uint8_t r, uint8_t g, uint8_t b) {
+    if (!led_display) return;
+
+    struct led_color_t color_on = {.red = r, .green = g, .blue = b}; 
+
+    for (int i = 0 ; i < strip.length ; i ++){
+        led_strip_set_pixel_color(led_display, i, &color_on);
+    }
+
+    led_strip_show(led_display);
+}
+
+/****************************************************************************************
+ * Sets LEDs based on a data packet consiting of rgb data
+ * offset - starting LED,
+ * length - number of leds (3x rgb bytes) 
+ * data - array of rgb values in multiples of 3 bytes
+ */
+void led_vu_data(uint8_t* data, uint16_t offset, uint16_t length) {
+    if (!led_display) return;
+
+	uint8_t* p = (uint8_t*) data;									        
+	for (int i = 0; i < length; i++) {					            
+		led_strip_set_pixel_rgb(led_display, i+offset, *p, *(p+1), *(p+2));
+        p+=3;
+	} 
+
+    led_strip_show(led_display);
+}
+
+/****************************************************************************************
+ * Progress bar display
+ * data - array of gain values(0-100)
+ * offset - starting position
+ * length - size of array
+ */
+void led_vu_spectrum(uint8_t* data, int bright, int length, int style) {
+    if (!led_display) return;
+    uint8_t gain,r,g,b;
+    int width = strip.length / length;
+    int pos = 0;
+    uint8_t* p = (uint8_t*) data;									        
+	for (int i=0; i<length; i++) {
+		gain = *p;
+        r = gain*gain/bright;
+        if (!style) {
+            g = 0;
+            b = gain;
+        } else {
+            g = r;
+            r = 0;
+            b = gain * (bright-gain)/bright;
+        }
+        for (int j=0; j<width; j++) {
+            led_strip_set_pixel_rgb(led_display, pos, r, g, b);
+            pos++;
+        }
+        p++;
+    }
+    
+    led_strip_show(led_display);
+ }
+
+/****************************************************************************************
+ * Progress bar display
+ * pct - percentage complete (0-100)
+ */
+void led_vu_progress_bar(int pct, int bright) {
+    if (!led_display) return;
+
+    // define colors
+    struct led_color_t color_on   = {.red = bright, .green = 0, .blue = 0};
+    struct led_color_t color_off = {.red = 0, .green = bright, .blue = 0};
+
+    // calcuate led position
+    int led_lit = strip.length * pct / 100;
+
+    // set colors
+    for (int i = 0; i < strip.length; i++) {
+        led_strip_set_pixel_color(led_display, i, (i < led_lit) ? &color_off : &color_on);
+    }
+
+    led_strip_show(led_display);
+}
+
+/****************************************************************************************
+ * Spin dial display
+ * gain - brightness (0-100), rate - color change speed (0-100) 
+ * comet - alternate display mode
+ */
+void led_vu_spin_dial(int gain, int rate, int speed, bool comet) 
+{
+    if (!led_display) return;
+
+    static int led_pos = 0;
+    static uint8_t r = 0;
+    static uint8_t g = 0;
+    static uint8_t b = 0;
+    
+    // calculate next color
+    uint8_t step = rate / 2; // controls color change speed
+    if (r == 0 && g == 0 && b == 0) {
+        r = LED_VU_MAX; g = step; 
+    } else if (b == 0) {
+        g = (g > LED_VU_MAX-step) ? LED_VU_MAX : g + step;
+        r = (r < step) ? 0 : r - step;
+        if (r == 0) b = step;
+    } else if (r == 0) {
+        b = (b > LED_VU_MAX-step) ? LED_VU_MAX : b + step;
+        g = (g < step) ? 0 : g- step;
+        if (g == 0) r = step;
+    } else { 
+        r = (r > LED_VU_MAX-step) ? LED_VU_MAX : r + step;
+        b = (b < step) ? 0 : b - step;
+        if (r == 0) b = step;
+    }
+
+    uint8_t rp = r * gain / LED_VU_MAX; 
+    uint8_t gp = g * gain / LED_VU_MAX; 
+    uint8_t bp = b * gain / LED_VU_MAX; 
+
+    // set led color
+    speed++;
+    if (comet) {
+        led_strip_clear(led_display);
+        led_strip_set_pixel_rgb(led_display, led_addr(led_pos-1), rp/2, gp/2, bp/2);
+        led_strip_set_pixel_rgb(led_display, led_addr(led_pos-2), rp/4, gp/4, bp/4);
+        led_strip_set_pixel_rgb(led_display, led_addr(led_pos-3), rp/8, gp/8, bp/8);
+        //led_strip_set_pixel_rgb(led_display, led_addr(led_pos-4), 0, 0, 0);
+    }
+    for (int i = 0; i < speed; i++) {
+        led_strip_set_pixel_rgb(led_display, led_pos, rp, gp, bp);
+        led_pos = led_addr(++led_pos);
+    }
+    
+    led_strip_show(led_display);
+}
+
+/****************************************************************************************
+ * VU meter display
+ * vu_l - left response (0-100), vu_r - right response (0-100)
+ * comet - alternate display mode
+ */
+void led_vu_display(int vu_l, int vu_r, int bright, bool comet) {
+    static int peak_l = 0;
+    static int peak_r = 0;
+    static int decay_l = 0;
+    static int decay_r = 0;
+    if (!led_display) return;
+
+
+
+    // scale vu samples to length
+    vu_l  = vu_l * strip.vu_length / bright;
+    vu_r = vu_r * strip.vu_length / bright;
+
+    // calculate hold peaks
+    if (peak_l > vu_l) {
+        if (decay_l-- < 0) {
+            decay_l = LED_VU_PEAK_HOLD;
+            peak_l--;
+        }
+    } else {
+        peak_l = vu_l;
+        decay_l = LED_VU_PEAK_HOLD;
+    }
+    if (peak_r > vu_r) {
+        if (decay_r-- < 0) {
+            decay_r = LED_VU_PEAK_HOLD;
+            peak_r--;
+        }
+    } else {
+        peak_r = vu_r;
+        decay_r = LED_VU_PEAK_HOLD;
+    }
+
+    // turn off all leds
+    led_strip_clear(led_display);
+
+    // set the led bar values
+    uint8_t step = bright / (strip.vu_length-1);
+    if (step < 1) step = 1; // dor low brightness or larger strips     
+    uint8_t g = bright * 2 / 3; // more red at top
+    uint8_t r = 0;
+    int shift = 0;
+    for (int i = 0; i < strip.vu_length; i++) {
+        // set left
+        if (i == peak_l) {
+            led_strip_set_pixel_rgb(led_display, strip.vu_start_l - i, r, g, bright);
+        } else if (i <= vu_l) {
+            shift = vu_l - i; 
+            if (comet)
+                led_strip_set_pixel_rgb(led_display, strip.vu_start_l - i, r>>shift, g>>shift, 0);
+            else
+                led_strip_set_pixel_rgb(led_display, strip.vu_start_l - i, r, g, 0);
+        }
+        // set right  
+        if (i == peak_r) {
+            led_strip_set_pixel_rgb(led_display, strip.vu_start_r + i, r, g, bright);
+        }  else if (i <= vu_r) {
+            shift = vu_r - i;
+            if (comet)
+                led_strip_set_pixel_rgb(led_display, strip.vu_start_r + i, r>>shift, g>>shift, 0);
+            else
+                led_strip_set_pixel_rgb(led_display, strip.vu_start_r + i, r, g, 0);
+        }
+        // adjust colors (with limit checks)
+        r = (r > bright-step) ? bright : r + step;
+        g = (g < step) ? 0 : g - step;
+    }
+
+    led_strip_show(led_display);
+}
+

+ 31 - 0
components/led_strip/led_vu.h

@@ -0,0 +1,31 @@
+/* 
+ *  Control of LED strip within squeezelite-esp32 
+ *     
+ *  (c) Wizmo 2021
+ * 
+ *  This software is released under the MIT License.
+ *  https://opensource.org/licenses/MIT
+ *
+ */
+
+#include <ctype.h>
+
+#define LED_VU_MAX    255U
+#define LED_VU_BRIGHT  20U
+
+#define led_vu_color_red(B)    led_vu_color_all(B, 0, 0)
+#define led_vu_color_green(B)    led_vu_color_all(0, B, 0)
+#define led_vu_color_blue(B)    led_vu_color_all(0, 0, B)
+#define led_vu_color_yellow(B)    led_vu_color_all(B/2, B/2, 0)
+
+extern struct led_strip_t* led_display;
+
+uint16_t led_vu_string_length();
+void led_vu_progress_bar(int pct, int bright);
+void led_vu_display(int vu_l, int vu_r, int bright, bool comet);
+void led_vu_spin_dial(int gain, int rate, int speed, bool comet);
+void led_vu_spectrum(uint8_t* data, int bright, int length, int style);
+void led_vu_color_all(uint8_t r, uint8_t g, uint8_t b);
+void led_vu_data(uint8_t* data, uint16_t offset, uint16_t length);
+void led_vu_clear();
+

+ 94 - 0
components/platform_console/cmd_config.c

@@ -29,6 +29,7 @@ const char * desc_spdif= "SPDIF Options";
 const char * desc_audio= "General Audio Options";
 const char * desc_bt_source= "Bluetooth Audio Output Options";
 const char * desc_rotary= "Rotary Control";
+const char * desc_ledvu= "Led Strip Options";
 
 extern const struct adac_s *dac_set[];
 
@@ -108,6 +109,15 @@ static struct {
 	struct arg_end * end;
 } rotary_args;
 //config_rotary_get
+
+static struct {
+	struct arg_str * type;
+	struct arg_int * length;
+	struct arg_int * gpio;
+	struct arg_lit * clear;
+	struct arg_end * end;
+} ledvu_args;
+
 static struct{
 		struct arg_str *sink_name;
 		struct arg_str *pin_code;
@@ -642,6 +652,53 @@ static int do_cspot_config(int argc, char **argv){
 	return nerrors;
 }
 #endif
+
+static int do_ledvu_cmd(int argc, char **argv){
+	ledvu_struct_t ledvu={  .type = "WS2812", .gpio = -1, .length = 0};
+	esp_err_t err=ESP_OK;
+	int nerrors = arg_parse(argc, argv,(void **)&ledvu_args);
+	if (ledvu_args.clear->count) {
+		cmd_send_messaging(argv[0],MESSAGING_WARNING,"ledvu config cleared\n");
+		config_set_value(NVS_TYPE_STR, "led_vu_config", "");
+		return 0;
+	}
+
+	char *buf = NULL;
+	size_t buf_size = 0;
+	FILE *f = open_memstream(&buf, &buf_size);
+	if (f == NULL) {
+		cmd_send_messaging(argv[0],MESSAGING_ERROR,"Unable to open memory stream.\n");
+		return 1;
+	}
+	if(nerrors >0){
+		arg_print_errors(f,ledvu_args.end,desc_ledvu);
+		return 1;
+	}
+
+	nerrors+=is_output_gpio(ledvu_args.gpio, f, &ledvu.gpio, true);
+	
+	if(ledvu_args.length->count==0 || ledvu_args.length->ival[0]<1 || ledvu_args.length->ival[0]>255){
+		fprintf(f,"error: strip length must be greater than 0 and no more than 255\n");
+		nerrors++;
+	}
+	else {
+		ledvu.length = ledvu_args.length->count>0?ledvu_args.length->ival[0]:0;
+	}
+	
+	if(!nerrors ){
+		fprintf(f,"Storing ledvu parameters.\n");
+		nerrors+=(config_ledvu_set(&ledvu )!=ESP_OK);
+	}
+	if(!nerrors ){
+		fprintf(f,"Done.\n");
+	}
+	fflush (f);
+	cmd_send_messaging(argv[0],nerrors>0?MESSAGING_ERROR:MESSAGING_INFO,"%s", buf);
+	fclose(f);
+	FREE_AND_NULL(buf);
+	return (nerrors==0 && err==ESP_OK)?0:1;
+}
+
 static int do_i2s_cmd(int argc, char **argv)
 {
 	i2s_platform_config_t i2s_dac_pin = {
@@ -848,6 +905,24 @@ cJSON * rotary_cb(){
 	}
 	return values;
 }
+
+cJSON * ledvu_cb(){
+	cJSON * values = cJSON_CreateObject();
+	const ledvu_struct_t *ledvu= config_ledvu_get();
+	
+	if(GPIO_IS_VALID_GPIO(ledvu->gpio) && ledvu->gpio>=0 && ledvu->length > 0){
+		cJSON_AddNumberToObject(values,"gpio",ledvu->gpio);
+		cJSON_AddNumberToObject(values,"length",ledvu->length);
+	}
+	if(strlen(ledvu->type)>0){
+		cJSON_AddStringToObject(values,"type",ledvu->type);
+	}
+	else {
+		cJSON_AddStringToObject(values,"type","WS2812");
+	}
+	return values;
+}
+
 cJSON * audio_cb(){
 	cJSON * values = cJSON_CreateObject();
 	char * 	p = config_alloc_get_default(NVS_TYPE_STR, "jack_mutes_amp", "n", 0);
@@ -1259,6 +1334,24 @@ static void register_rotary_config(void){
     ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
 }
 
+static void register_ledvu_config(void){
+	ledvu_args.type = arg_str1(NULL,"type","<none>|WS2812","Led type (supports one rgb strip to display built in effects and allow remote control through 'dmx' messaging)");
+	ledvu_args.length = arg_int1(NULL,"length","<1..255>","Strip length (1-255 supported)");
+	ledvu_args.gpio = arg_int1(NULL,"gpio","gpio","Data pin");
+	ledvu_args.clear = arg_lit0(NULL, "clear", "Clear configuration");
+	ledvu_args.end = arg_end(4);
+
+	const esp_console_cmd_t cmd = {
+        .command = CFG_TYPE_HW("ledvu"),
+        .help = desc_ledvu,
+        .hint = NULL,
+        .func = &do_ledvu_cmd,
+        .argtable = &ledvu_args
+    };
+	cmd_to_json_with_cb(&cmd,&ledvu_cb);
+    ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
+}
+
 static void register_audio_config(void){
 	audio_args.jack_behavior = arg_str0("j", "jack_behavior","Headphones|Subwoofer","On supported DAC, determines the audio jack behavior. Selecting headphones will cause the external amp to be muted on insert, while selecting Subwoofer will keep the amp active all the time.");
     audio_args.end = arg_end(6);
@@ -1350,5 +1443,6 @@ void register_config_cmd(void){
 		register_spdif_config();
 	}
 	register_rotary_config();
+	register_ledvu_config();
 }
 

+ 52 - 0
components/services/accessors.c

@@ -319,6 +319,28 @@ esp_err_t config_rotary_set(rotary_struct_t * config){
 	return err;	
 }
 
+/****************************************************************************************
+ * 
+ */
+esp_err_t config_ledvu_set(ledvu_struct_t * config){
+	int buffer_size=512;
+	esp_err_t err=ESP_OK;
+	char * config_buffer=malloc_init_external(buffer_size);
+	if(config_buffer)  {
+		snprintf(config_buffer,buffer_size,"%s,length=%i,gpio=%i",config->type, config->length, config->gpio);
+		log_send_messaging(MESSAGING_INFO,"Updating ledvu configuration to %s",config_buffer);
+		err = config_set_value(NVS_TYPE_STR, "led_vu_config", config_buffer);
+		if(err!=ESP_OK){
+			log_send_messaging(MESSAGING_ERROR,"Error: %s",esp_err_to_name(err));
+		}
+	} 
+	else {
+		err = ESP_ERR_NO_MEM;
+	}
+	FREE_AND_NULL(config_buffer);
+	return err;	
+}
+
 /****************************************************************************************
  * 
  */
@@ -732,6 +754,24 @@ const rotary_struct_t * config_rotary_get() {
 	return &rotary;
 }
 
+/****************************************************************************************
+ * 
+ */
+const ledvu_struct_t * config_ledvu_get() {
+
+	static ledvu_struct_t ledvu={  .type = "WS2812", .gpio = -1, .length = 0};
+	char *config = config_alloc_get_default(NVS_TYPE_STR, "led_vu_config", NULL, 0);
+	if (config && *config) {
+		char *p;
+	
+		// ToDo:  Add code for future support of alternate led types
+		if ((p = strcasestr(config, "gpio")) != NULL) ledvu.gpio = atoi(strchr(p, '=') + 1);
+		if ((p = strcasestr(config, "length")) != NULL) ledvu.length = atoi(strchr(p, '=') + 1);
+		free(config);
+	}
+	return &ledvu;
+}
+
 /****************************************************************************************
  *
  */
@@ -935,6 +975,17 @@ cJSON * get_Rotary_GPIO(cJSON * list){
 	return llist;
 }
 
+/****************************************************************************************
+ *
+ */
+cJSON * get_ledvu_GPIO(cJSON * list){
+	cJSON * llist = list?list:cJSON_CreateArray();
+
+	const ledvu_struct_t *ledvu= config_ledvu_get();
+	add_gpio_for_value(llist,"gpio",ledvu->gpio, "led_vu", false);
+	return llist;
+}
+
 /****************************************************************************************
  *
  */
@@ -1140,6 +1191,7 @@ cJSON * get_gpio_list(bool refresh) {
 	gpio_list=get_SPI_GPIO(gpio_list);
 	gpio_list=get_I2C_GPIO(gpio_list);
 	gpio_list=get_DAC_GPIO(gpio_list);
+	gpio_list=get_ledvu_GPIO(gpio_list);
 	gpio_list=get_psram_gpio_list(gpio_list);
 	gpio_list=get_eth_GPIO(gpio_list);
 	return gpio_list;

+ 9 - 1
components/services/accessors.h

@@ -85,6 +85,12 @@ typedef struct {
 	int timer;
 } rotary_struct_t;
 
+typedef struct {
+	char type[16];
+	int length;
+	int gpio;
+} ledvu_struct_t;
+
 typedef struct {
 	bool fixed;
 	char * name;
@@ -115,4 +121,6 @@ cJSON * 					get_gpio_list(bool refresh);
 bool 						is_dac_config_locked();
 bool 						are_statistics_enabled();
 const rotary_struct_t * 	config_rotary_get();
-esp_err_t 					config_rotary_set(rotary_struct_t * rotary);
+esp_err_t 					config_rotary_set(rotary_struct_t * rotary);
+const ledvu_struct_t * 		config_ledvu_get();
+esp_err_t 					config_ledvu_set(ledvu_struct_t * rotary);

+ 1 - 1
components/squeezelite-ota/CMakeLists.txt

@@ -1,5 +1,5 @@
 idf_component_register(SRC_DIRS .
 					  INCLUDE_DIRS .
 					  REQUIRES app_update esp_https_ota 
-					  PRIV_REQUIRES  console tools display services platform_config spi_flash vfs console freertos platform_console 
+					  PRIV_REQUIRES  console tools display led_strip services platform_config spi_flash vfs console freertos platform_console 
 					  )

+ 23 - 16
components/squeezelite-ota/squeezelite-ota.c

@@ -35,6 +35,7 @@
 #include "gds.h"
 #include "gds_text.h"
 #include "gds_draw.h"
+#include "led_vu.h"
 #include "platform_esp32.h"
 #include "lwip/sockets.h"
 #include "globdefs.h"
@@ -156,24 +157,26 @@ static progress_t * loc_displayer_get_progress_dft(){
 }
 static void loc_displayer_progressbar(uint8_t pct){
 	static progress_t * progress_coordinates;
-	if(!display){
-		return;
-	}
-	if(!progress_coordinates) progress_coordinates = loc_displayer_get_progress_dft();
-	int filler_x=progress_coordinates->filler.x1+(int)((float)progress_coordinates->filler.width*(float)pct/(float)100);
-
-	ESP_LOGD(TAG,"Drawing %d,%d,%d,%d",progress_coordinates->border.x1,progress_coordinates->border.y1,progress_coordinates->border.x2,progress_coordinates->border.y2);
-	GDS_DrawBox(display,progress_coordinates->border.x1,progress_coordinates->border.y1,progress_coordinates->border.x2,progress_coordinates->border.y2,GDS_COLOR_WHITE,false);
-	ESP_LOGD(TAG,"Drawing %d,%d,%d,%d",progress_coordinates->filler.x1,progress_coordinates->filler.y1,filler_x,progress_coordinates->filler.y2);
-	if(filler_x > progress_coordinates->filler.x1){
-		GDS_DrawBox(display,progress_coordinates->filler.x1,progress_coordinates->filler.y1,filler_x,progress_coordinates->filler.y2,GDS_COLOR_WHITE,true);
+	if(display) {
+		if(!progress_coordinates) progress_coordinates = loc_displayer_get_progress_dft();
+		int filler_x=progress_coordinates->filler.x1+(int)((float)progress_coordinates->filler.width*(float)pct/(float)100);
+
+		ESP_LOGD(TAG,"Drawing %d,%d,%d,%d",progress_coordinates->border.x1,progress_coordinates->border.y1,progress_coordinates->border.x2,progress_coordinates->border.y2);
+		GDS_DrawBox(display,progress_coordinates->border.x1,progress_coordinates->border.y1,progress_coordinates->border.x2,progress_coordinates->border.y2,GDS_COLOR_WHITE,false);
+		ESP_LOGD(TAG,"Drawing %d,%d,%d,%d",progress_coordinates->filler.x1,progress_coordinates->filler.y1,filler_x,progress_coordinates->filler.y2);
+		if(filler_x > progress_coordinates->filler.x1){
+			GDS_DrawBox(display,progress_coordinates->filler.x1,progress_coordinates->filler.y1,filler_x,progress_coordinates->filler.y2,GDS_COLOR_WHITE,true);
+		}
+		else {
+			// Clear the inner box
+			GDS_DrawBox(display,progress_coordinates->filler.x1,progress_coordinates->filler.y1,progress_coordinates->filler.x2,progress_coordinates->filler.y2,GDS_COLOR_BLACK,true);
+		}
+		ESP_LOGD(TAG,"Updating Display");
+		GDS_Update(display);
 	}
-	else {
-		// Clear the inner box
-		GDS_DrawBox(display,progress_coordinates->filler.x1,progress_coordinates->filler.y1,progress_coordinates->filler.x2,progress_coordinates->filler.y2,GDS_COLOR_BLACK,true);
+	if (led_display) {
+		led_vu_progress_bar(pct, LED_VU_BRIGHT);
 	}
-	ESP_LOGD(TAG,"Updating Display");
-	GDS_Update(display);
 }
 void sendMessaging(messaging_types type,const char * fmt, ...){
     va_list args;
@@ -450,6 +453,10 @@ void ota_task_cleanup(const char * message, ...){
 	    va_start(args, message);
 		sendMessaging(MESSAGING_ERROR,message, args);
 	    va_end(args);
+		
+	    if (led_display) led_vu_color_red(LED_VU_BRIGHT);
+	} else {
+	    if (led_display) led_vu_color_green(LED_VU_BRIGHT);
 	}
 	FREE_RESET(ota_status->ota_write_data);
 	FREE_RESET(ota_status->bin_data);

+ 1 - 0
components/squeezelite/CMakeLists.txt

@@ -17,6 +17,7 @@ idf_component_register( SRC_DIRS . external ac101 tas57xx wm8978
 						 			display
 						 			tools
 						 			audio
+									led_strip
 									_override
 						 			${target_requires}                                    
 						EMBED_FILES vu_s.data arrow.data

+ 96 - 26
components/squeezelite/displayer.c

@@ -16,6 +16,7 @@
 #include "gds_text.h"
 #include "gds_draw.h"
 #include "gds_image.h"
+#include "led_vu.h"
 
 #pragma pack(push, 1)
 
@@ -107,13 +108,20 @@ struct visu_packet {
 	};	
 };
 
+struct ledv_packet {
+	char  opcode[4];
+	u8_t which;
+	u8_t style;
+	u8_t bright;
+};
+
 struct ANIC_header {
 	char  opcode[4];
 	u32_t length;
 	u8_t mode;
 };
 
-struct dmxt_packet {
+struct ledd_packet {
 	char  opcode[4];
 	u16_t x;
 	u16_t length;
@@ -180,7 +188,7 @@ static struct {
 	bool enable, full;
 } artwork;
 
-#define MAX_BARS	32
+#define MAX_BARS	48
 #define VISU_ESP32	0x10
 static EXT_RAM_ATTR struct {
 	int bar_gap, bar_width, bar_border;
@@ -199,6 +207,8 @@ static EXT_RAM_ATTR struct {
 	} back;		
 } visu;
 
+static uint8_t* led_data;
+
 static EXT_RAM_ATTR struct {
 	float fft[FFT_LEN*2], samples[FFT_LEN*2], hanning[FFT_LEN];
 	int levels[2];
@@ -206,7 +216,7 @@ static EXT_RAM_ATTR struct {
 
 static EXT_RAM_ATTR struct {
 	int mode;
-	int max;
+	int n, style, max;
 	u16_t config;
 	struct bar_s bars[MAX_BARS] ;
 } led_visu;
@@ -247,11 +257,10 @@ static void grfs_handler(u8_t *data, int len);
 static void grfg_handler(u8_t *data, int len);
 static void grfa_handler(u8_t *data, int len);
 static void visu_handler(u8_t *data, int len);
-static void dmxt_handler(u8_t *data, int len);
+static void ledv_handler(u8_t *data, int len);
+static void ledd_handler(u8_t *data, int len);
 static void displayer_task(void* arg);
 
-void *led_display;
-
 /* scrolling undocumented information
 	grfs	
 		B: screen number
@@ -349,8 +358,8 @@ bool sb_displayer_init(void) {
 	}	
 	
 	if (led_display) {
-		// PLACEHOLDER to init config
-		led_visu.mode = VISU_VUMETER;
+		led_visu.config = led_vu_string_length();
+		led_data = malloc(MAX_BARS);
 	}
 	
 	// inform LMS of our screen/led dimensions
@@ -428,10 +437,11 @@ static void sendSETD(u16_t width, u16_t height, u16_t led_config) {
 	pkt_header.id = 0xfe; // id 0xfe is width S:P:Squeezebox2
 	pkt_header.length = htonl(sizeof(pkt_header) +  6 - 8);
 		
-	LOG_INFO("sending dimension %ux%u", width, height);	
+	LOG_INFO("sending dimension display:%ux%u led_config:%u", width, height, led_config);	
 
 	width = htons(width);
 	height = htons(height);
+	led_config = htons(led_config);
 		
 	LOCK_P;
 	send_packet((uint8_t *) &pkt_header, sizeof(pkt_header));
@@ -481,8 +491,10 @@ static bool handler(u8_t *data, int len){
 		grfa_handler(data, len);		
 	} else if (!strncmp((char*) data, "visu", 4)) {
 		visu_handler(data, len);
-	} else if (!strncmp((char*) data, "dmxt", 4)) {
-		dmxt_handler(data, len);		
+	} else if (!strncmp((char*) data, "ledv", 4)) {
+		ledv_handler(data, len);
+	} else if (!strncmp((char*) data, "ledd", 4)) {
+		ledd_handler(data, len);		
 	} else {
 		res = false;
 	}
@@ -1074,23 +1086,41 @@ static void displayer_update(void) {
 	}	
 	
 	// actualize led_vu
-	if (led_visu.mode) {
-		// PLACEHOLDER to handle led_display. you need potentially scaling of spectrum (X and Y) 
-		// and scaling of levels (Y) and then call the 
+	if (led_display && led_visu.mode) {
+		// run built in visualizer effects
+		if (led_visu.mode == VISU_VUMETER) {
+			vu_scale(led_visu.bars, led_visu.max, meters.levels);
+			led_vu_display(led_visu.bars[0].current, led_visu.bars[1].current, led_visu.max, led_visu.style);
+		} else if (led_visu.mode == VISU_SPECTRUM) { 
+			spectrum_scale(led_visu.n, led_visu.bars, led_visu.max, meters.samples);
+			uint8_t* p = (uint8_t*) led_data;
+			for (int i = 0; i < led_visu.n; i++) {
+				*p = led_visu.bars[i].current;
+				p++;
+			}
+			led_vu_spectrum(led_data, led_visu.max, led_visu.n, led_visu.style);
+		} else if (led_visu.mode == VISU_WAVEFORM) {
+			spectrum_scale(led_visu.n, led_visu.bars, led_visu.max, meters.samples);
+			led_vu_spin_dial(
+				led_visu.bars[led_visu.n-2].current,
+				led_visu.bars[(led_visu.n/2)+1].current * 50 / led_visu.max,
+				led_visu.bars[1].current * 4 / led_visu.max,
+				led_visu.style);
+		} 
 	}
 }
 
 /****************************************************************************************
  * Calculate spectrum spread
  */
-static void spectrum_limits(int min, int n, int pos) {
+static void spectrum_limits(struct bar_s *bars, int min, int n, int pos, float spectrum_scale) {
 	if (n / 2) {
-		int step = ((DISPLAY_BW - min) * visu.spectrum_scale)  / (n/2);
-		visu.bars[pos].limit = min + step;
-		for (int i = 1; i < n/2; i++) visu.bars[pos+i].limit = visu.bars[pos+i-1].limit + step;
-		spectrum_limits(visu.bars[pos + n/2 - 1].limit, n - n/2, pos + n/2);
+		int step = ((DISPLAY_BW - min) * spectrum_scale)  / (n/2);
+		bars[pos].limit = min + step;
+		for (int i = 1; i < n/2; i++) bars[pos+i].limit = bars[pos+i-1].limit + step;
+		spectrum_limits(bars, bars[pos + n/2 - 1].limit, n - n/2, pos + n/2, spectrum_scale);
 	} else {
-		visu.bars[pos].limit = DISPLAY_BW;
+		bars[pos].limit = DISPLAY_BW;
 	}	
 }
 
@@ -1103,7 +1133,7 @@ static void visu_fit(int bars, int width, int height) {
 		visu.n = bars ? bars : MAX_BARS;
 		visu.max = height - 1;
 		if (visu.spectrum_scale <= 0 || visu.spectrum_scale > 0.5) visu.spectrum_scale = 0.5;
-		spectrum_limits(0, visu.n, 0);
+		spectrum_limits(visu.bars, 0, visu.n, 0, visu.spectrum_scale);
 	} else {
 		visu.n = 2;
 		visu.max = (visu.style ? VU_COUNT : height) - 1;
@@ -1236,11 +1266,50 @@ static void visu_handler( u8_t *data, int len) {
 }	
 
 /****************************************************************************************
- * Dmx style packet handler
+ * Led_visu packet handler
+ */
+static void ledv_handler( u8_t *data, int len) {
+	struct ledv_packet *pkt = (struct ledv_packet*) data;
+
+	LOG_DEBUG("led_visu %u with parameters", pkt->which);
+		
+	xSemaphoreTake(displayer.mutex, portMAX_DELAY);
+	led_visu.mode = pkt->which;
+	led_visu.style = pkt->style;
+	led_visu.max = pkt->bright;
+
+	led_vu_clear();
+	if (led_visu.mode) {
+		if (led_visu.mode == VISU_SPECTRUM) {
+			led_visu.n = (led_visu.config < MAX_BARS) ? led_visu.config : MAX_BARS;
+			spectrum_limits(led_visu.bars, 0, led_visu.n, 0, 0.25);
+		} else if (led_visu.mode == VISU_WAVEFORM) {
+			led_visu.n = 6;
+			spectrum_limits(led_visu.bars, 0, led_visu.n, 0, 0.25);
+		} 
+		
+		displayer.wake = 1; // wake up 
+		
+		// reset bars maximum
+		for (int i = led_visu.n; --i >= 0;) led_visu.bars[i].max = 0;
+		
+		LOG_INFO("LED Visualizer mode %u with bars:%u max:%u style:%d", led_visu.mode, led_visu.n, led_visu.max, led_visu.style);
+	} else {
+		LOG_INFO("Stopping led visualizer");
+	}	
+	
+	xSemaphoreGive(displayer.mutex);
+	
+	// resume displayer task
+	vTaskResume(displayer.task);
+}	
+
+/****************************************************************************************
+ * Led_data dmx style packet handler
  * ToDo: make packet match dmx protocol format
  */
-static void dmxt_handler( u8_t *data, int len) {
-	struct dmxt_packet *pkt = (struct dmxt_packet*) data;
+static void ledd_handler( u8_t *data, int len) {
+	struct ledd_packet *pkt = (struct ledd_packet*) data;
 	uint16_t offset = htons(pkt->x);
 	uint16_t length = htons(pkt->length);
 
@@ -1248,8 +1317,9 @@ static void dmxt_handler( u8_t *data, int len) {
 
 	xSemaphoreTake(displayer.mutex, portMAX_DELAY);
 
-	// PLACEHOLDER
-	//led_vu_data(data + sizeof(struct dmxt_packet), offset, length);
+	led_vu_data(data + sizeof(struct ledd_packet), offset, length);
+	
+	displayer.wake = 1000; // wait a little while
 
 	xSemaphoreGive(displayer.mutex);
 }	

+ 1 - 1
main/CMakeLists.txt

@@ -1,4 +1,4 @@
 idf_component_register(SRC_DIRS . 
-					PRIV_REQUIRES _override esp_common wifi-manager pthread squeezelite-ota platform_console telnet display targets
+					PRIV_REQUIRES _override esp_common wifi-manager pthread squeezelite-ota platform_console telnet display targets led_strip
 					LDFRAGMENTS "linker.lf"                                            
                     	)

+ 15 - 4
main/esp_app_main.c

@@ -42,6 +42,7 @@
 #include "gds_draw.h"
 #include "gds_text.h"
 #include "gds_font.h"
+#include "led_vu.h"
 #include "display.h"
 #include "accessors.h"
 #include "cmd_system.h"
@@ -73,6 +74,7 @@ static bool bNetworkConnected=false;
 // as an exception _init function don't need include
 extern void services_init(void);
 extern void	display_init(char *welcome);
+extern void led_vu_init(void);
 extern void target_init(char *target);
 const char * str_or_unknown(const char * str) { return (str?str:unknown_string_placeholder); }
 const char * str_or_null(const char * str) { return (str?str:null_string_placeholder); }
@@ -285,6 +287,7 @@ void register_default_nvs(){
     register_default_string_val("ethtmout","8");
     register_default_string_val("dhcp_tmout","8");
 	register_default_string_val("target", CONFIG_TARGET);
+    register_default_string_val("led_vu_config", "");
 	wait_for_commit();
 	ESP_LOGD(TAG,"Done setting default values in nvs.");
 }
@@ -380,10 +383,18 @@ void app_main()
 		target_init(target);
 		free(target);
 	}
-	if(is_recovery_running && display){
-		GDS_ClearExt(display, true);
-		GDS_SetFont(display, &Font_line_2 );
-		GDS_TextPos(display, GDS_FONT_DEFAULT, GDS_TEXT_CENTERED, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, "RECOVERY");
+	ESP_LOGI(TAG,"Initializing led_vu");
+	led_vu_init();
+
+	if(is_recovery_running) {
+		if (display) {
+			GDS_ClearExt(display, true);
+			GDS_SetFont(display, &Font_line_2 );
+			GDS_TextPos(display, GDS_FONT_DEFAULT, GDS_TEXT_CENTERED, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, "RECOVERY");
+		}
+		if(led_display) {
+			led_vu_color_yellow(LED_VU_BRIGHT);
+		}
 	}
 
 	ESP_LOGD(TAG,"Getting firmware OTA URL (if any)");

BIN
plugin/SqueezeESP32.zip


+ 28 - 0
plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html

@@ -106,6 +106,34 @@
 		<hr>
 	[% END %]
 
+	[% IF prefs.pref_led_config %]
+		[% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_CONFIG" desc="PLUGIN_SQUEEZEESP32_LED_CONFIG_DESC" %]
+			<!--<input type="text" readonly class="stdedit" name="pref_led_config" id="led_config" value="[% prefs.pref_led_config %]" size="3">-->
+			<input type="hidden" name="pref_led_config" value="[% prefs.pref_led_config %]">
+			[% prefs.pref_led_config %]
+		[% END %]
+		[% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_VISUALIZER" desc="PLUGIN_SQUEEZEESP32_LED_VISUALIZER_DESC" %]
+			<select class="stdedit" name="pref_led_visualizer" id="led_visualizer">
+				[% num = 1 %][% last = 1 %]
+				[% FOREACH option = ledVisualModes.keys.nsort %]
+					<option [% IF prefs.pref_led_visualizer == option %]selected [% ELSIF num == 1 && option == '-1' %]selected [% END %]value="[% option %]">[% ledVisualModes.$option %]</option>
+				[%- END -%]
+			</select>
+		[% END %]
+		[% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS" desc="PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS_DESC" %]
+			<input type="text" class="stdedit sliderInput_0_255" name="pref_led_brightness" id="led_brightness" value="[% prefs.pref_led_brightness %]" size="2">
+		[% END %]
+		
+		[% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_DATA" desc="PLUGIN_SQUEEZEESP32_LED_DATA_DESC" %]
+			<input type="button" name="led_data_send" onclick="SqueezeJS.Controller.request({params: ['[% playerid %]', ['dmx', document.getElementById('led_data_cmd').value, document.getElementById('led_data_x').value]] });" value="[% "PLUGIN_SQUEEZEESP32_LED_DATA_SEND" | string %]">&nbsp;
+			[% "PLUGIN_SQUEEZEESP32_LED_DATA_X" | string %]&nbsp
+			<input type="text" class="stdedit" name="pref_led_data_x" id="led_data_x" value="0" size="2">
+			[% "PLUGIN_SQUEEZEESP32_LED_DATA_CMD" | string %]&nbsp
+			<input type="text" class="stdedit" name="pref_led_data_cmd" id="led_data_cmd" value="80,0,0,0,80,0,0,0,80" size="50">
+		[% END %]
+		<hr>
+	[% END %]
+	
 	[% IF pref_equalizer %]
 		[% WRAPPER setting title="PLUGIN_SQUEEZEESP32_EQUALIZER" desc="" %]
 			<div>[% "PLUGIN_SQUEEZEESP32_EQUALIZER_SAVE" | string %]</div>

+ 15 - 0
plugin/SqueezeESP32/Player.pm

@@ -10,6 +10,7 @@ use Slim::Utils::Log;
 use Slim::Utils::Prefs;
 
 use Plugins::SqueezeESP32::FirmwareHelper;
+use Plugins::SqueezeESP32::RgbLed;
 
 my $sprefs = preferences('server');
 my $prefs = preferences('plugin.squeezeesp32');
@@ -63,6 +64,10 @@ sub maxTreble {	20 }
 sub minTreble {	-13 }
 sub maxBass { 20 }
 sub minBass { -13 }
+sub hasLED { 
+	my $client = shift;
+	return $prefs->client($client)->get('led_config') || 0;
+}
 
 sub init {
 	my $client = shift;
@@ -98,6 +103,7 @@ sub init {
 
 	$client->SUPER::init(@_);
 	Plugins::SqueezeESP32::FirmwareHelper::init($client);
+	Plugins::SqueezeESP32::RgbLed::init($client);
 
 	main::INFOLOG && $log->is_info && $log->info("SqueezeESP player connected: " . $client->id);
 }
@@ -110,6 +116,9 @@ sub initPrefs {
 	$prefs->client($client)->init( {
 		equalizer => [(0) x 10],
 		artwork => undef,
+		led_config => 0,
+		led_visualizer => 0,
+		led_brightness => 20,
 	} );
 
 	$prefs->setValidate({
@@ -141,6 +150,9 @@ sub power {
 		$client->update_artwork(1);
 	} else {
 		$client->clear_artwork(1);
+		if ($client->hasLED) {
+			Plugins::SqueezeESP32::RgbLed::updateLED($client, 0);
+		}
 	}
 
 	return $res;
@@ -169,6 +181,9 @@ sub playerSettingsFrame {
 
 			main::INFOLOG && $log->is_info && $log->info("Setting player $value" . "x" . "$height for ", $client->name);
 		}
+		my $led_config = (unpack('Cnnn',$$data_ref))[3];
+		$prefs->client($client)->set('led_config', $led_config);
+		main::INFOLOG && $log->is_info && $led_config && $log->info("Setting led length $led_config for ", $client->name);
 	}
 
 	$client->SUPER::playerSettingsFrame($data_ref);

+ 12 - 0
plugin/SqueezeESP32/PlayerSettings.pm

@@ -7,6 +7,7 @@ use List::Util qw(first min max);
 
 use Slim::Utils::Log;
 use Slim::Utils::Prefs;
+use Slim::Utils::Strings qw(string cstring);
 
 my $sprefs = preferences('server');
 my $prefs = preferences('plugin.squeezeesp32');
@@ -33,6 +34,7 @@ sub prefs {
 	my ($class, $client) = @_;
 	my @prefs;
 	push @prefs, qw(width small_VU) if $client->displayWidth;
+	push @prefs, qw(led_config led_visualizer led_brightness);# if $client->hasLED;
 	return ($prefs->client($client), @prefs);
 }
 
@@ -86,6 +88,12 @@ sub handler {
 			$cprefs->set('equalizer', $equalizer);
 			$client->update_tones($equalizer);
 		}
+
+		if ($client->hasLED) {
+			$cprefs->set('led_visualizer', $paramRef->{'pref_led_visualizer'} || 0);
+			$cprefs->set('led_brightness', $paramRef->{'pref_led_brightness'} || 20);
+			Plugins::SqueezeESP32::RgbLed::updateLED($client);
+		}
 	}
 
 	if ($client->displayWidth) {
@@ -95,6 +103,10 @@ sub handler {
 		$paramRef->{'pref_artwork'} = $cprefs->get('artwork');
 	}
 
+	if ($client->hasLED) {
+		$paramRef->{'ledVisualModes'} = Plugins::SqueezeESP32::RgbLed::ledVisualModeOptions($client);
+	}
+
 	$paramRef->{'pref_equalizer'} = $cprefs->get('equalizer') if $client->can('depth') &&  $client->depth == 16;
 	$paramRef->{'player_ip'} = $client->ip;
 

+ 189 - 0
plugin/SqueezeESP32/RgbLed.pm

@@ -0,0 +1,189 @@
+package Plugins::SqueezeESP32::RgbLed;
+
+=head1 NAME
+
+Plugins::SqueezeESP32::RgbLed
+
+=head1 DESCRIPTION
+
+L<Plugins::SqueezeESP32::RgbLed>
+
+=cut
+
+use strict;
+use Slim::Utils::Strings qw(string cstring);
+
+use Slim::Utils::Log;
+use Slim::Utils::Prefs;
+use Plugins::SqueezeESP32::Player
+
+my $log = logger('player.RgbLed');
+
+my $prefs = preferences('plugin.squeezeesp32');
+my $log   = logger('plugin.squeezeesp32');
+
+sub init {
+	Slim::Control::Request::subscribe( sub { onNotification(@_) }, [ ['playlist'], ['open', 'pause', 'resume', 'stop', 'clear'] ]);
+
+	# register led visualizer comands to allow independant update and command line controls.
+	Slim::Control::Request::addDispatch([ 'dmx', '_data', '_xoff'], [1, 0, 0, \&sendDMX]);
+	Slim::Control::Request::addDispatch([ 'led_visual', '_mode', '_bright'], [1, 0, 0, \&setLEDVisu]);
+}
+
+my $VISUALIZER_NONE = 0;
+my $VISUALIZER_VUMETER = 1;
+my $VISUALIZER_SPECTRUM_ANALYZER = 2;
+my $VISUALIZER_WAVEFORM = 3;
+my @ledvisualizers = (
+	{ desc => ['BLANK'],
+	  params => [$VISUALIZER_NONE],
+    },
+	{ desc => ['VISUALIZER_ANALOG_VUMETER'],
+	  params => [$VISUALIZER_VUMETER, 0],
+    },
+	{ desc => ['VISUALIZER_DIGITAL_VUMETER'],
+	  params => [$VISUALIZER_VUMETER, 1],
+    },
+	{ desc => ['VISUALIZER_SPECTRUM_ANALYZER'],
+	  params => [$VISUALIZER_SPECTRUM_ANALYZER, 0],
+    },
+	{ desc => ['VISUALIZER_SPECTRUM_ANALYZER','2'],
+	  params => [$VISUALIZER_SPECTRUM_ANALYZER, 1],
+    },
+	{ desc => ['PLUGIN_SQUEEZEESP32_WAVEFORM'],
+	  params => [$VISUALIZER_WAVEFORM, 0],
+    },
+	{ desc => ['PLUGIN_SQUEEZEESP32_WAVEFORM','2'],
+	  params => [$VISUALIZER_WAVEFORM, 1],
+    },
+);
+
+my $nledvisualizers = $#ledvisualizers;
+
+sub ledVisualizerModes {
+	return \@ledvisualizers;
+}
+
+sub ledVisualizerNModes {
+	return $nledvisualizers;
+}
+
+sub updateLED {
+	my $client = shift;
+	my $on = shift || 1;
+	my $cprefs = $prefs->client($client);
+	
+	my $visu = $cprefs->get('led_visualizer') || 0;
+	my $bright = $cprefs->get('led_brightness') || 20;
+	
+	$visu = 0 if ($visu < 0 || $visu > ledVisualizerNModes || !(Slim::Player::Source::playmode($client) eq 'play') || !$on);
+	my $modes  = ledVisualizerModes;
+	my $params = $modes->[$visu]{'params'};
+	my $data = pack('CCC', $params->[0], $params->[1], $bright);
+	main::INFOLOG && $log->is_debug && $log->info("Sending visu mode $visu ", $client->name);
+
+	$client->sendFrame( ledv => \$data );
+}
+
+sub ledVisualParams {
+	my $client = shift;
+	
+	my $visu = $prefs->client($client)->get('led_visualizer') || 0;
+	
+	return $ledvisualizers[$visu]{params};
+}
+
+sub ledVisualModeOptions {
+	my $client = shift;
+
+	my $display = {
+		'-1' => ' '
+	};
+
+	my $modes  = ledVisualizerModes; 
+	my $nmodes = ledVisualizerNModes; 
+
+	for (my $i = 0; $i <= $nmodes; $i++) {
+
+		my $desc = $modes->[$i]{'desc'};
+
+		for (my $j = 0; $j < scalar @$desc; $j++) {
+
+			$display->{$i} .= ' ' if ($j > 0);
+			$display->{$i} .= string(@{$desc}[$j]) || @{$desc}[$j];
+		}
+	}
+
+	return $display;
+}
+
+sub sendDMX {
+	my $request = shift;
+
+	# check this is the correct command.
+	if ($request->isNotCommand([['dmx']])) {
+		$request->setStatusBadDispatch();
+		return;
+	}
+
+	# get our parameters
+	my $client   = $request->client();
+	
+	my $count = 0;
+	my $outData;
+	my @values = split(',', $request->getParam('_data') || '');
+	foreach my $val (@values) {
+		$outData .= pack ( 'C', $val);
+		$count++;
+	}
+	$count /= 3;
+
+	my $data = pack('nn', $request->getParam('_xoff') || 0, $count ) . $outData;
+	
+	# changed from dmxt to ledd (matches 'ledc' for tricolor led in receiver player)
+	$client->sendFrame( ledd => \$data );
+}
+
+sub setLEDVisu {
+	my $request = shift;
+
+	# check this is the correct command.
+	if ($request->isNotCommand([['led_visual']])) {
+		$request->setStatusBadDispatch();
+		return;
+	}
+
+	my $client   = $request->client();
+	return if (!$client->hasLED);
+	
+	my $cprefs = $prefs->client($client);
+	
+	my $visu = $cprefs->get('led_visualizer') || 0;
+	my $mode = $request->getParam('_mode') || -1;
+	if ($mode == -1) {
+		$visu+=1;
+	} else {
+		$visu = $mode;
+	} 
+	$visu = 0 if ($visu < 0 || $visu > ledVisualizerNModes);
+	$cprefs->set('led_visualizer', $visu);
+	
+	my $bright = $request->getParam('_bright') || -1;
+	if ($bright >= 0 && $bright < 256) {
+		$cprefs->set('led_brightness', $bright);
+	}
+	
+	updateLED($client);
+}
+
+sub onNotification {
+	my $request = shift;
+	my $client  = $request->client || return;
+	
+	foreach my $player ($client->syncGroupActiveMembers) {
+		next unless $player->isa('Plugins::SqueezeESP32::Player');
+		updateLED($player) if $player->hasLED;
+	}
+}
+
+1;

+ 38 - 0
plugin/SqueezeESP32/strings.txt

@@ -106,6 +106,44 @@ PLUGIN_SQUEEZEESP32_ARTWORK_X
 PLUGIN_SQUEEZEESP32_ARTWORK_Y
 	EN	Y
 
+PLUGIN_SQUEEZEESP32_LED_CONFIG
+	EN	Led RGB Strip
+
+PLUGIN_SQUEEZEESP32_LED_CONFIG_DESC
+	EN	Length of the Led strip reported by the player
+
+PLUGIN_SQUEEZEESP32_LED_VISUALIZER
+	EN	Led Visualizer
+
+PLUGIN_SQUEEZEESP32_LED_VISUALIZER_DESC
+	EN	Select Led Visualizer from the built in effects
+
+PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS
+	EN	Led Brghtness
+
+PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS_DESC
+	EN	Sets the brightness of the Led Visualizer effects
+
+PLUGIN_SQUEEZEESP32_LED_DATA
+	EN	Led Test
+
+PLUGIN_SQUEEZEESP32_LED_DATA_DESC
+	EN	Sends custom RGB data to the Led Strip.
+	EN	<br>Enter R,G,B values (comma delimited).  Repeat RGB for multiple Led sequences.  Use the Offset to specifiy the first LED of the sequence.
+	EN	<br>Use the Set button the transmit. 
+
+PLUGIN_SQUEEZEESP32_LED_DATA_SEND
+	EN	Set
+
+PLUGIN_SQUEEZEESP32_LED_DATA_X
+	EN	Offset
+
+PLUGIN_SQUEEZEESP32_LED_DATA_CMD
+	EN	Command
+
+PLUGIN_SQUEEZEESP32_WAVEFORM
+	EN	Waveform Visualizer
+
 PLUGIN_SQUEEZEESP32_EQUALIZER
 	DE	Grafischer Equalizer
 	EN	Graphic equalizer