Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 52 additions & 29 deletions firmware/main/led.c
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
}
}
}

Expand Down
33 changes: 27 additions & 6 deletions firmware/main/zigbee.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down