# THEORY.md — Физика и математика светодиода

## 1. От физики к коду

**Физическая величина:** падение напряжения на светодиоде (Vled) и предельный ток (I).
Светодиод — это не лампочка, у него нет встроенного сопротивления, ограничивающего ток.
Если подать на него полные 5 В без резистора — он сгорит за доли секунды.

**Формула резистора (закон Ома):**

```
R = (Vcc - Vled) / I
```

Пример для красного светодиода: Vcc = 5В, Vled ≈ 2В, I = 0.02А (20мА):

```
R = (5 - 2) / 0.02 = 150 Ом   → берём 220 Ом (стандартный, с запасом)
```

Для других цветов Vled другой (например, у синего/белого светодиода Vled ≈ 3.2В) —
конкретные значения резисторов для схемы этого модуля см. в [WIRING.md](WIRING.md).

В коде эта физика никак не "видна" — `digitalWrite(pin, HIGH)` просто подаёт логическую
единицу на пин. Резистор всегда стоит **на схеме**, а не в коде — это важно объяснить
ученикам: программа управляет логикой "включить/выключить", а защита тока — это
аппаратная часть.

## 2. Почему `millis()`, а не `delay()`

`delay()` останавливает выполнение **всего** кода процессора на заданное время.
`millis()` возвращает количество миллисекунд с момента старта платы и продолжает расти само —
поэтому можно просто **сравнивать разницу времени**, ничего не "замораживая".

```cpp
if (now - _lastChange >= wait) { /* пора переключить светодиод */ }
```

Это основа неблокирующей логики в [`LedController::update()`](src/LedController.cpp#L74-L97).

## 3. Физический смысл ➔ Ошибка ➔ Решение

**Несколько светодиодов мигают с разной частотой одновременно**
➔ Ошибка: использовать `delay()` для каждого — пока ждём один, остальные "замирают".
➔ Решение: каждый объект `LedController` хранит свой `_lastChange` и сравнивает его с
`millis()` независимо ([`update()`](src/LedController.cpp#L74-L97)).

**Бегущий огонь должен зажигать только один светодиод за раз**
➔ Ошибка: забыть выключить предыдущий пин перед включением следующего — все горят одновременно.
➔ Решение: `digitalWrite(_pins[_runningIndex], LOW)` перед переходом к новому индексу
([`update()`, ветка `MODE_RUNNING`](src/LedController.cpp#L89-L95)).

**Ученик вызвал `blinkArray(5, ...)`, а групп всего 4 (id 0..3)**
➔ Ошибка: обращение к `groups[5]` — выход за границы массива, неопределённое поведение.
➔ Решение: проверка `if (groupId < 0 || groupId >= MAX_GROUPS) return;` в каждой функции
фасада ([`simple_api.cpp`](src/simple_api.cpp)).

**Группе передали пустой массив пинов или `count = 0`**
➔ Ошибка: `_runningIndex = (_runningIndex + 1) % _count` при `_count == 0` — деление на
ноль, программа "падает" (перезагрузка по panic).
➔ Решение: конструктор [`LedController(const int* pins, int count)`](src/LedController.cpp#L14-L30)
при вырожденном входе создаёт "пустой" объект (`isConfigValid() == false`), и все методы
после этого превращаются в безопасный no-op — см. следующий раздел.

## 4. `isAlive()` у актуатора — что это значит

У датчиков `isAlive()` обычно значит "не оборвался ли провод, продолжает ли датчик
отвечать на шине". У этого модуля шины нет — светодиод либо подключен, либо нет, и
программно это не проверить. Зато можно проверить кое-что не менее важное для отладки:

> **Вызывается ли `update()` / `updateLeds()` вообще?**

Если в `loop()` забыли вызвать `updateLeds()` (или `loop()` где-то завис на `delay()`
или в бесконечном цикле), мигание останавливается — но само по себе это не всегда сразу
заметно на глаз (светодиод может "залипнуть" во включённом состоянии). Поэтому
[`LedController::isAlive()`](src/LedController.cpp#L99-L101) хранит метку времени
последнего вызова `update()` и сравнивает её с `millis()`:

```cpp
bool LedController::isAlive(unsigned long timeoutMs) const {
    return (millis() - _lastUpdateCall) < timeoutMs;
}
```

Порог по умолчанию — 500 мс, с большим запасом относительно типичной длительности
одной итерации `loop()` (обычно меньше 1 мс), чтобы не срабатывать ложно на короткие
задержки где-то ещё в программе. В тестовом стенде [`main.cpp`](src/main.cpp) это
значение выводится в Serial каждую секунду вместе с `isConfigValid()`.

## 5. Проверочные вопросы для ученика

1. Что произойдёт со светодиодами, если в `loop()` забыть вызвать `updateLeds()`? Как
   это увидеть, не разбирая код построчно?
2. Почему в тестовом стенде `Serial.println(...)` печатается раз в секунду, а
   светодиоды при этом не "сбиваются" с ритма?
3. Зачем функции фасада (`blinkSingle`, `blinkArray`) проверяют пин или `groupId`,
   прежде чем что-то делать?
4. Что хранится внутри `simple_api.cpp`, чего ученик никогда не видит, вызывая `initLed(2)`?
5. Светодиоду нужно напряжение 2В и ток 20мА, питание платы выдаёт 5В. Посчитай нужный
   резистор по формуле `R = (Vcc - Vled) / I`.
6. Почему у этого модуля нет проверки "оборвался ли провод к светодиоду", в отличие от
   датчиков вроде `AHT20_BMP280`, где есть `isAlive()` c похожим именем, но другим смыслом?
