Давайте знакомится!
Меня зовут Андрей, и я учусь на 1-м курсе по направлению радиоэлектроники. В этой статье я поделюсь своим путём разработки очередной железки, с важной деталью: попытаться сделать не просто проект, а настоящий продукт, который не стыдно кому-нибудь подарить.
Так как статья получилась довольно объёмной, она будет состоять из 2-х частей: В первой части я расскажу, как появилась идея, какие я ставил перед собой цели, вместе разберёмся, как управлять адресной светодиодной лентой, изогнутой в сложную форму, и напишем несколько режимов анимации. Во второй части Вас ждёт рассказ о написании с нуля своего веб-интерфейса для управления вывеской, продумывании UX, решении возникающих трудностей и реализация своего способа хранения пресетов в памяти микроконтроллера.
Ну что, начинаем!
Как появилась идея?
Вот у вас есть любимые музыканты или, может быть, актеры? А вам не хотелось подарить им какую-нибудь памятную вещь? Мне иногда в голову приходят именно такие мысли.
Вот, например, лет в 12-14 я очень любил сериал «Стар против сил зла». После его просмотра я захотел как-то выразить своё восхищение мультиком его автору. Тогда я только недавно купил 3D-принтер и решил, что сделаю фигурку главной героини в 3D и распечатаю. Логично, что воплотить в жизнь задумку у меня не получилось, но зато я прокачал свои навыки в 3D-моделировании.
Так произошло и сейчас, только одна маленькая безумная идея переросла в нечто большее — не просто проект, а целый продукт, который хочется дальше развивать и улучшать.
Отмотаем плёнку на 2025. Весна. Я в 11 классе готовлюсь к сдаче ЕГЭ. Очень эмоционально сложное для меня время, силы на исходе, есть проблемки в жизни, и вот, слушая «Мою волну» по пути в подземку, я залипаю на трек «Антенны», а затем «Зима» и другие. Они как-то отзывались во мне, давали силы двигаться дальше, несмотря на усталость от постоянной учёбы 24/7. Именно так для себя я открыл такого исполнителя, как OMNIXX. С того времени я успел поступить в ВУЗ, проблем больше никаких нет, а треки до сих пор слушаю и подпеваю, пока еду в метро.
После сдачи ЕГЭ наступает лето. Время, когда нужно определиться, куда поступать, и хорошенько отдохнуть перед первым курсом. Но сидеть я на месте не люблю, мне нужна была электроника, какой-нибудь проект. Желательно, который я смогу довести до MVP, а то меня уже изрядно бесят проекты, которые лежат на полке и ждут, когда я к ним вернусь и доделаю.
Именно в этот момент я складываю идеальный пазл проекта в голове:
Мозгом должна быть ESP. Если использовать жёсткую логику (как я бы хотел), то вероятность неудачи и очередного недоделанного проекта стала бы 100%.
Проект должен иметь продуманный UI/UX, чтобы быть понятным для людей, не связанных с электроникой.
Было бы круто использовать адресную светодиодную ленту, потому что я с ней так и не работал.
И тут я понимаю, что самый лучший вариант — светодиодная вывеска с управлением по Wi-Fi & Bluetooth. Но что же писать на этой вывеске? А ответ не заставил себя долго ждать. Пишем OMNIXX. А потом, может, и получится ему как-нибудь подарить, ну или отправить СДЕКом.
Буквы и неон
Идея есть. Дело за малым - реализовать её. Для начала определимся с физической частью нашей вывески. В качестве ленты будем использовать гибкий неон. Важно, что бы он был именно адресным, ведь иначе мы не сможем сделать крутые эффекты. Но обычной светодиодной ленте с маркетплейса ещё нужно как-то придать форму. Решил, что самый простой и аккуратный способ - напечатать формы букв для неона с отверстиями для проводов, куда будет вставляться лента и держаться там даже без клея.
Сначала я думал написать текст без разрывов, одним куском. Пересмотрел кучу различным «неоновых» шрифтов, попытался нарисовать сам, но ни один из вариантов мне не понравился. Поэтому пришлось вручную вырисовывать форму всех букв, учитывая максимально возможный изгиб ленты. Так я пришел к следующим элементам: паз для ленты шириной 5мм, стенка толщиной 2мм и «юбка» 3мм, в целом, я её добавлял для красоты и дизайна, но это оказалось крайне удачным решением. Ведь именно на ней я сделал площадки для крепления букв к основе для вывески.

