├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md └── main ├── component.mk └── main.c /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | build 3 | .idea 4 | sdkconfig 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masoncj/esp32-dimmer/73bab1e40469acc37594aef1a3f39d32a29acfc7/.gitmodules -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 4 | 5 | project(esp_dimmer) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 cmason1978 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ESP32 Light Dimmer 2 | 3 | 4 | An attempt at a multi-channel incandescent light dimmer using the ESP32. Will eventually provide HTML/javascript based UI for dimming lights. 5 | 6 | ### Status 7 | 8 | Still under construction. Able to do fades of up to 4 dimmer channels. 9 | 10 | Experiments with the LEDC peripheral revealed a need to perform dimming manually (instead of with LEDC [fade functionality](http://esp-idf.readthedocs.io/en/latest/api/ledc.html#_CPPv213ledc_set_fade11ledc_mode_t14ledc_channel_t8uint32_t21ledc_duty_direction_t8uint32_t8uint32_t8uint32_t)) due to inability to adjust PWM phase via dimming. So, we implemented the dimming logic inside zero-crossing interrupt routine, but needed to perform all math as fixed-point due to hard faults when doing floating point in ISR. 11 | 12 | Next step is to implement HTML/wifi based control. Looking at possibly using 13 | 14 | 15 | ### Hardware 16 | 17 | * [Sparkfun ESP 32 Thing](https://www.sparkfun.com/products/13907) (although any ESP32 module should work, possibly with changes in [pin configuration](https://cdn.sparkfun.com/assets/learn_tutorials/5/0/7/esp32-thing-graphical-datasheet-v02.png)). 18 | * 2x [4 channel AC light dimmer board](http://www.inmojo.com/store/krida-electronics/item/4-channel-ac-light-dimmer-arduino--v2/) (but any MOSFET-based dimmer board with zero-crossing detection should work). 19 | 20 | ### Building 21 | 22 | Requires [ESP32 IDF](https://github.com/espressif/esp-idf/). 23 | 24 | Currently using v4.4.3 (`git checkout v4.4 --recurse-submodules`) 25 | 26 | ``` 27 | . /path/to/esp/idf/export.sh 28 | idf.py set-target esp32 29 | idf.py menuconfig 30 | idf.py build 31 | idf.py -p /dev/tty.usbserial* flash 32 | ``` 33 | -------------------------------------------------------------------------------- /main/component.mk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masoncj/esp32-dimmer/73bab1e40469acc37594aef1a3f39d32a29acfc7/main/component.mk -------------------------------------------------------------------------------- /main/main.c: -------------------------------------------------------------------------------- 1 | #include "freertos/FreeRTOS.h" 2 | #include "freertos/task.h" 3 | #include "freertos/queue.h" 4 | #include "freertos/portmacro.h" 5 | #include "freertos/xtensa_api.h" 6 | #include "esp_wifi.h" 7 | #include "esp_system.h" 8 | #include "esp_event.h" 9 | #include "esp_log.h" 10 | #include "nvs_flash.h" 11 | #include "driver/gpio.h" 12 | #include "driver/ledc.h" 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // Timing configuration /////////////////////////////////////////////////////////// 20 | 21 | #define TICK_RATE_HZ 100 22 | #define TICK_PERIOD_MS (1000 / TICK_RATE_HZ) 23 | #define HZ 60 24 | #define TESTING_FADE_DURATION_MSECS 5000 25 | #define REPORT_DURATION_MSECS 10000 26 | #define DUTY_BIT_DEPTH 10 27 | 28 | // TRIAC is kept high for TRIAC_GATE_IMPULSE_CYCLES PWM counts before setting low. 29 | #define TRIAC_GATE_IMPULSE_CYCLES 10 30 | 31 | // TRIAC is always set low at least TRIAC_GATE_QUIESCE_CYCLES PWM counts before the next zero crossing. 32 | #define TRIAC_GATE_QUIESCE_CYCLES 50 33 | 34 | // Pin configuration ////////////////////////////////////////////////////////////// 35 | #define LED_PIN GPIO_NUM_5 36 | #define ZERO_CROSSING_PIN GPIO_NUM_4 37 | #define NUM_CHANNELS 4 38 | static const uint8_t channel_pins[NUM_CHANNELS] = { 39 | GPIO_NUM_16, 40 | GPIO_NUM_12, 41 | GPIO_NUM_13, 42 | GPIO_NUM_14, 43 | }; 44 | 45 | // End Configurable /////////////////////////////////////////////////////////////// 46 | 47 | volatile static uint32_t cycle_counter = 0; 48 | 49 | // Fade record: each record represents a dimming interval for one or more channels. 50 | typedef struct { 51 | uint16_t channels; // Bitmask of channels to fade. 52 | uint16_t start_brightness; // 0 to 1 << DUTY_BIT_DEPTH - 1. 53 | uint16_t end_brightness; // 0 to 1 << DUTY_BIT_DEPTH - 1. 54 | uint32_t start_cycle_num; // Value of cycle_counter when fade starts/started. 55 | uint32_t end_cycle_num; // Value of cycle_counter when fade ends/ended. 56 | } fade_t; 57 | 58 | // NUM_FADE_RECORDS Must be power of 2. 59 | #define NUM_FADE_RECORDS 32 60 | 61 | // Circular buffer of fade records: 62 | volatile static fade_t fades[NUM_FADE_RECORDS]; 63 | // See https://www.snellman.net/blog/archive/2016-12-13-ring-buffers/ for explanation of ring buffer mechanics. 64 | volatile static uint32_t start_fade_index = 0; 65 | volatile static uint32_t end_fade_index = 0; 66 | 67 | #define str(x) #x 68 | #define xstr(x) str(x) 69 | #define SHOW(x) ESP_LOGD(LOG_FADE, "%s %i\n", xstr(x), x); 70 | 71 | #define MIN(x, y) ((x > y) ? y : x); 72 | #define MAX(x, y) ((x > y) ? x : y); 73 | 74 | static const char* LOG_STARTUP = "startup"; 75 | static const char* LOG_FADE = "fade"; 76 | static const char* LOG_CYCLES = "cycles"; 77 | 78 | 79 | esp_err_t event_handler(void *ctx, system_event_t *event) { 80 | return ESP_OK; 81 | } 82 | 83 | /** Fade start task and queue are used to ensure serialized access to ring buffer. External tasks push fade 84 | request records onto fade_start_queue and fade_start_task then places them into buffer. */ 85 | xQueueHandle fade_start_queue; 86 | void fade_start_task(void* arg){ 87 | ESP_LOGI(LOG_STARTUP, "Fade start task started.\n"); 88 | while(1) { 89 | fade_t fade; 90 | if (xQueueReceive(fade_start_queue, &fade, portMAX_DELAY)) { 91 | if (end_fade_index - start_fade_index > NUM_FADE_RECORDS) { 92 | ESP_LOGE(LOG_FADE, "Dropping fade at %i.\n", cycle_counter); 93 | continue; 94 | } 95 | fades[end_fade_index & (NUM_FADE_RECORDS - 1)] = (volatile fade_t) fade; 96 | end_fade_index += 1; 97 | ESP_LOGD( 98 | LOG_FADE, 99 | "At %i, enqueued fade from time %i, brightness %i to time %i, brightness %i. %i active fades.\n", 100 | cycle_counter, 101 | fade.start_cycle_num, 102 | fade.start_brightness, 103 | fade.end_cycle_num, 104 | fade.end_brightness, 105 | end_fade_index - start_fade_index 106 | ); 107 | } 108 | } 109 | } 110 | 111 | /** Enqueue a request to fade the given channel over given time period to given final brightness. 112 | * 113 | * @param channel Integer channel number (0 - MAX_CHANNELS-1) 114 | * @param duration_msecs Duration of fade in milliseconds. 115 | * @param end_brightness ending brightness (from 0 to 1 DUTY_BIT_DEPTH -1). Starting brightness is determined 116 | * from either current brightness of channel (if not fading) or ending brightness of most recent fade. 117 | * @return pdPASS if successful, or error from queuing. 118 | */ 119 | BaseType_t set_channel_fade(uint32_t channel, uint32_t duration_msecs, uint16_t end_brightness) { 120 | if (duration_msecs == 0) { 121 | return 0; 122 | } 123 | end_brightness = MIN(end_brightness, (1 << DUTY_BIT_DEPTH) - 1); 124 | 125 | // Look for currently running fades to derive initial brightness and start time. 126 | uint16_t start_brightness; 127 | uint32_t start_cycle = cycle_counter; 128 | bool found = false; 129 | for(uint32_t fade_index = start_fade_index; fade_index < end_fade_index; ++fade_index) { 130 | fade_t fade = (fade_t) fades[fade_index & (NUM_FADE_RECORDS - 1)]; 131 | if ((fade.channels & (1 << channel)) && fade.end_cycle_num >= start_cycle) { 132 | start_brightness = fade.end_brightness; 133 | start_cycle = fade.end_cycle_num; 134 | found = true; 135 | } 136 | } 137 | if (!found) { 138 | // Not currently fading, use current value, but remember to invert it (low hpoint is high brightness). 139 | start_brightness = ((1 << DUTY_BIT_DEPTH) - 1) - LEDC.channel_group[0].channel[channel].hpoint.hpoint; 140 | } 141 | 142 | fade_t fade; 143 | fade.channels = 1 << channel; 144 | fade.start_brightness = start_brightness; 145 | fade.end_brightness = end_brightness; 146 | fade.start_cycle_num = cycle_counter; 147 | fade.end_cycle_num = (uint32_t)(fade.start_cycle_num + (duration_msecs / 1000 * HZ * 2)); 148 | 149 | // Enqueue a task to put the fade on the ring buffer. 150 | BaseType_t ret = xQueueSend( 151 | fade_start_queue, 152 | &fade, 153 | 0 // Don't block. 154 | ); 155 | 156 | if (ret == pdPASS) { 157 | ESP_LOGD( 158 | LOG_FADE, 159 | "At %i, setting channel %i to fade from time %i, brightness %i to time %i, brightness %i.\n", 160 | cycle_counter, 161 | channel, 162 | fade.start_cycle_num, 163 | fade.start_brightness, 164 | fade.end_cycle_num, 165 | fade.end_brightness 166 | ); 167 | } else { 168 | ESP_LOGE(LOG_FADE, "At %i, failed to set channel fade: %i\n", cycle_counter, ret); 169 | } 170 | return ret; 171 | } 172 | 173 | /** Fade end queue and task receive notification when a given fade has completed. 174 | * Currently, for testing, we simply enqueue another request to fade to max or min brightness. 175 | */ 176 | xQueueHandle fade_end_queue; 177 | void fade_end_task(void* arg){ 178 | ESP_LOGI(LOG_STARTUP, "Fade end task started.\n"); 179 | while(1) { 180 | fade_t fade; 181 | if(xQueueReceive(fade_end_queue, &fade, portMAX_DELAY)) { 182 | uint16_t brightness = fade.end_brightness > fade.start_brightness ? 0 : (1 << DUTY_BIT_DEPTH) - 1; 183 | for (uint16_t channel = 0; channel < NUM_CHANNELS; ++channel) { 184 | if (fade.channels & (1 << channel)) { 185 | set_channel_fade(channel, TESTING_FADE_DURATION_MSECS, brightness); 186 | ESP_LOGD(LOG_FADE, "Reset fade for channel %i to %i.\n", channel, brightness); 187 | } 188 | } 189 | } 190 | } 191 | } 192 | 193 | /** Debugging task that reports current cycle counter. */ 194 | void timer_report_task(void* arg) { 195 | ESP_LOGI(LOG_STARTUP, "Timer report task started, %i ticks per.\n", REPORT_DURATION_MSECS / TICK_PERIOD_MS); 196 | while(1) { 197 | vTaskDelay( REPORT_DURATION_MSECS / TICK_PERIOD_MS); 198 | struct timeval tv; 199 | gettimeofday(&tv, NULL); 200 | ESP_LOGD(LOG_CYCLES, "At time %li.%li, cycle count %i with %i active fades.\n", 201 | tv.tv_sec, tv.tv_usec, cycle_counter, end_fade_index - start_fade_index); 202 | } 203 | } 204 | 205 | /** Apply any active fade requests to the LEDC hardware. 206 | * 207 | * @param current_cycle The current cycle counter. 208 | */ 209 | void set_up_fades(uint32_t current_cycle) { 210 | // Apply any active fades. 211 | for (uint32_t fade_index = start_fade_index; fade_index != end_fade_index; ++fade_index) { 212 | fade_t fade = (fade_t) (fades[fade_index & (NUM_FADE_RECORDS - 1)]); 213 | if (fade.end_cycle_num >= current_cycle && fade.start_cycle_num <= current_cycle) { 214 | int32_t duration = fade.end_cycle_num - fade.start_cycle_num; 215 | int32_t time_left = fade.end_cycle_num - current_cycle; 216 | int32_t brightness = ((int32_t)fade.end_brightness) - ((int32_t)fade.start_brightness); 217 | int32_t hpoint_scale = brightness * time_left / duration; 218 | // TODO: do we need to scale here to account for non-linear brightness response? 219 | uint32_t hpoint = (uint32_t) (((int32_t)fade.start_brightness) + hpoint_scale); 220 | hpoint = MIN((1 << DUTY_BIT_DEPTH) - 1, hpoint); 221 | 222 | // Don't get too close to the zero crossing or the TRIAC will turn immediately off at highest 223 | // brightness. 224 | hpoint = MAX(TRIAC_GATE_QUIESCE_CYCLES, hpoint); 225 | uint32_t duty = TRIAC_GATE_IMPULSE_CYCLES; 226 | 227 | for (uint16_t channel = 0; channel < NUM_CHANNELS; ++channel) { 228 | if (fade.channels & (1 << channel)) { 229 | if (hpoint >= (1 << DUTY_BIT_DEPTH) - 1 - TRIAC_GATE_IMPULSE_CYCLES) { 230 | // If hpoint if very close to the maximum value, ie mostly off, simply turn off 231 | // the output to avoid glitch where hpoint exceeds duty. 232 | LEDC.channel_group[0].channel[channel].conf0.sig_out_en = 0; 233 | } else { 234 | LEDC.channel_group[0].channel[channel].hpoint.hpoint = hpoint; 235 | LEDC.channel_group[0].channel[channel].duty.duty = duty << 4; 236 | LEDC.channel_group[0].channel[channel].conf0.sig_out_en = 1; 237 | LEDC.channel_group[0].channel[channel].conf1.duty_start = 1; 238 | } 239 | } 240 | } 241 | } 242 | } 243 | 244 | uint32_t old_start_fade_index = start_fade_index; 245 | // Check if head of buffer is a completed fade. 246 | for (start_fade_index; start_fade_index != end_fade_index; ++start_fade_index) { 247 | volatile fade_t *start_fade = fades + (start_fade_index & (NUM_FADE_RECORDS - 1)); 248 | if (start_fade->end_cycle_num > current_cycle) break; 249 | } 250 | // Notify of finished fades. (Deliver after updating start_fade_index above so that receivers always see 251 | // finished fades as complete.) 252 | for (uint32_t fade_index = old_start_fade_index; fade_index != end_fade_index; ++fade_index) { 253 | volatile fade_t *fade = fades + (fade_index & (NUM_FADE_RECORDS - 1)); 254 | if (fade->end_cycle_num == current_cycle) { 255 | xQueueSendFromISR(fade_end_queue, (fade_t*) fade, NULL); 256 | } 257 | } 258 | } 259 | 260 | 261 | void IRAM_ATTR zero_crossing_interrupt(void* arg) { 262 | // ISR triggered by GPIO edge at the end of each Alternating Current half-cycle. 263 | // Used to reset the PWM timer, which synchronize the TRIAC operation with 264 | // the mains frequency. Also used to perform fading of PWM phase. 265 | 266 | uint32_t intr_st = GPIO.status; 267 | if (intr_st & (1 << ZERO_CROSSING_PIN)) { 268 | // Delay very slightly and retest pin to avoid bounce on negative edge. 269 | for (int i = 0; i < 100; ++i) {} 270 | if (GPIO.in & (1 << ZERO_CROSSING_PIN)) { 271 | 272 | GPIO.out_w1ts = 1 << LED_PIN; 273 | 274 | // Zero the PWM timer at the zero crossing. 275 | LEDC.timer_group[0].timer[0].conf.rst = 1; 276 | LEDC.timer_group[0].timer[0].conf.rst = 0; 277 | 278 | uint32_t current_cycle = (uint32_t)cycle_counter; 279 | set_up_fades(current_cycle); 280 | cycle_counter += 1; 281 | 282 | GPIO.out_w1tc = 1 << LED_PIN; 283 | } 284 | } 285 | GPIO.status_w1tc = intr_st; 286 | } 287 | 288 | 289 | void app_main(void) { 290 | nvs_flash_init(); 291 | 292 | esp_log_level_set(LOG_FADE, ESP_LOG_WARN); 293 | esp_log_level_set(LOG_CYCLES, ESP_LOG_WARN); 294 | 295 | ESP_LOGI(LOG_STARTUP, "\n\nIn main.\n"); 296 | 297 | // tcpip_adapter_init(); 298 | // ESP_ERROR_CHECK( esp_event_loop_init(event_handler, NULL) ); 299 | // wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 300 | // ESP_ERROR_CHECK( esp_wifi_init(&cfg) ); 301 | // ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) ); 302 | // ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) ); 303 | // wifi_config_t sta_config = { 304 | // .sta = { 305 | // .ssid = "mason", 306 | // .password = "PASSWORD", 307 | // .bssid_set = false 308 | // } 309 | // }; 310 | // ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &sta_config) ); 311 | // ESP_ERROR_CHECK( esp_wifi_start() ); 312 | // ESP_ERROR_CHECK( esp_wifi_connect() ); 313 | // 314 | // ESP_LOGI(LOG_STARTUP, "Configured wifi.\n"); 315 | 316 | // Set LED to monitor setup: 317 | gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT); 318 | gpio_set_level(LED_PIN, 1); 319 | 320 | // Zero Crossing interrupt: 321 | gpio_set_direction(ZERO_CROSSING_PIN, GPIO_MODE_INPUT); 322 | gpio_set_intr_type(ZERO_CROSSING_PIN, GPIO_INTR_POSEDGE); 323 | gpio_set_pull_mode(ZERO_CROSSING_PIN, GPIO_PULLDOWN_ONLY); 324 | 325 | ESP_ERROR_CHECK( gpio_isr_register( 326 | zero_crossing_interrupt, 327 | NULL, // No argument to handler. 328 | ESP_INTR_FLAG_LEVEL2, 329 | NULL // No need to store handler to interrupt registration. 330 | ) ); 331 | 332 | ESP_LOGI(LOG_STARTUP, "Configured Zero Crossing Interrupt.\n"); 333 | 334 | // Fade task and queue are used to serialize addition to the circular buffer of fade actions. 335 | fade_start_queue = xQueueCreate( 336 | 16, 337 | sizeof(fade_t) 338 | ); 339 | xTaskCreate( 340 | fade_start_task, 341 | "Fade start task", // Task name. 342 | 2048, // Stack size. 343 | NULL, // No parameters. 344 | 10, // Priority 345 | NULL // No need to store task handle. 346 | ); 347 | 348 | // Fade end task and queue are used to notify about the end of fades. 349 | fade_end_queue = xQueueCreate( 350 | 16, 351 | sizeof(fade_t) 352 | ); 353 | xTaskCreate( 354 | fade_end_task, 355 | "Fade end task", // Task name. 356 | 2048, // Stack size. 357 | NULL, // No parameters. 358 | 8, // Priority 359 | NULL // No need to store task handle. 360 | ); 361 | 362 | // Debugging task to report current cycle counter. 363 | xTaskCreate( 364 | timer_report_task, 365 | "Timer report task", // Task name. 366 | 2048, // Stack size. 367 | NULL, // No parameters. 368 | 6, // Priority 369 | NULL // No need to store task handle. 370 | ); 371 | 372 | ESP_LOGI(LOG_STARTUP, "Configured Fade Tasks and Queues.\n"); 373 | 374 | periph_module_enable(PERIPH_LEDC_MODULE); 375 | 376 | ESP_LOGI(LOG_STARTUP, "\nConfigured LED Controller.\n"); 377 | 378 | ledc_timer_config_t timer_config = { 379 | .speed_mode = LEDC_HIGH_SPEED_MODE, 380 | .timer_num = 0, 381 | .bit_num = DUTY_BIT_DEPTH, 382 | .freq_hz = HZ * 2, 383 | }; 384 | ESP_ERROR_CHECK( ledc_timer_config(&timer_config) ); 385 | 386 | ESP_LOGI(LOG_STARTUP, "Configured timer.\n"); 387 | 388 | for (int i = 0; i < NUM_CHANNELS; ++i) { 389 | ledc_channel_config_t led_config = { 390 | .gpio_num = channel_pins[i], 391 | .speed_mode = LEDC_HIGH_SPEED_MODE, 392 | .channel = i, 393 | .timer_sel = LEDC_TIMER_0, 394 | .duty = (1 << DUTY_BIT_DEPTH) - 1, 395 | .intr_type = LEDC_INTR_DISABLE, 396 | }; 397 | ESP_ERROR_CHECK( ledc_channel_config(&led_config) ); 398 | LEDC.channel_group[0].channel[i].duty.duty = TRIAC_GATE_IMPULSE_CYCLES << 4; 399 | // Initial brightness of 0, meaning turn TRIAC on at very end: 400 | LEDC.channel_group[0].channel[i].hpoint.hpoint = (1 << DUTY_BIT_DEPTH) - 1; 401 | LEDC.channel_group[0].channel[i].conf0.sig_out_en = 1; 402 | LEDC.channel_group[0].channel[i].conf1.duty_start = 1; 403 | } 404 | 405 | ESP_LOGI(LOG_STARTUP, "Configured channels.\n"); 406 | 407 | ESP_ERROR_CHECK( gpio_intr_enable(ZERO_CROSSING_PIN) ); 408 | ESP_LOGI(LOG_STARTUP, "Enabled zero crossing interrupt.\n"); 409 | 410 | for (uint16_t i = 0; i < NUM_CHANNELS; ++i) { 411 | set_channel_fade(i, TESTING_FADE_DURATION_MSECS, (1 << DUTY_BIT_DEPTH) - 1); 412 | vTaskDelay( TESTING_FADE_DURATION_MSECS / NUM_CHANNELS / TICK_PERIOD_MS); 413 | } 414 | 415 | ESP_LOGI(LOG_STARTUP, "Set channel fades.\n"); 416 | 417 | gpio_set_level(LED_PIN, 0); 418 | 419 | } 420 | --------------------------------------------------------------------------------