diff --git a/firmware/main/led.c b/firmware/main/led.c index a17e370..3cf00f3 100644 --- a/firmware/main/led.c +++ b/firmware/main/led.c @@ -23,7 +23,10 @@ static SemaphoreHandle_t led_mutex = NULL; // Global LED color and intensity variables (GRB format for WS2812 LEDs) static uint32_t led_color = LED_COLOR_OFF; // Current LED color -static float led_intensity = 0.6f; // Current LED intensity (0.0 to 1.0) +// Start at 0 so the LEDs stay dark until button_init() loads the saved +// brightness from NVS - otherwise every boot flashes the startup color even +// when the user has set brightness to off. +static float led_intensity = 0.0f; // Current LED intensity (0.0 to 1.0) // LED state structure for WS2812 driver static struct led_state led_new_state = {0}; @@ -39,50 +42,70 @@ static struct led_state led_new_state = {0}; */ static void led_task(void *pvParameters) { + // Rewriting an unchanged frame 50x/s keeps the WS2812 data line busy at all + // times, so a frame corrupted by a coincident radio TX burst (Zigbee + BLE + // push every 10 s) briefly latches a random color - visible as a blink even + // when the LEDs are set to off. Only write when the frame changes; after a + // change, repeat the frame briefly so a corrupted transition write self-heals; + // when lit, refresh slowly as a safety net; when off, keep the line idle. + uint32_t last_written_color = 0xFFFFFFFF; // Sentinel: force first write + TickType_t last_write_ticks = 0; + TickType_t last_change_ticks = 0; + while (1) { + bool have_color = false; + uint32_t current_color = LED_COLOR_OFF; + float current_intensity = 0.0f; + // Take mutex to safely read LED color and intensity if (led_mutex != NULL && xSemaphoreTake(led_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { // Read current LED color and intensity safely - uint32_t current_color = led_color; - float current_intensity = led_intensity; + current_color = led_color; + current_intensity = led_intensity; xSemaphoreGive(led_mutex); - + have_color = true; + } else if (led_mutex == NULL) { + // Fallback mode if mutex is not available + ESP_LOGW("led", "LED mutex not available, using direct access"); + current_color = led_color; + current_intensity = led_intensity; + have_color = true; + } else { + ESP_LOGW("led", "Failed to take LED mutex - skipping update"); + } + + if (have_color) { // Apply intensity to color uint32_t final_color = apply_color_intensity(current_color, current_intensity); - - // Set all 3 LEDs to the same color - for (int i = 0; i < NUM_CONTROLLED_LEDS; i++) { - led_new_state.leds[i] = final_color; - } - - // Set remaining LEDs to off - for (int i = NUM_CONTROLLED_LEDS; i < NUM_LEDS; i++) { - led_new_state.leds[i] = LED_COLOR_OFF; - } - - // Update WS2812 LEDs - ws2812_write_leds(led_new_state); - } else { - // Fallback mode if mutex is not available - if (led_mutex == NULL) { - ESP_LOGW("led", "LED mutex not available, using direct access"); - // Apply intensity to color - uint32_t final_color = apply_color_intensity(led_color, led_intensity); - + + TickType_t now = xTaskGetTickCount(); + bool changed = (final_color != last_written_color); + // Repeat writes for 500ms after a change so a corrupted frame can't stick + bool settling = (now - last_change_ticks) < pdMS_TO_TICKS(500); + // Slow refresh while visibly lit; fully idle once settled at off + bool refresh_due = (final_color != LED_COLOR_OFF) && + (now - last_write_ticks) >= pdMS_TO_TICKS(1000); + + if (changed || settling || refresh_due) { + if (changed) { + last_change_ticks = now; + } + // Set all 3 LEDs to the same color for (int i = 0; i < NUM_CONTROLLED_LEDS; i++) { led_new_state.leds[i] = final_color; } - + // Set remaining LEDs to off for (int i = NUM_CONTROLLED_LEDS; i < NUM_LEDS; i++) { led_new_state.leds[i] = LED_COLOR_OFF; } - + // Update WS2812 LEDs - ws2812_write_leds(led_new_state); - } else { - ESP_LOGW("led", "Failed to take LED mutex - skipping update"); + if (ws2812_write_leds(led_new_state) == ESP_OK) { + last_written_color = final_color; + last_write_ticks = now; + } } } diff --git a/firmware/main/zigbee.c b/firmware/main/zigbee.c index 596f335..dba7557 100644 --- a/firmware/main/zigbee.c +++ b/firmware/main/zigbee.c @@ -287,16 +287,37 @@ void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) 0, STARTUP_REPORT_DELAY_MS); } } else { - s_init_fail_count++; - if (s_init_fail_count >= INIT_FAIL_MAX) { - ESP_LOGE(TAG, "Zigbee init failed %d times – rebooting to reset radio", + if (s_init_fail_count < 250) { + s_init_fail_count++; + } + /* A commissioned device whose coordinator is unreachable + * (powered off, out of range, network gone) fails init forever. + * Reboot at most once to clear a genuinely wedged radio - if we + * already rebooted for this (ESP_RST_SW) it didn't help, so keep + * retrying with exponential backoff instead of boot-looping, + * which flashes the LED on every restart. */ + if (s_init_fail_count == INIT_FAIL_MAX && + esp_reset_reason() != ESP_RST_SW) { + ESP_LOGE(TAG, "Zigbee init failed %d times – rebooting once to reset radio", s_init_fail_count); esp_restart(); } - ESP_LOGI(TAG, "Waiting for coordinator (%s), attempt %d/%d, retrying", - esp_err_to_name(err_status), s_init_fail_count, INIT_FAIL_MAX); + uint32_t delay_ms = REJOIN_BACKOFF_INIT_MS; + if (s_init_fail_count > INIT_FAIL_MAX) { + uint8_t doublings = s_init_fail_count - INIT_FAIL_MAX; + if (doublings > 9) { + doublings = 9; + } + delay_ms <<= doublings; + if (delay_ms > REJOIN_BACKOFF_MAX_MS) { + delay_ms = REJOIN_BACKOFF_MAX_MS; + } + } + ESP_LOGI(TAG, "Waiting for coordinator (%s), attempt %d, retrying in %lu ms", + esp_err_to_name(err_status), s_init_fail_count, + (unsigned long)delay_ms); esp_zb_scheduler_alarm((esp_zb_callback_t)bdb_start_top_level_commissioning_cb, - ESP_ZB_BDB_MODE_INITIALIZATION, 1000); + ESP_ZB_BDB_MODE_INITIALIZATION, delay_ms); } break;