Ночник у кровати я собрал за один вечер и тут же его испортил: щелчок кнопки — и комната в полную яркость, как фотографическая вспышка в тёмной аудитории. Зрачок, который минуту назад привыкал к темноте, такое переживает плохо: на секунды пропадает картинка, а вместе с ней и сонливость. Самое обидное, что заменённая лампа накаливания вела себя иначе — вольфрамовая нить разогревалась за десятую долю секунды, и её вспышку глаз прощал. Светодиод включается мгновенно, и эту плавность, которую лампа давала даром, ночнику приходится программировать руками.
В этом уроке собираем ночник из двух приёмов, которые редко идут в паре в готовых скетчах. Первый — цвет глубже лампового: не 2700K «тёплый белый», а около 1900K, как тлеющий уголёк в камине, — пропорция каналов, где синего почти нет. Второй — плавный пуск: кнопка на D2 не зажигает ленту, а задаёт цель, и яркость сама доезжает до неё за две с половиной секунды. Повторный щелчок — и свет так же неторопливо оседает в темноту. Вся механика на millis(), без единого delay(): пока рампа ползёт, кнопка остаётся живой.
Числа в уроке — с моего ночника из шестнадцати пикселей в деревянном бруске с молочным рассеивателем. В симуляторе та же логика крутится на кнопке: нажмите — и следите, как свет разгоняется по ступеням, а не щёлкает. Каркас кадров общий с эффектами без delay, так что если он у вас уже набит на руке, новое здесь ровно два слова в объявлении: цель и текущее значение.
Цвет 1900K: теплее тёплого
На коробках с лампами пишут 2700K и называют это тёплым светом — это цвет вольфрамовой нити, привычный по всей квартире. Пламя свечи заметно теплее: около 1850-1900K, и это уже не «тёплый белый», а янтарно-оранжевый свет с почти нулевой синей составляющей. Разница на бумаге — восемьсот кельвинов, на глаз — пропасть: лампу 2700K в темноте я читаю как «свет», а 1900K — как «огонь». Для прибора, который горит в комнате, где человек пытается уснуть, эта разница важнее любой электроники: тёплый край шкалы не режет глаза даже на перепаде, а нейтральный белый в три часа ночи превращает спальню в операционную.
Кельвины на ленте переводятся в пропорцию каналов. Ламповые 2700K в уроке про ночник с режимами записаны как CRGB(90, 45, 15): зелёный вполовину красного, синий — треть. Для 1900K зелёного оставляю треть, а синему достаётся одна двадцать пятая: CRGB(255, 84, 10). Обратите внимание на направление: «теплее» на ленте — это не «больше красного», красный и так упёрся в потолок 255, — а меньше зелёного и синего. Я долго тупил в эту деталь, пока не собрал три ночника подряд и не увидел, что вся разница между оттенками тепла живёт в двух младших каналах.
Откуда брать такие числа и как они работают внутри, подробно разобрано в уроке про CRGB и fill_solid — здесь беру готовую пропорцию. Спорное из практики: 1900K при дневном свете выглядит «грязной», и гости просят «чуть посветлее». Лечится подъёмом всех трёх каналов с сохранением пропорции, а не заменой на белый. Хочется живости — добавьте подмигивание из урока про свечу, но на ночь я его выключаю: мерцание у кровати бодрит ровно тогда, когда нужен обратный эффект.
Плавный пуск: рампа вместо щелчка
Механика рампы умещается в две переменные: где яркость сейчас и куда она идёт. Цель — ноль или LEVEL, — переключается фронтом кнопки, а дальше каждый кадр двигает текущее значение к цели на два пункта. Потолок у меня 200 из 255, значит полный ход — сто кадров, и при кадре 25 миллисекунд выходит ровно 2,5 секунды: от тьмы до рабочего уровня свет доезжает сам, и глаз успевает за ним. Нажатие в середине рампы не ломает ничего — цель просто меняется, и свет доезжает туда, куда попросили: с 80 вверх до 200 или обратно к нулю.
Скорость подбирается под комнату: два пункта за кадр для спальни, шесть — для прихожей, где ночник работает ночным ориентиром и ждать три секунды некогда. Важная привычка — рампа ведёт значение в буфере, а не дёргает setBrightness(): глобальная яркость принадлежит всему проекту и может стоять на потолке для других сценариев, а здесь я масштабирую пропорцию 1900K текущим значением. Так же ведёт яркость ручка в уроке про плавный диммер — привычка переносится один в один, меняется только источник цели: не analogRead, а два числа.
Соблазн, который подстерегает на этом шаге, — нарастить яркость циклом с delay(10) внутри. Работать будет, но скетч на время рампы оглохнет: пока свет разгоняется, кнопка не опрашивается, и второй щелчок в этот интервал просто пропадает. Рампа на millis() держит loop() свободным: каждый проход опрашивает кнопку, двигает значение на шаг и отдаёт кадр — та же дисциплина, что в уроке про millis вместо delay. Кнопка при этом стоит на INPUT_PULLUP, и фронт ловится парой «прошлое состояние — текущее»: один клик — одно переключение цели.
#include <FastLED.h> #define LED_PIN 6#define BTN_PIN 2#define NUM_LEDS 16#define LEVEL 200 // потолок ночника из 255: ярче спальному свету не надо#define STEP 2 // пунктов за кадр: 200/2 * 25 мс = 2,5 с на полный ход CRGB leds[NUM_LEDS];int v = 0; // где яркость сейчасint target = 0; // куда идём: 0 или LEVELint prevBtn = HIGH;unsigned long lastFrame = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(150); // общий потолок проекта; рампа ведёт цвет в буфере FastLED.clear(); FastLED.show(); // гасим мусорный кадр, который лента успевает показать при подаче питания pinMode(BTN_PIN, INPUT_PULLUP); Serial.begin(115200);} void loop() { int btn = digitalRead(BTN_PIN); if (prevBtn == HIGH && btn == LOW) { // фронт нажатия: один клик — одно действие if (target == 0) { target = LEVEL; } else { target = 0; } } prevBtn = btn; if (millis() - lastFrame < 25) { return; } lastFrame = millis(); if (v < target) { v = v + STEP; if (v > target) { v = target; } } if (v > target) { v = v - STEP; if (v < target) { v = target; } } // пропорция 1900K: зелёный — треть красного, синий — одна двадцать пятая for (int i = 0; i < NUM_LEDS; i++) { leds[i] = CRGB(255L * v / LEVEL, 84L * v / LEVEL, 10L * v / LEVEL); } FastLED.show();}Уровень и шаг — две ручки, которые стоит подобрать под свою комнату, и подбор занимает вечер: LEVEL 120 с рассеивателем оказался моим рабочим потолком, но в комнате с тёмными стенами свет просил все двести. Шаг через два пункта — верхняя граница терпимого: при четырёх рампа уже ощущается как «доводка», при одном — полный ход растягивается до пяти секунд, и глаз теряет момент, когда свет готов. Пару «200 и два» я оставил как компромисс между мягкостью и нетерпением спящего.
Две строки в setup() легко пропустить глазами, а работают они громче всего: FastLED.clear() и первый show() гасят мусорный кадр, который лента успевает показать между подачей питания и первым проходом loop(). Без них ночник встречает включение в розетку случайной вспышкой — и вся плавность пуска выглядит обманом, потому что до рампы свет уже моргнул как попало. Дальше по коду всё по прологу: фронт переключает цель, кадр на 25 миллисекундах двигает v к ней, пропорция умножается на текущее значение. Заметьте 255L с длинным суффиксом: 255 × 200 на восьмибитной плате вылезает за signed int, и без L арифметика тихо портит цвет.
Про электрику коротко, потому что ночник — самый щадящий потребитель в разделе: шестнадцать пикселей в пропорции уголька на уровне 200 просят порядка 200-250 мА, и с общей яркостью 150 через setBrightness бюджет падает ещё вдвое. Телефонная зарядка на 1 А тянет такой ночник с тройным запасом, разговоры про блоки питания и инъекции остаются за большими проектами. Потолок при этом подобран по глазу: 200 в темноте хватает с избытком, и я держал бы 120, если бы не рассеиватель, съедающий треть.
Рампа в симуляторе
В симуляторе проверьте два сценария. Первый — полный ход: нажмите и считайте, до половины яркости свет доезжает за секунду с четвертью, и глаз уже различает комнату — самое полезное в ночнике происходит в первой половине рампы. Второй — щелчок посередине разгона: цель меняется на лету, свет плавно разворачивается и уходит обратно, ни одного рывка. Заодно обратите внимание на Serial: строки on/off печатаются в момент клика, а не в момент прибытия — событие и движение разделены, и это стоит держать в голове при отладке.
Уровень 200 из 255 — не святая цифра: в комнате без внешнего света хватает и 120, и разница в потреблении выходит вдвое. А вот рампу короче двух секунд делать не советую: при шаге больше пяти пунктов за кадр ход распадается на видимые ступени по 25 миллисекунд, и вместо «загорается» получается «мигнула и горит». Если захочется, чтобы ночник сам засыпал после получаса — каркас таймера уже есть в том же loop(), а память последнего режима и номера эффекта — тема урока про EEPROM: одна ячейка, и ночник просыпается таким, каким его выключили.
Кнопка против ручки — спор, который я закрыл на себе. У ночника на тумбочке нет места для потенциометра, и вслепую, не открывая глаз, крутилку не настроишь: рука промахивается, уровень уезжает мимо. Кнопка решает это одной позицией пальца, а единственный минус — два-три заготовленных уровня вместо плавного перебора — лечится добавлением ступеней: LEVEL уже константа, и четвёртая ступень — ещё одна строка в переключении цели. Потенциометр остаётся честным ответом там, где прибор стоит на столе и его крутят сознательно, при свете и с интересом.
| Источник света | Кельвины | Пропорция каналов на ленте |
|---|---|---|
| Свеча, тлеющий уголёк | 1850-1900K | 255 : 84 : 10 — синего почти нет, глаз читает «огонь» |
| Лампа накаливания | 2700-2800K | 90 : 45 : 15 — привычный «тёплый белый» из урока про ночник |
| Нейтральный офисный | 4000K | примерно 1 : 1 : 0,8 — спальному свету противопоказан |
| Дневной | 6500K | 1 : 1 : 1 — для ночника не берём вовсе |
- 1. Чем цвет 1900K отличается от ламповых 2700K на ленте?
- 2. Кадр 25 мс, шаг рампы 2 пункта, потолок 200. Сколько длится полный ход?
- 3. Почему рампа на delay(10) в цикле ломает ночник?
- 4. Кликнули посередине разгона вверх. Что делает скетч?
- 5. Зачем в setup() стоят FastLED.clear() и первый show()?
- 1900K на ленте — пропорция 255 : 84 : 10: «теплее» значит меньше зелёного и синего, а не больше красного
- Рампа — две переменные: текущее значение и цель; 2 пункта за кадр при 25 мс дают 2,5 секунды полного хода
- Яркость ведём в буфере, а не setBrightness(): глобальный потолок принадлежит всему проекту
- Фронт кнопки «прошлое — текущее» даёт один клик — одно действие; рампа на millis() держит кнопку живой
- FastLED.clear() и первый show() в setup() гасят мусорный кадр при подаче питания
Частые вопросы по теме урока
- Чем цвет 1900K отличается от ламповых 2700K на ленте?
- Красный и так упёрся в потолок 255, поэтому «теплее» — это меньше зелёного и синего. 1900K — цвет уголька, 2700K — нити лампы. Зелёного и синего меньше: пропорция 255 : 84 : 10 вместо 90 : 45 : 15
- Кадр 25 мс, шаг рампы 2 пункта, потолок 200. Сколько длится полный ход?
- 200 пунктов по 2 за кадр — 100 кадров, 100 × 25 мс = 2,5 с. Шаг и кадр — две ручки, которые задают темп. 2,5 секунды: 100 кадров по 25 мс
- Почему рампа на delay(10) в цикле ломает ночник?
- Пока крутится цикл с delay, loop() стоит: щелчок в этот интервал пропадает. Рампа на millis() держит скетч живым. Скетч глухнет: кнопка не опрашивается, пока свет разгоняется
- Кликнули посередине разгона вверх. Что делает скетч?
- Рампа движется к цели каждый кадр: цель сменилась — направление развернётся само, без рывков и сбросов. Меняет цель на лету: свет плавно разворачивается вниз
- Зачем в setup() стоят FastLED.clear() и первый show()?
- До первого show() буфер содержит мусор, и лента успевает его показать. Гасим до всего остального — пуск остаётся честным. Чтобы погасить мусорный кадр, который лента показывает при подаче питания