Зимой вставать в тёмной комнате на будильник — пытка: за секунду из сна в яркий свет, глазам больно, и тело не верит, что утро. Лампы-рассветы решают это за деньги, но суть у них простая: полчаса свет растёт от еле тёплого угля до дневной яркости, и мозг успевает проснуться до звонка. То же самое делает лента WS2812, которая уже висит у вас ночником: к ночнику с режимами на EEPROM добавляется один новый режим с таймером — и будильник готов. Первый мой вариант жил на цепочке delay и половине экрана: будил исправно, но ронял кнопку и часы; версия на millis-планировщике из этого урока работает второй сезон без правок.
Под рассветом я имею в виду две ручки, которые едут одновременно: яркость от нуля до потолка и температура цвета от глубокого красного к тёплому белому. Настоящее солнце на восходе светит именно так — низко и красно, потом выше и белее. Первый свой рассвет я сделал на одной яркости, и комната «вспыхнула» белым на середине подъёма — я проснулся ровно там, где надо было ещё дремать. С тех пор цвет едет вместе с яркостью всегда, и обе ручки держатся за одну ступень планировщика.
Разберём: как связаны цвет и яркость одной ступенью, почему 30 минут — это 60 шагов по 30 секунд и что делает кнопка. Плюс история про рассвет, который включался сам после моргания света, — она дала уроку главную ловушку и объясняет, почему запуск никогда не живёт в setup().
Тело рассвета: две ручки от одной ступени
Цвет удобнее всего вести через blend: blend(EMBER, NOON, t) возвращает смесь двух цветов, где t — доля второго от 0 до 255. EMBER — тлеющий уголь вроде CRGB(60, 6, 0), NOON — дневной тёплый белый CRGB(255, 190, 110). Промежуточные ступени дают честный восход: персиковый, потом соломенный, потом белый — механику смешивания соседних цветов трогали в уроке про градиенты RGB и CHSV. Новое здесь то, что t — не переменная эффекта, а ступень планировщика: рассвет не имеет собственного темпа, его темп задаёт расписание шагов. Заодно это решает конфликты эффектов: пока рассвет идёт, остальные сцены просто не рисуют — это тоже состояние, а не договорённость по кадрам.
Яркость едет той же ступенью, но через отдельный масштаб: c.nscale8(t) прижимает цвет к нулю на старте и отпускает к полной силе в конце. Если пустить цвет без масштаба, первые минуты будут тёмно-красной заливкой на полной яркости — по ощущению тускло, но под потолком. С масштабом первые ступени — еле видный уголь под потолком, и глаз спит дальше. Резкое начало ломает сон сильнее, чем резкий конец, поэтому самые первые двадцать минут я и проектирую как «почти ничего не происходит».
Почему 30 минут. В моих будильниках и обзорах заводских ламп рабочий коридор — 20-40 минут: короче, тело не успевает; длиннее — просыпаешься посреди подъёма и выключаешь руками. Я остановился на 30. В машинных единицах это 1800 секунд, и их удобно резать на 60 шагов по 30 секунд: каждый шаг — одна целочисленная ступень t = шаг * 255 / 59, без float и без накопления ошибок. Внутри шага лента не перерисовывается вовсе: планировщик будит её раз в полминуты, и между шагами плата занята чем угодно ещё.
#include <FastLED.h>#define LED_PIN 6#define NUM_LEDS 20#define STEP_MS 30000UL // 30 минут = 60 шагов по 30 секунд CRGB leds[NUM_LEDS];const CRGB EMBER = CRGB(60, 6, 0); // тлеющий угольconst CRGB NOON = CRGB(255, 190, 110); // дневной тёплый белый bool dawnActive = false;unsigned long dawnStart = 0;int prevBtn = HIGH; void startDawn() { dawnActive = true; dawnStart = millis(); Serial.println("dawn: start");} void cancelDawn() { dawnActive = false; FastLED.clear(); FastLED.show(); Serial.println("dawn: cancel");} void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(255); pinMode(2, INPUT_PULLUP); Serial.begin(115200); // Никакого startDawn() здесь: рассвет запускает событие, а не подача питания.} void loop() { int btn = digitalRead(2); if (prevBtn == HIGH && btn == LOW) { // по фронту: старт или отмена if (dawnActive) { cancelDawn(); } else { startDawn(); } } prevBtn = btn; if (!dawnActive) return; unsigned long elapsed = millis() - dawnStart; int step = elapsed / STEP_MS; // 0..59 целых ступеней if (step > 59) step = 59; int t = step * 255 / 59; // одна ручка на цвет и яркость CRGB c = blend(EMBER, NOON, t); // красный ползёт к белому c.nscale8(t); // яркость едет той же ступенью fill_solid(leds, NUM_LEDS, c); FastLED.show(); static int lastStep = -1; if (step != lastStep) { lastStep = step; Serial.print("dawn: step "); Serial.println(step); } if (step >= 59) { dawnActive = false; Serial.println("dawn: done"); }}Скелет скетча — состояние в двух полях: dawnActive и метка старта. startDawn() и cancelDawn() — единственные точки, где состояние меняется; кадр рассвета выводится из состояния, а не наоборот. Ступень считается делением elapsed / 30000 — ровно 60 целых значений; последний шаг прижимается к 59, чтобы t дошёл до полного 255 и рассвет закончился заявленным светом, а не 254-м байтом. Никакого delay между ступенями: между ними живёт кнопка и всё остальное, каркас тот же, что в уроке про millis вместо delay.
Кнопка на D2 читается по фронту — сравнением прошлого и текущего состояния, как в уроке про кнопку, листающую эффекты. Нажатие в покое запускает, нажатие в ходе отменяет: две команды на одном контакте, потому что у рассвета нет двух одновременных состояний. Отмена гасит ленту сразу, без спуска по ступеням обратно: обратный закат не нужен, человек уже проснулся. Для проверки «а как оно ночью» кнопки хватает; постоянную настройку времени подъёма ведут из часов, и об этом ниже. Дребезг контактов здесь не враг: дребезжащее нажатие повторит команду, но повторный старт лишь перезапишет метку, и цикл начнётся заново — для отладки даже удобно.
Про целые числа — деталь, которая на AVR не мелочь. На Uno int шестнадцатибитный: step * 255 при 59 даёт 15045 и влезает в диапазон до 32767, но стоит «улучшить» формулу до t = elapsed * 255 / 1800000 — произведение уедет за разрядность, и рассвет начнёт прыгать по ступеням. Порядок операций держит числа маленькими: сначала делите время на длину шага, потом масштабируете ступень. Переполнение millis() через 49,7 суток здесь не страшно: разность беззнаковых корректна и через границу, мы это считали в уроке про планировщик.
Планировщик ступеней и кнопка-отмена
Прогоните цикл и следите за Serial: ступени идут с равным шагом, но глаз видит нелинейность — первые «минуты» почти ничего не происходит, потому что уголь на четверти силы невидим. Это правильно: сон ломает не подъём, а скачок. Отмена в середине проверяется так же, как на плате: темнота наступает сразу, без обратного спуска. И обратите внимание на состояние dawnDone: рассвет завершён, лента остаётся гореть дневным — многие пользуются такой лампой как ночником до самого подъёма, и гасить свет за них — отдельное решение, а не побочный эффект.
Связка с часами превращает скетч в будильник: раз в минуту читаете rtc.now(), сверяете с временем подъёма, при совпадении вызываете тот же startDawn(). DS3231 умеет и настоящие будильники с прерыванием — механика разбиралась в уроке про будильник RTC, а пробуждение платы по ножке SQW — в уроке про SQW-прерывание; тогда Arduino спит до самого рассвета. Разделение ролей держите жёстким: часы только зажигают событие, всё остальное — у планировщика и его состояния. И время на плате берите именно из DS3231, а не из millis(): после перезагрузки счётчик начнёт с нуля, и «шесть утра» без часов рассвет не найдёт.
Шаг в тридцать секунд выбран не только ради ровного счёта. Между соседними ступенями яркость меняется на четыре-пять байт из 255, и глаз не видит сам переход — видит тренд за минуту. Значит, перерисовывать ленту чаще тридцати секунд бессмысленно: кадр по расписанию, а не по циклу. Планировщик раз в полминуты просыпается, делает один fill_solid, один show() на двадцать пикселей — около 0,6 мс — и снова спит до следующей ступени; остаток кадра принадлежит кнопке и часам.
| Ступень (время от старта) | t: 0-255 | Цвет и яркость на ленте | Что видит глаз |
|---|---|---|---|
| 0 — старт | 0 | Чёрный: nscale8 гасит всё | Комната ещё ночная |
| 15 — 7,5 минуты | 64 | Тлеющий уголь на четверти силы: около 27, 13, 6 | Еле тёплый отблеск под потолком |
| 40 — 20 минут | 172 | Тёплый полумрак: около 129, 87, 50 | Контур комнаты читается, сон рыхлый |
| 59 — 29,5 минуты | 255 | Полный тёплый белый 255, 190, 110 | Пора вставать: ярко, но без синего |
Про питание по-честному. Двадцать пикселей в белом на полной яркости — до ампера с небольшим, и этот пик рассвет держит только на последних ступенях. Ночнику на 60 пикселей я бы уже советовал отдельный блок 5 В и общую землю с платой — счёт по пикселям и потолкам вели в уроке про бюджет яркости. Деталь, которая экономит силу: потолок рассвета необязательно держать 255 — NOON на 200 из 255 выглядит почти так же, а силовая часть живёт спокойнее; у меня на 60 пикселях стоит 190. И потолок блока проверяйте на последних ступенях, а не по среднему: тестер в пике показывает правду, которую средние значения прячут.
И последняя мелочь быта: рассвет, который разбудил, никто не выключает — человек уходит на работу, и лента горит дневным до вечера. Лечится вторым порогом в том же планировщике: через N минут после завершения лента плавно съезжает в ночной режим ночника или гаснет. Уровень ведётся по чуть-чуть, два-три пункта за кадр, и уход света никто не замечает; мой ночник после рассвета сидит на угле 40 из 255 и гаснет сам через час — будить второй раз нечему.
- 1. Сколько шагов в рассвете на 30 минут при шаге 30 секунд?
- 2. Что делает blend(EMBER, NOON, t)?
- 3. Почему яркость ведут отдельным nscale8(t), а не только цветом?
- 4. Рассвет завершился, второго порога нет. Что будет с лентой?
- 5. Почему нельзя вызывать startDawn() в setup()?
- Рассвет — две ручки от одной ступени: blend(EMBER, NOON, t) на цвет и nscale8(t) на яркость
- 30 минут = 60 шагов по 30 секунд, t = шаг * 255 / 59 — целые числа, без float и накопления ошибок
- Рабочий коридор длительности 20-40 минут: короче тело не успевает, длиннее просыпаешься посреди подъёма
- Кнопка по фронту: в покое старт, в ходе отмена; обратного заката нет — темнота сразу
- Запуск — только из события: кнопка, будильник DS3231, команда; из setup() рассвет отыграется после каждой просадки питания
Частые вопросы по теме урока
- Сколько шагов в рассвете на 30 минут при шаге 30 секунд?
- 1800 секунд делим на 30-секундные шаги — ровно 60 ступеней. Каждая ступень — одно целочисленное значение t без float. 60
- Что делает blend(EMBER, NOON, t)?
- blend берёт два цвета и долю: при t = 64 примерно четверть NOON в EMBER. Рассвет — это движение t планировщиком от 0 до 255. Возвращает смесь двух цветов, где t — доля второго от 0 до 255
- Почему яркость ведут отдельным nscale8(t), а не только цветом?
- Без масштаба ранние ступени дают тёмно-красную заливку под потолком — сон ломается сразу. Масштаб той же ручкой прижимает и яркость: сначала еле видный уголь. Иначе первые ступени — тёмный цвет на полной яркости: красная заливка вместо угля
- Рассвет завершился, второго порога нет. Что будет с лентой?
- После последней ступени лента остаётся на NOON. Бытовое лечение — второй порог: через N минут плавный съезд в ночной режим или гашение. Будет гореть дневным светом до вечера
- Почему нельзя вызывать startDawn() в setup()?
- Моргание света или перенос ночника в розетку днём — и setup честно запустит солнце в пять утра. Запуск живёт в событиях, а при старте время сверяется с часами. После любой просадки питания рассвет отыграет заново: состояние не должно рождаться из включения