Напечатал одну букву, запихнул ленту — всё идеально, только изначальная высота паза для ленты оказалась недостаточной. Сбоку были видны вырезы в резине неона для проводов. Я увеличил высоту прямо до начала прозрачной части ленты. И такой вариант меня устроил. Отлично, с буквами разобрались, осталось только все их напечатать.
Но эти буквы надо к чему-то прикреплять. Для опытной модели, которая должна была использоваться для понимания общей картины проекта во время его разработки, я решил особо не заморачиваться и распечатал монтажные рейки все на том же 3д принтере. Так как единая рейка не умещалась на столе моего принтера, мне пришлось разделить её на 2 части. Способ крепления букв прост: на оборотной стороне монтажной рейки находятся пазы для гаек М3, которые удерживают их от проворачивания в момент затяжки винта М3. Не забыл я и про кабель менеджмент. Что бы провода не торчали во все стороны с обратной стороны, я сделал маленькие прорези для стяжек.


Начало работы над проектом
Когда физическая опытная модель готова, начинается самый интересный и долгий этап: программирование микроконтроллера. Делать что, не зная что, — сомнительная затея, поэтому я сначала поставил перед собой задачи. Я отучился в Московской школе программистов, где прошел курс объектно-ориентированного программирования. И знаете, мне очень это понравилось, поэтому я хотел не просто реализовать вывеску, но и сделать это красиво и правильно, по ООП-шному, с классами, методами, разбиением кода на файлы и составляющие, а не одним длинным main.cpp с обычными функциями и сотней глобальных переменных. Первый пункт нашего воображаемого ТЗ есть, что же ещё нужно? Ну как минимум различные режимы работы, ну и реализовать их настройку и переключение. Изначально я думал сделать мобильное приложение в конструкторе и управлять через него по Bluetooth, но быстро понял, что для MVP это слишком труднореализуемая цель, так что остановимся на WEB-странице. Ну и добавим мой самый важный пунктик: «сделать красиво». Я считаю, что устройство должно быть не только технологичным и навороченным, а ещё и приятным в использовании. Прямо как у Apple. Это означает, что нужно предусмотреть каждую мелочь, дать возможность пользователю настраивать почти всё, работа самой вывески должна быть максимально стабильной и отказоустойчивой, ну и, конечно же, всё должно быть стильно.
Получилось как-то так:
Реализовать различные режимы и эффекты подсветки
Сделать ВЕБ-страницу для управления
Написать красивый код по ООП стилистике
Продумать UX и сделать всё стильно
Зажигаем светодиоды
Прежде всего начнём с эффектов. Долго думая, какие вообще нужно добавлять, а остановился на вот таком вот базовом наборе:
FLASHING LIGHT — бегущий вправо блик
RAINBOW — радуга
COLOR — заливка цветом
GRADIENT — градиент
FILLIGN LINE — заполнение цветом, а потом такое же постепенное угасание
STROBE — стробоскоп
Каждый из режимов ещё должен по-своему настраивается, так что возможностей для полёта фантазии пользователя точно хватит.
Итак, что мы имеем:
адресную светодиодную ленту;
контроллер ESP8266, который будет управлять всем этим делом.
Для начала нужно сделать так, чтобы лента могла светиться. Тут мы сталкиваемся с первыми сложностями: нужно узнать тип этой ленты, чтобы подобрать подходящую библиотеку, и повысить логический уровень, ведь ESP работает на 3-вольтовой логике, а светодиодная лента питается от 5 В. Проблему согласования решим с помощью обычного транзистора, подключённого по вот такой схеме:

Схема есть, значит, пора время взять в руки паяльник и сделать первую версию контроллера: припаять к макетке Wemos 2 резистора, транзистор и немного перемычек и гребёнок для более удобного подключения.

