# 💡 Адресные светодиоды (WS2812B / SK6812) на ESP32 — FastLED

Учебный проект про адресные LED-ленты: один и тот же провод данных управляет
десятками независимых светодиодов, каждый из которых можно зажечь своим
цветом. Проект показывает, как это работает "под капотом" (протокол,
периферия RMT), и даёт готовый набор из 9 анимационных эффектов (радуга,
дыхание, комета, твинкл и др.), которыми можно управлять прямо из
Serial-консоли, не перепрошивая плату.

## 📋 Документация проекта

| Файл | Что внутри | Кому |
|---|---|---|
| [THEORY.md](THEORY.md) | Физика протокола WS2812B/RMT, целочисленная математика эффектов FastLED, архитектура класса `LedEffects`, поддержка RGBW | Хочешь понять, как это работает изнутри |
| [WIRING.md](WIRING.md) | Схема подключения, резистор, расчёт бюджета мощности, согласование уровней 3.3В/5В | Собираешь железо |

## Архитектура кода

Один класс-драйвер на компонент, без обёрток из свободных функций поверх
класса — стандартный паттерн для всех компонентов этого репозитория
(см. [`COMPONENT_STANDARD.md`](../../COMPONENT_STANDARD.md)):

```text
├── include/
│   ├── LedEffects.h          # Класс-драйвер: 9 эффектов, машина состояний, isAlive()
│   ├── LedPhysicalLink.h     # Внутренний помощник: RGBW-упаковка (см. THEORY.md, раздел 7.3)
│   └── LedHardwareConfig.h   # ЕДИНАЯ точка настройки чипа, цвет. порядка и режима каналов (RGB/RGBW)
├── src/
│   ├── LedEffects.cpp        # Реализация эффектов (HSV, синусоиды времени, неблокирующие таймеры)
│   ├── LedPhysicalLink.cpp   # Упаковка кадра в 3 или 4 байта/пиксель перед FastLED.show()
│   └── main.cpp              # Точка входа: разбор команд из Serial + вызов LedEffects
├── platformio.ini            # Конфигурация окружения pioarduino
├── THEORY.md                 # Физика WS2812B/RMT, математика эффектов, архитектура класса
└── WIRING.md                 # Схема подключения встроенного диода и внешних лент
```

`LedEffects` — единственный публичный класс-драйвер: конструктор получает
указатель на буфер `CRGB` и его длину (Dependency Injection — класс ничего не
знает про конкретный GPIO/чип), а весь публичный API — это `setEffect()` +
`update()` как обязательный минимум, плюс компактные сеттеры с разумными
значениями по умолчанию (`tickRainbow(speedMs = 20, ...)` и т.д.). Именно
поэтому отдельный упрощённый слой поверх класса не потребовался — `main.cpp`
использует `LedEffects` напрямую, без фасада.

