#include "ws2812_status.h" #include "dcdc_controller.h" #include "led_strip.h" #include "esp_check.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/timers.h" #include "freertos/semphr.h" #include "sdkconfig.h" #ifndef CONFIG_WATCH_WS2812_LED_COUNT #define CONFIG_WATCH_WS2812_LED_COUNT 5 #endif #ifndef CONFIG_WATCH_WS2812_GPIO #define CONFIG_WATCH_WS2812_GPIO 8 #endif #ifndef CONFIG_WATCH_WS2812_RMT_RESOLUTION #define CONFIG_WATCH_WS2812_RMT_RESOLUTION (10 * 1000 * 1000) #endif #define WS2812_STATUS_GPIO ((gpio_num_t)CONFIG_WATCH_WS2812_GPIO) #define WS2812_STATUS_RESOLUTION_HZ CONFIG_WATCH_WS2812_RMT_RESOLUTION #define WS2812_ANIM_REFRESH pdMS_TO_TICKS(100) #define WS2812_ALERT_BLINK_PERIOD pdMS_TO_TICKS(200) #define WS2812_ALERT_GAP_PERIOD pdMS_TO_TICKS(2000) #define WS2812_VPN_ALERT_BLINKS 2 #define WS2812_APP_ALERT_BLINKS 3 #define WS2812_BRIGHTNESS_PERCENT 5 #define WS2812_VPN_SECTION_TICKS (WS2812_ALERT_BLINK_PERIOD * 2U * WS2812_VPN_ALERT_BLINKS) #define WS2812_APP_SECTION_TICKS (WS2812_ALERT_BLINK_PERIOD * 2U * WS2812_APP_ALERT_BLINKS) static const char *TAG = "ws2812"; static led_strip_handle_t s_strip; static bool s_channel_enabled[WS2812_STATUS_LED_COUNT]; static bool s_vpn_state[WS2812_STATUS_LED_COUNT]; static bool s_app_state[WS2812_STATUS_LED_COUNT]; static SemaphoreHandle_t s_ws_lock; static TimerHandle_t s_animation_timer; static bool s_error_state; static bool s_startup_hold; static TickType_t s_startup_expire_tick; static TickType_t s_alert_cycle_epoch; static size_t s_active_vpn_alerts; static size_t s_active_app_alerts; static esp_err_t ws2812_status_apply(void); static void ws2812_animation_timer_cb(TimerHandle_t timer); static void ws2812_status_compute_color(size_t index, TickType_t now_ticks, uint8_t *r, uint8_t *g, uint8_t *b); static bool ws2812_recalculate_alert_counts(void); static uint8_t ws2812_apply_brightness(uint8_t component); static void ws2812_animation_timer_cb(TimerHandle_t timer) { (void)timer; ws2812_status_apply(); } static bool ws2812_startup_hold_active(TickType_t now_ticks) { if (!s_startup_hold) { return false; } if (now_ticks >= s_startup_expire_tick) { s_startup_hold = false; return false; } return true; } static void ws2812_status_compute_color(size_t index, TickType_t now_ticks, uint8_t *r, uint8_t *g, uint8_t *b) { if (!r || !g || !b || index >= WS2812_STATUS_LED_COUNT) { return; } *r = 0; *g = 0; *b = 0; if (s_error_state) { *r = 40; return; } if (ws2812_startup_hold_active(now_ticks)) { *g = 40; return; } if (!s_channel_enabled[index]) { return; } const bool vpn_alert = !s_vpn_state[index]; const bool app_alert = !s_app_state[index]; if (!vpn_alert && !app_alert) { return; } const bool vpn_global = (s_active_vpn_alerts > 0); const bool app_global = (s_active_app_alerts > 0); TickType_t cycle_ticks = 0; if (vpn_global) { cycle_ticks += WS2812_VPN_SECTION_TICKS + WS2812_ALERT_GAP_PERIOD; } if (app_global) { cycle_ticks += WS2812_APP_SECTION_TICKS + WS2812_ALERT_GAP_PERIOD; } if (cycle_ticks == 0) { return; } TickType_t blink_period = WS2812_ALERT_BLINK_PERIOD ? WS2812_ALERT_BLINK_PERIOD : 1; TickType_t cycle_pos = (now_ticks - s_alert_cycle_epoch) % cycle_ticks; if (vpn_global) { if (cycle_pos < WS2812_VPN_SECTION_TICKS) { if (vpn_alert) { const bool on = ((cycle_pos / blink_period) % 2U) == 0U; if (on) { *r = 90; } } return; } cycle_pos -= WS2812_VPN_SECTION_TICKS; if (cycle_pos < WS2812_ALERT_GAP_PERIOD) { return; } cycle_pos -= WS2812_ALERT_GAP_PERIOD; } if (app_global) { if (cycle_pos < WS2812_APP_SECTION_TICKS) { if (app_alert) { const bool on = ((cycle_pos / blink_period) % 2U) == 0U; if (on) { *r = 80; *g = 80; } } return; } cycle_pos -= WS2812_APP_SECTION_TICKS; if (cycle_pos < WS2812_ALERT_GAP_PERIOD) { return; } } } static esp_err_t ws2812_status_apply(void) { if (!s_strip) { return ESP_ERR_INVALID_STATE; } if (!s_ws_lock) { return ESP_ERR_INVALID_STATE; } if (xSemaphoreTake(s_ws_lock, portMAX_DELAY) != pdTRUE) { return ESP_ERR_TIMEOUT; } const TickType_t now_ticks = xTaskGetTickCount(); esp_err_t err = ESP_OK; for (size_t i = 0; i < WS2812_STATUS_LED_COUNT && err == ESP_OK; ++i) { uint8_t r = 0, g = 0, b = 0; ws2812_status_compute_color(i, now_ticks, &r, &g, &b); err = led_strip_set_pixel(s_strip, i, ws2812_apply_brightness(g), ws2812_apply_brightness(r), ws2812_apply_brightness(b)); } if (err == ESP_OK) { err = led_strip_refresh(s_strip); } xSemaphoreGive(s_ws_lock); return err; } esp_err_t ws2812_status_init(void) { if (s_strip) { return ESP_OK; } led_strip_config_t strip_config = { .strip_gpio_num = WS2812_STATUS_GPIO, .max_leds = WS2812_STATUS_LED_COUNT, .led_model = LED_MODEL_WS2812, .flags.invert_out = false, }; led_strip_rmt_config_t rmt_cfg = { .clk_src = RMT_CLK_SRC_DEFAULT, .resolution_hz = WS2812_STATUS_RESOLUTION_HZ, .flags.with_dma = false, }; ESP_RETURN_ON_ERROR( led_strip_new_rmt_device(&strip_config, &rmt_cfg, &s_strip), TAG, "Не вдалося створити RMT пристрій для WS2812"); ESP_RETURN_ON_ERROR(led_strip_clear(s_strip), TAG, "clear fail"); s_ws_lock = xSemaphoreCreateMutex(); ESP_RETURN_ON_FALSE(s_ws_lock, ESP_ERR_NO_MEM, TAG, "mutex alloc failed"); const TickType_t now = xTaskGetTickCount(); for (size_t i = 0; i < WS2812_STATUS_LED_COUNT; ++i) { s_channel_enabled[i] = false; s_vpn_state[i] = true; s_app_state[i] = true; } s_error_state = false; s_startup_hold = false; s_startup_expire_tick = 0; s_alert_cycle_epoch = now; (void)ws2812_recalculate_alert_counts(); if (!s_animation_timer) { s_animation_timer = xTimerCreate("ws2812_anim", WS2812_ANIM_REFRESH, pdTRUE, NULL, ws2812_animation_timer_cb); ESP_RETURN_ON_FALSE(s_animation_timer, ESP_ERR_NO_MEM, TAG, "anim timer alloc failed"); ESP_RETURN_ON_FALSE(xTimerStart(s_animation_timer, 0) == pdPASS, ESP_FAIL, TAG, "anim timer start failed"); } (void)ws2812_recalculate_alert_counts(); return ws2812_status_apply(); } esp_err_t ws2812_status_set_channel_state(size_t channel, bool enabled) { if (channel >= WS2812_STATUS_LED_COUNT) { return ESP_ERR_INVALID_ARG; } if (!s_strip) { return ESP_ERR_INVALID_STATE; } s_channel_enabled[channel] = enabled; if (!enabled) { s_vpn_state[channel] = true; s_app_state[channel] = true; } return ws2812_status_apply(); } esp_err_t ws2812_status_set_error(bool has_error) { if (!s_strip) { return ESP_ERR_INVALID_STATE; } s_error_state = has_error; return ws2812_status_apply(); } esp_err_t ws2812_status_set_service_state(size_t channel, bool vpn_ok, bool app_ok) { if (channel >= WS2812_STATUS_LED_COUNT) { return ESP_ERR_INVALID_ARG; } if (!s_strip) { return ESP_ERR_INVALID_STATE; } bool changed = false; if (s_vpn_state[channel] != vpn_ok) { s_vpn_state[channel] = vpn_ok; changed = true; } if (s_app_state[channel] != app_ok) { s_app_state[channel] = app_ok; changed = true; } if (changed) { (void)ws2812_recalculate_alert_counts(); s_alert_cycle_epoch = xTaskGetTickCount(); } return ws2812_status_apply(); } esp_err_t ws2812_status_refresh_from_dcdc(void) { if (!s_strip) { return ESP_ERR_INVALID_STATE; } const size_t available_channels = dcdc_channel_count(); const size_t count = available_channels < WS2812_STATUS_LED_COUNT ? available_channels : WS2812_STATUS_LED_COUNT; for (size_t i = 0; i < count; ++i) { s_channel_enabled[i] = dcdc_get_state(i); } for (size_t i = count; i < WS2812_STATUS_LED_COUNT; ++i) { s_channel_enabled[i] = false; s_vpn_state[i] = true; s_app_state[i] = true; } (void)ws2812_recalculate_alert_counts(); s_alert_cycle_epoch = xTaskGetTickCount(); return ws2812_status_apply(); } esp_err_t ws2812_status_set_startup_hold(uint32_t duration_ms) { if (!s_strip) { return ESP_ERR_INVALID_STATE; } if (duration_ms == 0) { s_startup_hold = false; s_startup_expire_tick = 0; return ws2812_status_apply(); } TickType_t duration_ticks = pdMS_TO_TICKS(duration_ms); if (duration_ticks == 0) { duration_ticks = 1; } s_startup_hold = true; s_startup_expire_tick = xTaskGetTickCount() + duration_ticks; return ws2812_status_apply(); } static bool ws2812_recalculate_alert_counts(void) { size_t vpn = 0; size_t app = 0; for (size_t i = 0; i < WS2812_STATUS_LED_COUNT; ++i) { if (!s_channel_enabled[i]) { continue; } if (!s_vpn_state[i]) { ++vpn; } if (!s_app_state[i]) { ++app; } } const bool changed = (vpn != s_active_vpn_alerts) || (app != s_active_app_alerts); s_active_vpn_alerts = vpn; s_active_app_alerts = app; return changed; } static uint8_t ws2812_apply_brightness(uint8_t component) { // Scale brightness down (component * 20%) with rounding to nearest. const uint16_t scaled = (component * WS2812_BRIGHTNESS_PERCENT + 50U) / 100U; return (uint8_t)scaled; }