From afd0da16a5835488c745bca2ff65b57e7492b786 Mon Sep 17 00:00:00 2001 From: Wizmo2 Date: Mon, 21 Nov 2022 19:01:19 -0500 Subject: [PATCH] merge changes from led_visu to v4.3 --- components/led_strip/CMakeLists.txt | 11 + components/led_strip/LICENSE | 202 +++++++++ components/led_strip/led_strip.c | 407 ++++++++++++++++++ components/led_strip/led_strip.h | 96 +++++ components/led_strip/led_vu.c | 363 ++++++++++++++++ components/led_strip/led_vu.h | 31 ++ components/platform_console/cmd_config.c | 95 ++++ components/services/accessors.c | 54 +++ components/services/accessors.h | 10 +- components/squeezelite-ota/CMakeLists.txt | 2 +- components/squeezelite-ota/squeezelite-ota.c | 36 +- components/squeezelite/CMakeLists.txt | 1 + components/squeezelite/displayer.c | 116 +++-- main/CMakeLists.txt | 2 +- main/esp_app_main.c | 19 +- plugin/SqueezeESP32.zip | Bin 18239 -> 19514 bytes .../plugins/SqueezeESP32/settings/player.html | 28 ++ plugin/SqueezeESP32/Player.pm | 12 + plugin/SqueezeESP32/PlayerSettings.pm | 12 + plugin/SqueezeESP32/RgbLed.pm | 188 ++++++++ plugin/SqueezeESP32/strings.txt | 38 ++ 21 files changed, 1676 insertions(+), 47 deletions(-) create mode 100644 components/led_strip/CMakeLists.txt create mode 100644 components/led_strip/LICENSE create mode 100644 components/led_strip/led_strip.c create mode 100644 components/led_strip/led_strip.h create mode 100644 components/led_strip/led_vu.c create mode 100644 components/led_strip/led_vu.h create mode 100644 plugin/SqueezeESP32/RgbLed.pm diff --git a/components/led_strip/CMakeLists.txt b/components/led_strip/CMakeLists.txt new file mode 100644 index 00000000..c7e9f3f0 --- /dev/null +++ b/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 +) diff --git a/components/led_strip/LICENSE b/components/led_strip/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/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. diff --git a/components/led_strip/led_strip.c b/components/led_strip/led_strip.c new file mode 100644 index 00000000..92b1ba3d --- /dev/null +++ b/components/led_strip/led_strip.c @@ -0,0 +1,407 @@ +/* ---------------------------------------------------------------------------- + File: led_strip.c + Author(s): Lucas Bruder + 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 + +#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) +{ + TaskHandle_t led_strip_task_handle; + + 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); + BaseType_t task_created = xTaskCreate(led_strip_task, + "led_strip_task", + LED_STRIP_TASK_SIZE, + led_strip, + LED_STRIP_TASK_PRIORITY, + &led_strip_task_handle); + + 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; +} diff --git a/components/led_strip/led_strip.h b/components/led_strip/led_strip.h new file mode 100644 index 00000000..0809a27f --- /dev/null +++ b/components/led_strip/led_strip.h @@ -0,0 +1,96 @@ +/* --------------------------------------------------------------------------- + File: led_strip.h + Author(s): Lucas Bruder + 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 +#include +#include "freertos/FreeRTOS.h" + +#include + +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 diff --git a/components/led_strip/led_vu.c b/components/led_strip/led_vu.c new file mode 100644 index 00000000..49ba26a1 --- /dev/null +++ b/components/led_strip/led_vu.c @@ -0,0 +1,363 @@ +/* + * 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 +#include +#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 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_ + led_strip_set_pixel_rgb(led_display, led_pos, rp, gp, bp); + if (comet) { + 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); + } + + // next led + 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); +} + diff --git a/components/led_strip/led_vu.h b/components/led_strip/led_vu.h new file mode 100644 index 00000000..f6c26f12 --- /dev/null +++ b/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 + +#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, 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(); + diff --git a/components/platform_console/cmd_config.c b/components/platform_console/cmd_config.c index f25bc919..19d78e32 100644 --- a/components/platform_console/cmd_config.c +++ b/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; @@ -635,6 +645,54 @@ static int do_cspot_config(int argc, char **argv){ FREE_AND_NULL(buf); return nerrors; } + + +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 = { @@ -842,6 +900,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); @@ -1252,6 +1328,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","|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); @@ -1340,5 +1434,6 @@ void register_config_cmd(void){ register_spdif_config(); } register_rotary_config(); + register_ledvu_config(); } diff --git a/components/services/accessors.c b/components/services/accessors.c index 8bc4b18d..61113dc0 100644 --- a/components/services/accessors.c +++ b/components/services/accessors.c @@ -316,6 +316,30 @@ 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=calloc(buffer_size,1); + char * config_buffer2=calloc(buffer_size,1); + if(config_buffer && config_buffer2) { + 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); + FREE_AND_NULL(config_buffer2); + return err; +} + /**************************************************************************************** * */ @@ -722,6 +746,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; +} + /**************************************************************************************** * */ @@ -925,6 +967,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; +} + /**************************************************************************************** * */ @@ -1130,6 +1183,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; diff --git a/components/services/accessors.h b/components/services/accessors.h index d7c9c640..959af575 100644 --- a/components/services/accessors.h +++ b/components/services/accessors.h @@ -84,6 +84,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; @@ -114,4 +120,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); \ No newline at end of file +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); \ No newline at end of file diff --git a/components/squeezelite-ota/CMakeLists.txt b/components/squeezelite-ota/CMakeLists.txt index cf75eab9..e7fefbaa 100644 --- a/components/squeezelite-ota/CMakeLists.txt +++ b/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 ) diff --git a/components/squeezelite-ota/squeezelite-ota.c b/components/squeezelite-ota/squeezelite-ota.c index d98f4867..488e3cf0 100644 --- a/components/squeezelite-ota/squeezelite-ota.c +++ b/components/squeezelite-ota/squeezelite-ota.c @@ -157,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); + 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); + 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; @@ -452,6 +454,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); diff --git a/components/squeezelite/CMakeLists.txt b/components/squeezelite/CMakeLists.txt index 5c1f0375..0505c672 100644 --- a/components/squeezelite/CMakeLists.txt +++ b/components/squeezelite/CMakeLists.txt @@ -13,6 +13,7 @@ idf_component_register( SRC_DIRS . external ac101 tas57xx wm8978 display tools audio + led_strip EMBED_FILES vu_s.data arrow.data ) diff --git a/components/squeezelite/displayer.c b/components/squeezelite/displayer.c index 22949bdb..c06ca2c6 100644 --- a/components/squeezelite/displayer.c +++ b/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; @@ -206,7 +214,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 +255,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 +356,7 @@ bool sb_displayer_init(void) { } if (led_display) { - // PLACEHOLDER to init config - led_visu.mode = VISU_VUMETER; + led_visu.config = led_vu_string_length(); } // inform LMS of our screen/led dimensions @@ -428,10 +434,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 +488,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 +1083,40 @@ 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) { + // scale to correct rgb brightness + if (led_visu.mode == VISU_VUMETER) vu_scale(led_visu.bars, led_visu.max, meters.levels); + else spectrum_scale(led_visu.n, led_visu.bars, led_visu.max, meters.samples); + + // run built in visualizer effects + if (led_visu.mode == VISU_VUMETER) { + 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) { + uint8_t* led_data = malloc(led_visu.n); + 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); + free(led_data); + } else if (led_visu.mode == VISU_WAVEFORM) { + led_vu_spin_dial(led_visu.bars[1].current, led_visu.bars[(led_visu.n/2)+1].current * 50 / 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 +1129,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 +1262,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 +1313,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); } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f98926be..80054ede 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ 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 EMBED_FILES ../server_certs/github.pem LDFRAGMENTS "linker.lf" ) diff --git a/main/esp_app_main.c b/main/esp_app_main.c index 0343e332..fa829e02 100644 --- a/main/esp_app_main.c +++ b/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 @@ extern const uint8_t server_cert_pem_end[] asm("_binary_github_pem_end"); // 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); } @@ -368,6 +370,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", ""); #ifdef CONFIG_CSPOT_SINK register_default_string_val("enable_cspot", STR(CONFIG_CSPOT_SINK)); register_default_string_val("cspot_config", ""); @@ -467,10 +470,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); + } } diff --git a/plugin/SqueezeESP32.zip b/plugin/SqueezeESP32.zip index cc52e727d79951d7e032a4513b0cd218de157327..79b7f39b9ef5a73613bdb1920e04f5570d198dd3 100644 GIT binary patch literal 19514 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 literal 18239 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 S2>^ib@j3n2;^F`G?f(O_;I#Ds diff --git a/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html b/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html index f5e683d5..0ddf2fd6 100644 --- a/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html +++ b/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html @@ -106,6 +106,34 @@
[% END %] + [% IF prefs.pref_led_config %] + [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_CONFIG" desc="PLUGIN_SQUEEZEESP32_LED_CONFIG_DESC" %] + + + [% prefs.pref_led_config %] + [% END %] + [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_VISUALIZER" desc="PLUGIN_SQUEEZEESP32_LED_VISUALIZER_DESC" %] + + [% END %] + [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS" desc="PLUGIN_SQUEEZEESP32_LED_BRIGHTNESS_DESC" %] + + [% END %] + + [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_LED_DATA" desc="PLUGIN_SQUEEZEESP32_LED_DATA_DESC" %] +   + [% "PLUGIN_SQUEEZEESP32_LED_DATA_X" | string %]  + + [% "PLUGIN_SQUEEZEESP32_LED_DATA_CMD" | string %]  + + [% END %] +
+ [% END %] + [% IF pref_equalizer %] [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_EQUALIZER" desc="" %]
[% "PLUGIN_SQUEEZEESP32_EQUALIZER_SAVE" | string %]
diff --git a/plugin/SqueezeESP32/Player.pm b/plugin/SqueezeESP32/Player.pm index cbc32d3b..00dd167d 100644 --- a/plugin/SqueezeESP32/Player.pm +++ b/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({ @@ -169,6 +178,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); diff --git a/plugin/SqueezeESP32/PlayerSettings.pm b/plugin/SqueezeESP32/PlayerSettings.pm index 4265f335..3c1298ae 100644 --- a/plugin/SqueezeESP32/PlayerSettings.pm +++ b/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; diff --git a/plugin/SqueezeESP32/RgbLed.pm b/plugin/SqueezeESP32/RgbLed.pm new file mode 100644 index 00000000..99167516 --- /dev/null +++ b/plugin/SqueezeESP32/RgbLed.pm @@ -0,0 +1,188 @@ +package Plugins::SqueezeESP32::RgbLed; + +=head1 NAME + +Plugins::SqueezeESP32::RgbLed + +=head1 DESCRIPTION + +L + +=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 $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')); + 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; diff --git a/plugin/SqueezeESP32/strings.txt b/plugin/SqueezeESP32/strings.txt index bd9daf29..78e3d1b8 100644 --- a/plugin/SqueezeESP32/strings.txt +++ b/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
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
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