`LedPhysicalLink` — небольшой внутренний помощник (не альтернативный
"слой API", а часть реализации): решает исключительно задачу упаковки
RGBW-кадра в 4 байта на пиксель для чипов с отдельным белым каналом
(подробности — [THEORY.md, раздел 7.3](THEORY.md#73-вариант-sk6812-rgbw-4-канала--как-это-реализовано)).
В обычном 3-канальном (RGB) режиме это нулевые накладные расходы: `sync()`
сразу выходит, физический и логический буферы — один и тот же указатель.

### Вотчдог `isAlive()`

Это компонент-актуатор (прямой GPIO/RMT-выход), а не датчик — "обрыв
провода" в классическом смысле неприменимо. `strip.isAlive()` отвечает на
другой, педагогически более вероятный вопрос: вызывается ли `update()`
достаточно регулярно. Если `loop()` где-то завис (например, на `delay()`
или бесконечном цикле) — анимация молча застынет на одном кадре, а
диагностика в консоли покажет `!!! ЛЕНТА НЕ ОБНОВЛЯЕТСЯ !!!` вместо `ACTIVE`
(тот же принцип, что и в [`Components/2. LED_middle`](../LED_middle/README.md)).

### Минимальный пример использования

```cpp
#include "LedEffects.h"
#include "LedHardwareConfig.h"

constexpr int LED_PIN = 5;
constexpr uint16_t NUM_LEDS = 60;

CRGB leds[NUM_LEDS];
LedEffects strip(leds, NUM_LEDS);   // autoShow = true по умолчанию — этого достаточно для одной ленты

void setup() {
    FastLED.addLeds<LED_CHIPSET, LED_PIN, LED_COLOR_ORDER>(leds, NUM_LEDS);
    strip.setEffect(LedEffects::Effect::RAINBOW);
}

void loop() {
    strip.update();   // ОДИН вызов в loop() — и вся анимация крутится сама
}
```

Тип чипа (WS2812B / SK6812 / ...) и порядок цветов настраиваются один раз в
`LedHardwareConfig.h` — в коде выше про них ничего не сказано. Если нужна
RGBW-лента (4-й, белый канал) или несколько независимых лент одновременно —
смотри разделы ["Выбор типа светодиодов"](#-выбор-типа-светодиодов-чип-3-или-4-канала-и-почему-не-neopixel)
и ["Масштабирование на несколько лент"](#-масштабирование-на-несколько-лент)
ниже — там показан полный `LedPhysicalLink`-вариант из `main.cpp` этого
проекта.

### Как менялась архитектура (для понимания читаемого кода)

| Было | Стало | Зачем |
| :--- | :--- | :--- |
| Таймеры на `EVERY_N_MILLISECONDS` и `static` внутри методов | Таймеры — `private`-поля объекта (`_lastRainbowMs` и т.д.) | Несколько экземпляров `LedEffects` (несколько лент) больше не делят таймеры и состояние strobe друг с другом — см. `THEORY.md` 6.1 |
| `tickStrobe()` красил только `_leds[0]` | `fill_solid(_leds, _numLeds, color)` | Strobe корректно работает на ленте любой длины |
| Эффект выбирался комментированием строк в `main.cpp` | `enum class Effect` + `update()` + словесные Serial-команды | Переключение эффекта без повторной прошивки, понятный синтаксис для новичков |
| Каждый `tick*()` сам вызывал `FastLED.show()` без альтернативы | Флаг `autoShow`, вызывающий код сам делает один общий `show()` | Несколько лент на одном стенде без лишних вызовов `FastLED.show()` — `THEORY.md` 6.3 |
| Только WS2812B зашит в коде | `LED_CHIPSET` в `LedHardwareConfig.h` (WS2812B/SK6812/...) | Лента SK6812 теперь действительно работает — `THEORY.md`, раздел 7 |
| Поддерживался только 3-канальный RGB | `LED_CHANNEL_MODE` (`LED_MODE_RGB`/`LED_MODE_RGBW`) + класс `LedPhysicalLink` | Лента SK6812 **RGBW** (4 канала, с отдельным белым кристаллом) теперь тоже работает корректно — `THEORY.md`, раздел 7.3 |
| Только 4 эффекта | 9 эффектов (включая Comet, ColorWipe, Twinkle, Pulse-от-точки) | Богаче визуально, проще для новичков — `THEORY.md`, раздел 8 |
| Посимвольный разбор Serial-команд | Построчный парсер, словесные команды ("rainbow", "pulse 12") | Надёжнее и понятнее — `THEORY.md`, раздел 9.2 |
| Отдельный слой `simple_api.h/.cpp` (свободные функции-обёртки) поверх класса | Убран — `main.cpp` использует `LedEffects`/`LedPhysicalLink` напрямую | Стандарт репозитория прямо запрещает фасад из свободных функций поверх класса; публичный API самого класса уже достаточно компактен |

---

## ⚙️ Конфигурация окружения (`platformio.ini`)

Использование форка `pioarduino` обязательно, так как оригинальная платформа
`espressif32` в PlatformIO не имеет полноценной поддержки Arduino Core 3.x.
Зависимость проекта — `fastled/FastLED @ ^3.9.0`.

---

## 💡 Выбор типа светодиодов: чип, 3 или 4 канала, и почему не NeoPixel

### Шаг 1 — какой у вас чип

Если лента не реагирует на код — скорее всего, дело не в проводах, а в том,
что чип ленты не совпадает с тем, что зашит в коде (подробная физика —
`THEORY.md`, раздел 7.1). Откройте `include/LedHardwareConfig.h` и поменяйте
одну строку:

```cpp
#define LED_CHIPSET SK6812   // было WS2812B — поменяйте на чип вашей ленты
```

### Шаг 2 — 3 канала (RGB) или 4 канала (RGBW)

У SK6812 бывают обе версии: обычная (3 канала, как у WS2812B) и RGBW (4 канала — добавлен отдельный белый кристалл). Это тоже одна строка в том же файле:

```cpp
#define LED_CHANNEL_MODE LED_MODE_RGBW   // или LED_MODE_RGB для 3-канальной ленты
```

**Как понять, какая у вас лента:** если после `LED_MODE_RGB` первые 1-2 диода работают нормально, а дальше по ленте цвета "съезжают"/смешиваются — это RGBW-лента, переключайтесь на `LED_MODE_RGBW`. Подробное объяснение симптома — `THEORY.md`, раздел 7.3.

В RGBW-режиме управление белым каналом — через команду `white N` в Serial-консоли (см. таблицу команд ниже) или напрямую `stripLink.setAllWhite(255)` в коде. Для RGB-ленты это безопасно, но ничего не делает — физического белого канала там просто нет.

> ⚠️ Если белый канал светится "не тем" цветом или цвета внутри RGBW перепутаны — поменяйте порядок байт `LED_RGBW_BYTE_ORDER_*` в том же файле (по умолчанию настроен самый частый вариант GRBW).

### Шаг 3 — а не лучше ли использовать Adafruit_NeoPixel?

Закономерный вопрос: у `Adafruit_NeoPixel` RGBW поддерживается "из коробки" — `Adafruit_NeoPixel(n, pin, NEO_GRBW + NEO_KHZ800)` и `setPixelColor(i, r, g, b, w)`, без описанного в `THEORY.md` трюка с переинтерпретацией буфера. Это честно **проще**, если вам нужна только RGBW-лента без анимаций. Мы тем не менее остаёмся на FastLED — вот честное сравнение:

| | FastLED (этот проект) | Adafruit_NeoPixel |
| :--- | :--- | :--- |
| RGBW из коробки | Нет — нужен `LedPhysicalLink` (но он уже написан и спрятан от вас) | Да, нативно: `setPixelColor(i, r, g, b, w)` |
| Готовая математика эффектов (радуга, синусоиды, шум, угасание, насыщенная арифметика) | Да — `fill_rainbow`, `beatsin8`, `nscale8`, `fadeToBlackBy`, `qadd8/qsub8`, `random8` и т.д. | Нет — есть только `setPixelColor`/`fill`, остальное пишете сами |
| Палитры и цветовые градиенты | Да, встроены | Нет |
| Опора всего класса `LedEffects` на этой математике | Полная — переход на NeoPixel означал бы переписать все 9 эффектов | — |
| RMT-драйвер на ESP32 | Свой, `FASTLED_RMT_BUILTIN_DRIVER`, активно поддерживается под новые чипы (включая ESP32-C6) | Свой (другой) RMT-драйвер; в сообществе периодически всплывают проблемы с заявкой RMT-канала при использовании совместно с другими RMT-устройствами |

Итог: если бы это был **только** "зажечь RGBW-ленту ровным светом" — NeoPixel был бы проще и короче. Но раз вся ценность проекта — в эффектах (радуга, дыхание, комета, твинкл, пульс и т.д.), переезд на NeoPixel означал бы выбросить готовую, проверенную математику FastLED и написать её самостоятельно. Подробный разбор — `THEORY.md`, раздел 7.4.

---

## 🐧 Настройка хост-системы Linux (при работе через Native USB)

Для работы с портом **Native USB** (`/dev/ttyACM*`) обычному пользователю требуются права на чтение и запись.

```bash
sudo usermod -a -G dialout,plugdev $USER
```

После этого необходимо **перезагрузить компьютер** (или хотя бы выйти и снова войти в графическую сессию), чтобы новые группы применились. Экстренный фикс без перезагрузки:

```bash
sudo chmod 666 /dev/ttyACM*
```

---

## 🚀 Как запустить

1. Подключите ленту по схеме из [WIRING.md](WIRING.md) и плату — к компьютеру по USB.
2. Соберите, залейте прошивку и откройте монитор порта:
   ```bash
   pio run -t upload -t monitor
   ```
3. При старте лента на секунду вспыхивает сплошным белым (проверка, что все диоды подключены и чип задан верно), затем автоматически запускается эффект **Rainbow**.

### 🎛️ Управление эффектами без повторной прошивки

Во время работы можно переключать эффекты прямо в мониторе порта — наберите слово (или короткую букву) и нажмите **Enter**:

| Команда | Действие |
| :--- | :--- |
| `solid` | Solid — сплошной белый |
| `rainbow` / `r` | Rainbow — бегущая радуга |
| `breath` / `b` | Breath — плавное дыхание цветом Aqua |
| `strobe` / `s` | Strobe — стробоскоп Red/Blue |
| `comet` / `c` | Comet — бегущая комета с угасающим хвостом |
| `wipe` / `w` | Color Wipe — заливка цветом по очереди, диод за диодом |
| `twinkle` / `t` | Twinkle — случайно мерцающие звёзды |
| `pulse` / `p` | Pulse — волна, расходящаяся от **середины** ленты |
| `pulse N` | Pulse — волна, расходящаяся от диода **номер N** (например `pulse 12`) |
| `off` / `0` | Off — погасить ленту |
| `white N` | Уровень белого канала N (0..255) — только для RGBW-ленты (`LED_MODE_RGBW`) |
| `+` / `-` | Программная яркость ярче/тусклее |
| `help` / `h` | Показать справку по командам ещё раз |

Если команда не распознана, прошивка явно ответит `[CMD] Неизвестная команда: "..."` — это удобный способ проверить, что Serial физически работает, даже если вы просто ошиблись в слове. Подробнее о том, почему построчный разбор надёжнее посимвольного — `THEORY.md`, раздел 9.2.

Раз в секунду в консоль выводится диагностическая строка:
```text
Status: ACTIVE | Brightness: 255 | Free SRAM: 430124 bytes
```
`Status` берётся из `strip.isAlive()` — если вместо `ACTIVE` вы видите
`!!! ЛЕНТА НЕ ОБНОВЛЯЕТСЯ !!!`, значит `loop()` где-то завис (см. раздел
"Вотчдог isAlive()" выше).

---

## 🚨 Диагностика неисправностей

| Симптом | Причина | Решение |
| :--- | :--- | :--- |
| Ошибка `could not open port /dev/ttyACM*` | Порт заблокирован, или плата сменила индекс (`ttyACM0` → `ttyACM1`). | Переподключите кабель, `sudo chmod 666 /dev/ttyACM*`. Перед прошивкой зажмите **BOOT**, нажмите **RST**, отпустите **BOOT**. |
| В консоли циклический мусор (`Guru Meditation Error`) | Ошибка инициализации RMT-драйвера или битая Flash-память. | Убедитесь, что `#define FASTLED_RMT_BUILTIN_DRIVER 1` объявлен строго до `#include <FastLED.h>` в `LedEffects.h`. |
| Лента совсем не светится / мигает хаотично, хотя провода в порядке | В `LedHardwareConfig.h` указан не тот чип (например, `WS2812B` вместо реальной `SK6812`). | Смените `LED_CHIPSET` в `LedHardwareConfig.h` — см. раздел "Выбор типа светодиодов" выше и `THEORY.md`, 7.1. |
| Первые 1-2 диода работают нормально, дальше цвета "съезжают"/смешиваются | Лента — это SK6812 **RGBW** (4 канала), а в `LedHardwareConfig.h` стоит `LED_MODE_RGB`. | Поставьте `LED_CHANNEL_MODE = LED_MODE_RGBW` — см. раздел "Выбор типа светодиодов" выше и `THEORY.md`, 7.3. |
| Белый канал светится не тем цветом / RGBW-цвета перепутаны | Порядок 4 байт в потоке у вашей конкретной партии ленты не GRBW. | Поменяйте `LED_RGBW_BYTE_ORDER_*` в `LedHardwareConfig.h`. |
| Команды в Serial-мониторе "не работают" | Чаще всего — забыли нажать **Enter** после ввода слова (так работает фильтр `send_on_enter` в `platformio.ini`). | Наберите команду и нажмите Enter. Если после этого видите `[CMD] Неизвестная команда: ...` — Serial работает, просто проверьте написание слова. Если вообще ничего не печатается — проверьте `monitor_speed = 115200` и что используется верный `/dev/ttyACM*`. |
| Один-два диода горят одним цветом или хаотично мигают белым, остальные в порядке | Скорее всего дефект самого диода в цепочке, либо отсутствует общий GND при подключении внешней ленты (см. `WIRING.md`). | Проверьте общий провод земли; если не помогает — вероятен брак конкретного диода. |
| Strobe мигает только на первом диоде ленты, остальные "замёрзли" | Используется версия класса до v2.0 (`_leds[0] = ...` вместо `fill_solid`). | Обновите `LedEffects.cpp` до текущей версии — см. `THEORY.md`, раздел 5.4. |
| При двух лентах эффекты "путают" скорость или фазу друг друга | Использована версия класса до v2.0 со `static`-таймерами внутри методов. | Обновите класс до текущей версии — таймеры теперь поля объекта, см. `THEORY.md`, раздел 6.1. |
| В консоли `!!! ЛЕНТА НЕ ОБНОВЛЯЕТСЯ !!!` вместо `ACTIVE` | `update()` не вызывается достаточно часто — `loop()` где-то завис (например, на `delay()` или в бесконечном цикле). | Проверьте, не добавили ли вы блокирующий код в `loop()`. |

---

## 📈 Масштабирование на несколько лент

Класс `LedEffects` готов к одновременной работе с несколькими независимыми лентами:

```cpp
CRGB ledsA[60];
CRGB ledsB[1];

// autoShow = false на ОБОИХ экземплярах — см. THEORY.md, раздел 6.3
LedEffects stripA(ledsA, 60, /*autoShow=*/false);
LedEffects stripB(ledsB, 1,  /*autoShow=*/false);

void loop() {
    stripA.update();
    stripB.update();
    FastLED.show(); // один общий вызов на весь кадр сразу для всех лент
}
```

Подробное объяснение того, почему именно так, а не иначе — `THEORY.md`, раздел 6.3 и 6.4.

### RGBW-лента через `LedPhysicalLink`

Если лента RGBW (4 канала), используйте `LedPhysicalLink` — именно так устроен `main.cpp` этого проекта:

```cpp
#include "LedEffects.h"
#include "LedPhysicalLink.h"

LedPhysicalLink stripLink(60, /*rgbw=*/true);
LedEffects strip(stripLink.logicalBuffer(), 60, /*autoShow=*/false);

void setup() {
    FastLED.addLeds<SK6812, 8, GRB>(stripLink.physicalBuffer(), stripLink.physicalSlotCount());
    strip.setEffect(LedEffects::Effect::RAINBOW);
}

void loop() {
    strip.update();
    stripLink.setAllWhite(128);           // независимое управление белым каналом
    stripLink.sync(strip.getBrightnessScale());
    FastLED.show();
}
```

`LedEffects` здесь, как и везде в проекте, работает только с обычным 3-канальным буфером и ничего не знает про White — RGBW-специфика целиком в `LedPhysicalLink`.

---

## 📝 Разбор используемых технологий

* **FastLED (v3.9.0+)**: оптимизированная 8-битная математика цвета (HSV-пространство), целочисленные вычисления без `float`.
* **Неблокирующие таймеры на полях объекта**: в коде полностью исключена функция `delay()` в рабочих эффектах. Каждый экземпляр класса хранит собственные отметки времени, что делает диспетчеризацию задач по-настоящему независимой между несколькими лентами — подробности в `THEORY.md`.


## Связанные материалы

- [Резистор](/docs/circuit_design/electrical-engineering/0.-components/rezistor/) —
  токоограничение и расчёт мощности; для ленты особенно важен бюджет питания.
- [Ambilight](/docs/projects/ambilight/) — проект на адресной ленте: подсветка,
  повторяющая цвета экрана.