С этим разобрались, теперь, проведя вскрытие маленького кусочка неона, определяем, что перед нами обычные WS2812. Для управления будем использовать распространённую библиотеку FastLED. Мы уже можем загрузить пример и увидеть мигание одного светодиода, но… это как-то мало, нам нужно больше!
А как рулить?
Взаимодействие с лентой максимально простое. У нас есть массив CRGB leds[NUM_LEDS], в котором хранятся цвета наших светодиодов. В setup мы инициализируем библиотеку, давая её понять, с какой лентой мы работаем, сколько у нас есть светодиодов и ещё по мелочи настроек, а уже в основной функции loop присваиваем значения цветов светодиодов и обновляем вывод. Всё просто!
void setup() { FastLED.addLeds<WS2812, DATA_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(60); FastLED.setCorrection(TypicalLEDStrip); FastLED.clear(); } void loop() { for (int indx = 0; indx < NUM_LEDS; indx++){ leds[indx].setHue(value); // можно так или [тип value - byte] leds[indx] = color; // или вот так [тип color - CRGB] } FastLED.show(); }
Уже зная это, можно написать кучу анимаций и эффектов, но есть одно но… Для контроллера все светодиоды идут последовательно друг за другом, а у нас нет. Конечно, есть стандартные функции для светодиодных матриц, но даже они нам не подойдут: светодиоды в буквах располагаются хаотично. Для заливки цветом это неважно, но для остальных эффектов последовательность зажигания светодиодов крайне важна. Что ж, придётся делать собственную матрицу. Для этого нужно разбить нашу надпись сеткой. Количество строк определим по наибольшему количеству светодиодов на одной вертикали, а ширину столбцов прикинем «на глаз», примерно по ширине свечения одного пикселя. А дальше идём с левого края и заполняем индексами светодиодов. Но что же делать с пустым местом?* «0» *туда не поставишь, ведь это индекс первого светодиода. Делаем все наоборот: ставим большое число, в моём случае я поставил 100, но можно и 200, и 999 и если такой индекс будет попадаться, то просто игнорируем его.

int order[18][4] = { //... // пример матрицы для буквы N. 100 - пустое пространство {49, 48, 57, 56}, {100, 100, 100, 55}, {51, 52, 53, 54}, //... }
Теперь, проходясь по строчкам такой матрицы, мы будем последовательно зажигать столбцы на нашей вывеске. Это позволяет нам реализовать нашу самую первую анимацию: бегущий блик.
Для этого мы будем проходить в цикле по всему нашему массиву order и присваивать нужным светодиодам из leds значение цвета color, а остальные очищать, т. е. «выключать». Не забудем, что между итерациями цикла нужна задержка (переменная speed), ведь скорость работы МК большая, и без неё наш глаз не успеет увидеть пробегающую полоску. Для большей вариативности сделаем ещё одну задержку, только уже между циклами нашей анимации. Обязательно делаем на millis(), чтобы не останавливать работу нашей программы. Это окажется критически важным дальше.
byte led_lines_number = 18; void flash_light(CRGB* leds, CRGB color, unsigned int speed, unsigned int cool_down) { static byte l = 0; // задержка мажду циклами анимации на время cool_down + время на 1 цикл if (millis() - time_stamp >= cool_down + speed*led_lines_number) { // задержка между сдвигами вертикальной полоски if (millis() - secondary_time_stamp >= speed) { // Если долши до конца, то обнуляем итератор L if (l == led_lines_number) { time_stamp = millis(); l = 0; } else { FastLED.clear(); // проходимся про строчке матрицы и зажигаем вертикальную линию на вывеске for (int indx = 0; indx < 4; indx++) { // игнорируем пустоту с индексом 100 if (order[l][indx] != 100) { leds[order[l][indx]] = color; } } l++; // двигаемся дальше secondary_time_stamp = millis(); } } } else { FastLED.clear(); // выключаем все светодиоды. } }
Ну вот и всё! Первая анимация готова! Не так уж и было сложно. На реализации каждой анимации я останавливаться не буду, иначе Вы точно уснёте, а вот про особенность регулировки яркости, которая учитывается у меня в проекте, расскажу.
Когда простое оказывается сложным
Давайте вместе реализуем ещё один простенький режим. Пусть это будет плавное мигание всей ленты выбранным цветом. На первый взгляд тут все тоже легко: в библиотеке яркость ленты задаётся значением от 0 до 255, просто в цикле присваиваем то возрастающее, то убывающее значений яркости значение и всё!
Но если мы будем линейно увеличивать яркость, то в какой-то момент нашему глазу покажется, что яркость как-то неравномерно увеличивается. И глаз будет прав! Дело в том, что он воспринимает яркость экспоненциально, а не линейно. Получается нам необходимо перевести линейные значения к экспоненциальным. На просторах интернета найдём уравнение вида:
где k - коэффициент гамма-коррекции, обычно принимается в диапазоне 2.0-3.0. Чтобы не вычислять каждый раз новое значение и не нагружать этим микроконтроллер, запишем значения, полученные для каждого уровня яркости от 0 до 255, в массив:
const uint8_t light_gamma[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, ..... 202, 205, 207, 209, 211, 214, 216, 218, 220, 223, 225, 226, 230, 231, 235, 236, 240, 241, 245, 246, 250, 251, 255, };
Теперь, если изменять яркость с гамма-коррекцией, то мы просто обращаемся к значению из массива, находящемуся по индексу, соответствующему желаемому уровню яркости.
Ну вот теперь наверное все? Нет, это до сих пор не конец.
Для понимания, чего нам ещё не хватает, предлагаю представить такую ситуацию: Пользователь настроил яркость вывески в темной комнате. Она не слепит, но и не светит тускло. Идеально, одним словом. Пусть этот уровень яркости будет равен 20 с учётом гамма-коррекции. Пользователь включает нашу анимацию плавной пульсации. Вывеска плавно потухла… «Красота!» — подумал пользователь. Затем она начала потихоньку светиться, потом ещё ярче, ещё, ещё и ещё, пока не дойдёт до уровня 255 и не ослепит пользователя в тёмной комнате.
Что бы такого не происходило, обязательно добавляем проверку, что яркость светодиодов не оказалось выше указанной пользователем (BRIGHT).
Предлагаю уже посмотреть на конечный вид режима. У меня реализована ещё возможность выбрать, как будет изменяться яркость: линейно, или в соответствии с гамма-коррекцией.
void falshing_color(CRGB* leds, CRGB color, unsigned int speed, bool by_gamma){ static short index = 0; // индекс яркости из массива гаммы (яркость от 0-255) static bool increasing = 0; // флаг, отвечающий за увеличение или уменьшение яркости // задержка между шагами уменьшения яркости if (millis() - time_stamp >= speed) { // определяем, как мы уменьшаем яркость if (by_gamma) { FastLED.setBrightness(light_gamma[index]); } else { FastLED.setBrightness(index); } // определение: увеличение или уменьшение индекса if (!increasing) { index--; if (index < 0) { increasing = !increasing; // меняем направление index = 0; } } else { index++; // проверка, что мы достигли нужного уровня. в обоих режимах if ((by_gamma && light_gamma[index] >= BRIGHT) || (!by_gamma && index >= BRIGHT) || index == 255) { increasing = !increasing; // меняем направление } } time_stamp = millis(); } }
Вот так мы вместе с вами разработали режим с нуля, и при этом обнаружили много занимательных мелочей, которые надо учитывать и не упускать из виду. Так я реализовывал и продумывал все 23 анимации, которые получились различными вариациями 6 основных режимов.
Фуф, вы там не устали? А ведь это только начало!
Рефакторинг — наше всё
Где-то на этом моменте в нашу историю врывается объектно-ориентированное программирование. Помните я о нём говорил в начале? Так вот, его время настало.
Для тех, кто не знает, вот краткий ответ ИИ, что же это такое:
ООП (Объектно-Ориентированное Программирование) — это подход к написанию программ, в котором код организован вокруг объектов и классов, а не вокруг отдельных функций и переменных.
Писать огромный проект в один файл — ужасная идея, так что самое время быстренько разбить текущий проект на файлы.
Давайте разберемся, как это работает на примере знакомых функций анимации. После рефакторинга структура проекта становится следующей:
src |-- animations | |-- animations.h | |-- animations.cpp |-- main.cpp
В файле .h инициализируем наши функции.
void flash_light(CRGB* leds, CRGB color, unsigned int speed, unsigned int cool_down); void falshing_color(CRGB* leds, CRGB color, unsigned int speed, bool by_gamma);
Именно его мы будем импортировать во все места, где он будет требоваться. А уже само определение функций будет в .cpp файле. Это нужно, чтобы не происходило переопределение при импорте. Ещё важно помнить про иерархию наследования и заранее продумывать, куда какой файл и с какими функциями, классами и переменными будет подключаться, чтобы не получить ошибку циклического импорта.
Итак, мы уже проделали большую часть работы:
сделали 3д модели и распечатали их, а потом собрали опытную версию вывески
спаяли контроллер для управления
сделали несколько анимаций
провели небольшой рефакторинг, что бы не запутаться во всем коде Но впереди еще много работы: нужно придумать, как управлять всеми режимами, чтобы это было удобно для пользователя.
Спасибо за прочтение! Увидимся в следующей части!
P.S. Так как все предметы, связанные с электроникой начинаются со 2-го курса, то я самоучкой изучаю схемотехнику, программирование и разводку печатных плат, поэтому какие-то решения и рассуждения могут быть нелогичными или неправильными. Так что если вы вдруг заметите ошибки или знаете как можно было лучше решить возникшие проблемы — буду рад любым комментариям!
m039
На самом деле не вижу в этом ничего страшного, просто это проблема в плохом структурировании, а не в том, что все в одном файле. Ведь можно в одном файле расположить все культурно, а можно и с ООП со множеством файлов сделать кашу. По мне, если нужно разбиение на файлы, то его стоит сделать, в противном случае по желанию.