Files
watch-watch/main/ws2812_status.c

368 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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;
}