Неделю назад я наконец-то написал статью о сварганенной мной почти 3 года назад светодиодной подсветке на монитор типа Ambilight, на которую ушла только треть из купленной мной на Алишке пятиметровой ленты на ws2812b. За первые полгода использования этой подсветки я неторопливо размышлял над вопросом - куда же применить оставшиеся 3 с лишним метра, то есть 198 светодиодов? И в итоге придумал - сделать себе на стену информационное табло / бегущую строку с выводом текущего времени, а также названия текущего трека, если я прослушиваю музыку.
Зимой 2014 года я приступил к реализации этого замечательного проекта, впоследствии обогатив его новыми дополнительными функциями. Но обо всём по порядку!
Проектирование
Пиксельное разрешение
Первое, о чём нужно было подумать - разрешение этого табло. Так как я планировал выводить на него, в первую очередь, текстовую и числовую информацию, я сразу прикинул, каким должно быть разрешение, то есть, количество пикселей (светодиодов) по вертикали. Я начал прикидывать в уме изображение всех арабских цифр: для них полностью хватало высоты в 5 строк. С буквами, конечно, было посложнее, тем не менее все, как минимум, заглавные буквы русского и английского алфавитов также более менее умещались в этот формат, за исключением таких букв, как Ё и Й.
Так как у меня оставалось всего 198 светодиодов, нетрудно было подсчитать, что табло должно было получиться с разрешением 39x5 пикселей.
Управление
Так как у меня отсутствовал модуль Ethernet, да и программирование на голом C меня не особо-то и радовало, я решил оставить на Arduino лишь базовые функции - часы, яркость и способность удалённого управления каждым пикселем с компьютера по USB, а уже на компьютере написать ПО (или драйвер, если будет так угодно), которое бы устанавливало время, получало текущую песню и выводило любую информацию на табло. У такого подхода есть как минусы (зависимость работы дополнительных функций от компьютера), так и неоспоримые плюсы:
- Можно сделать удобный редактор шрифта и менять варианты начертания каждого символа "на лету", не перепрошивая каждый раз микроконтроллер.
- Можно придумать любое количество шрифтов / символов без ограничения встроенной памяти микроконтроллера (хотя надо очень постараться, чтобы для этого не хватило 32х килобайт Atmega328P)
- Более высокоуровневый язык программирования означает повышенную простоту и уменьшенное время разработки любых решений.
- Возможность выводить не только инфу, полученную из внешних источников, но и какие-либо данные с самого компьютера (не реализовал за ненадобностью).
Сборка
Картонная основа для табло
Начал я, естественно, с нарезки оставшейся ленты на 5 кусков по 39 светодиодов каждый, после чего начал готовить поверхность для их наклеивания. Так как табло должно было быть достаточно жёстким (но эксплуатироваться исключительно в помещении), я взял прямоугольный лист очень плотного картона и отрезал от него 2 полосы нужной ширины, после чего склеил их между собой в длину, приклеив на шов с обратной стороны 2 пластиковых карточки, которые жёстко скрепили эти две полоски между собой: получилась одна длинная картонная полоса из двух половинок, чуть длиннее, чем сами отрезки ленты.
Чтобы двухсторонний скотч 3M, на котором лента сидела по заводу, хорошо приклеился к получившейся картонной полосе, я обклеил всю её лицевую поверхность бумажными глянцевыми страницами каталога какого-то строймаркета из почтового ящика.
Наклеивание ленты
Получившуюся основу я разметил карандашом под подготовленные отрезки ленты таким образом, чтобы вертикальные расстояния между светодиодами соответствовали горизонтальным, после чего аккуратно наклеил полоски по начерченным линиям:
Коммутация
Естественно, всё должно было выглядеть цивильно, поэтому все провода я решил спрятать с обратной стороны изделия. Для этого я с обеих сторон каждого отрезка светодиодной ленты пробил по 3 отверстия для проводов (Земля, 5V и управление):
И начал последовательно продевать и припаивать соответствующие провода:
После этого я замотал места пайки с обеих сторон чёрной изолентой:
А с обратной стороны соединил между собой все группы силовых проводов, заизолировав соединения термоусадкой:
С управляющим проводом я пошёл на небольшую хитрость: дело в том, что ленты нужно было соединить последовательно, только вот в каком порядке? Чтобы сэкономить провод, можно было бы, конечно, просто соединять концы ленты то слева, то справа, но, в итоге, это бы вылилось в полнейший ад при написании прошивки, ведь для вывода определённого символа в определённую позицию табло нужно знать конкретные номера затрагиваемых этим символом светодиодов. А как быстро вычислить номера светодиодов на конкретной позиции, когда, например, на нечётных строках они считаются слева направо, на чётных - справа налево? Это куча лишних вычислений, вместо которых можно было бы пару лишних раз обновить содержимое всего табло.
Поэтому я выбрал другой способ - конец каждой полосы соединяется с началом каждой следующей. Для этого пришлось протянуть 4 длинных провода через всё изделие, но я использовал обычную витую пару пятой категории: у меня её столько, что не жалко совсем. Зато для адресации каждого нижестоящего светодиода теперь надо было просто добавить к номеру текущего 39 - количество светодиодов в одной строке табло. Вот и всё, никакого алгоритма, никаких сложных вычислений - отличное решение!
Для подключения табло к Arduino я использовал обычный МГТФ.
Само табло я, разумеется, решил запитать от компьютера, для чего я, также как и в статье про Эмбилайт на Arduino, выбрал толстые провода, чтобы на них минимально проседало напряжение, ведь табло должно располагаться в нескольких метрах от источника питания, а сам источник (Corsair AX860i) - с цифровым управлением и по пятивольтовой шине выдаёт ровно 5.00 вольт, так что любые потери на кабеле - это потери яркости готового устройства.
Все соединения силовых кабелей я решил сделать модульными, используя такие разъёмы "папа":
И такие разъёмы "мама":
Естественно, это дало возможность делать удлинители, тем самым подбирая длину кабеля в зависимости от места установки. Для подключения к компьютерному БП я использовал штыри, подходящие по диаметру к разъёму Molex.
Разумеется, сразу же захотелось временно подключить всё и проверить, работает ли. Для этого я быстро сварганил скетч, просто выводящий на табло разноцветные вертикальные линии (заодно и убедился, что не прогадал с адресацией):
Придание "товарного" вида
Так как табло всё ещё выглядело как чёрт знает что, ему нужно было придать товарный вид. И тут я не нашёл ничего лучше, кроме как нарезать полоски определённой ширины из белых листов бумаги формата A4 и заклеить ими всё межсветодиодное пространство на морде. Сначала вертикальные полосы:
А потом и горизонтальные:
С обратной стороны, которую при любом раскладе не должно быть видно, я заклеил всё широким малярным скотчем:
Получилось очень красиво и аккуратно, почти идеально белая лицевая панель с торчащими белыми светодиодами!
Система управления светодиодным табло
Аппаратная часть - Arduino
В качестве основных мозгов устройства, естественно, выступил микроконтроллер Arduino с родным шилдом и некоторым навесным:
Во-первых, нужен оригинальный шилд (или его точная китайская копия) с USB-COM преобразователем на Atmega16u2, т.к. более простые аналоги обычно не поддерживают высокую скорость передачи данных (у меня - 2 мегабода в секунду) или работают на ней с большим количеством ошибок. Также, такую скорость должен поддерживать сам контроллер USB вашего компьютера и его драйвер.
Дополнительное оборудование
Так как основная функция моего табло - это часы, оно должно работать постоянно, независимо от компьютера. Для этого необходим резервный источник питания, для которого отлично подошёл адаптер питания для смартфона - HTC E250. Его номинал - 5V 1A, но по факту он и полторашку выдаёт без проблем. Также этот адаптер сертифицирован по пятому классу энергоэффективности, что говорит о его низком нагреве и высоком КПД при работе.
Для переключения между питанием от компьютера и этим устройством служит обычная релюха, правда я решил ничего не паять и просто приклеил к самому шилду термоклеем и подключил обычный пятивольтовый модуль реле для Arduino.
Фоторезистор - датчик освещённости. Разумеется, необходима мгновенная автоматическая регулировка яркости изображения на табло - включили свет в комнате - яркость выше, ночью - ниже.
Пара конденсаторов - для того, чтобы в момент выключения компьютера и перещёлкивания реле питание на контроллере не пропадало и часы, соответственно, не сбивались. Действительно, помогает, даже без диодов.
Схему подключения приводить не буду: всё и так понятно по фото, а если нет - изучение скетча окончательно прояснит ситуацию.
Прошивка (скетч Arduino)
Специально для вас я прокомментировал почти все строки в скетче, чтобы любой мог разобраться в моём говнокоде гениальном алгоритме, но для начала небольшой брифинг для более лёгкого понимания кода.
FastLED
Как и в случае с Эмбилайтом, я решил использовать полюбившуюся мне библиотеку FastLED версии 3.1.0. Она работает так: есть массив leds[194] (195 светодиодов, первый - 0), в котором у каждого элемента есть поля r, g и b, которые отвечают за яркости цветовых каналов каждого светодиода на табло. Чтобы, например, покрасить третий светодиод четвёртой строки табло в зелёный цвет, нам надо сделать следующее:
- Определить его порядковый номер на табло. Это просто: третий светодиод в строке имеет номер 2, ведь отсчёт идёт с нуля. Так как он стоит на четвёртой строке - добавляем к его номеру 3 предыдущих строки по 39 светодиодов, то есть 39*3, получаем номер 119 - это и есть искомый нами светодиод (пиксель).
- Т.к. идеально зеленый цвет в представлении RGB это 0, 255, 0, то именно это мы и записываем в данный пиксель: leds[119].r=0; leds[119].g=255; leds[119].b=0;
- Вот и всё, осталось лишь передать на табло изменённую нами информацию, для чего мы используем метод FastLED.show();
Естественно, это лишь один из нескольких вариантов работы с этой библиотекой. Но в данном случае он отлично подходит - меняем данные в массиве leds[] и когда у нас готов следующий кадр - обновляем табло.
Алгоритм работы устройства
Режимы работы
Так как при отключённом компьютере табло превращается в обычные часы (хотя это ж Arduino, так что при должном умении оно может превращаться даже в элегантные шорты!), есть 2 основных режима работы:
- Автономный, в котором табло запитывается от слаботочного источника и, соответственно, работает с пониженным энергопотреблением. На экране - обычные часы.
- Режим вывода информации с компьютера - табло полностью управляется своим драйвером, самостоятельно регулируя только свою яркость, подстраиваясь под световую обстановку в помещении.
Ночной режим
Автономный режим, в свою очередь, делится на 2 подрежима:
- Дневной, когда в помещении светло и часы плавно меняют свой цвет каждую минуту, постоянно подстраивая свою яркость под уровень освещённости, чтобы показания легко читались, но и не слепили.
- Ночной, когда в помещении темно (например, когда я сплю или меня нет дома) и табло работает на минимальной яркости, чтобы не освещать комнату и не мешать нормально спать.
Скетч
Остальное, вроде, должно быть понятно по моим комментариям:
#include "FastLED.h" //Подключаем библиотеку FastLED #define NUM_LEDS 195 //Определяем количество светодиодов на табло #define DATA_PIN 10 //Пин, по которому происходит управление табло #define ROWS 5 //Количество строк табло #define COLS 39 //Количество столбцов табло #define pc5vPin 12 //Раньше на этом пине был мониторинг наличия питания по 5V шине от компа, сейчас не используется #define relayPin 2 //Пин, управляющий релюхой #define minRGB 2 //Минимальная яркость светодиодов в обычном режиме работы #define maxBRT 80 //Не используется #define colorOrder GRB//Порядок цветов для FastLED #define brightnessThreshold 8 //Пороговое значение, на которое должна измениться текущая яркость освещения с фоторезистора для изменения яркости табло #define brtRetry 45 //Количество перепроверок уровня яркости с фоторезистора, которые необходимо выполнить с положительным результатом для изменения яркости табло #define dimmLux 60 //Минимальное значение яркости с фоторезистора, начиная с которо и ниже будет включаться ночной режим. #define dR 2 //Яркость красного канала в ночном режиме #define dG 0 //Яркость зелёного канала в ночном режиме #define dB 0 //Яркость синего канала в ночном режиме CRGB leds[NUM_LEDS]; //Массив светодиодов для работы с FastLED int r=0,g=0,b=0,c,br,a=0,n=0,up,down,bt,lux,luxCurr,xStep; //Служебные переменные boolean ir=true,ig=true,ib=true,selfPowered,qt,first=true,dimmed; //Служебные переменные char ch; //Переменная для хранения полученного из буфера последовательного порта символа byte clrs[4]; //Т.к. цвет часов в автономном режиме плавно изменяется, яркости его каналов (красного, зелёного и синего) хранятся в этом массиве byte maxRGB[4];//Массив с макисмальными яркостями для каждого из каналов в автономном режиме (ограничивается, чтобы потребляемый ток табло не превысил ток, выдаваемый источником питания) unsigned long syncTime,syncedTime=00000000, Time, time,timeX; //Переменные для реализации алгоритма работы часов unsigned long k60=60000; //Количество миллисекунд в минуте - коэффициент для отладки. //Функция зажигает светодиод n цветом r,g,b (выключенный светодиод n это частный случай зажигания светодиода n цветом 0,0,0) void setLed(int n,int r,int g,int b) { leds[n].r=r; leds[n].g=g; leds[n].b=b; } //Функция выводит цифру d на позицию p цветом r,g,b. В ней запрограммированы все 10 арабских цифр. void putDigit(int p,int d,int r,int g,int b) { int x=7+(p*4); if (p>2) {x=x+2;} if (d==0) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } if (d==1) { setLed(x,0,0,0); setLed(x+1,r,g,b); setLed(x+2,0,0,0); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,0,0,0); x=x+COLS; setLed(x,0,0,0); setLed(x+1,r,g,b); setLed(x+2,0,0,0); x=x+COLS; setLed(x,0,0,0); setLed(x+1,r,g,b); setLed(x+2,0,0,0); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } if (d==2) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,0,0,0); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } if (d==3) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } if (d==4) { setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); } if (d==5) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,0,0,0); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } if (d==6) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,0,0,0); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } if (d==7) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); } if (d==8) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } if (d==9) { setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); x=x+COLS; setLed(x,0,0,0); setLed(x+1,0,0,0); setLed(x+2,r,g,b); x=x+COLS; setLed(x,r,g,b); setLed(x+1,r,g,b); setLed(x+2,r,g,b); } } //Функция выводит точки, разделяющие часы и минуты, цветом r,g,b. void putDots(int r, int g, int b){ setLed(58,r,g,b); setLed(136,r,g,b); } //функция получает следующий цвет для вывода времени в автономном режиме (немного отличается от предыдущего). void getColor() { xStep=1;//random(1,maxBRT/20+1); if (clrs[up]>=maxRGB[up]) { do {up=random(1,4);} while (clrs[up]>=maxRGB[up] || up==down); } clrs[up]=clrs[up]+xStep; if (clrs[down]<=minRGB) { do {down=random(1,4);} while (clrs[down]<=minRGB || up==down); } clrs[down]=clrs[down]-xStep; r=clrs[1]; g=clrs[2]; b=clrs[3]; } //Функция для проверки, включён ли компьютер (приходит ли с него питание). boolean pcIsOn() { if (analogRead(5)>=500) {return true;} else {return false;} } //Функция выводит текущее время в массив светодиодов leds[], используя вышеописанные функции PutDigit и putDots. void putTime() { for (int a=0; a<195; a++) { leds[a].r = 0; leds[a].g = 0; leds[a].b = 0; } putDigit(4,timeX % 10,r,g,b); putDigit(3,timeX % 60 / 10,r,g,b); putDigit(2,timeX/60 % 10,r,g,b); putDigit(1,timeX/60 / 10,r,g,b); putDots(r,g,b); } //Ну вы ж прекрасно знаете, что этот кусок кода выполнятеся один раз в жизни при включении устройства void setup() { //pinMode(pc5vPin,INPUT); //Раньше я мониторил, включён ли комп, через цифровой вход, но потом понял, что через аналоговый надёжнее. pinMode(relayPin,OUTPUT); //Устанавливаем пин релюхи как выходной. pinMode(13,OUTPUT); //Дополнительно будем светодиодом моргать на шилде digitalWrite(13,LOW); //Но щас его тушим if (pcIsOn()) { //Если включён комп selfPowered=false; //Это типо переменная, показывающая, в автономном ли мы режиме. Указываем, что в автономном. digitalWrite(relayPin,HIGH); //Переключаем релюху на питание от компа digitalWrite(13,HIGH); //Включаем светодиод на шилде } else { //Ну а если вырублен - делаем всё с точностью до наоборот selfPowered=true; digitalWrite(relayPin,LOW); digitalWrite(13,LOW); } FastLED.addLeds<WS2812B, DATA_PIN, colorOrder>(leds, NUM_LEDS); //Инициализируем библиотеку FastLED с заданными нами параметрами. Теперь массив leds - это данные о всех наших пикселях на табло. Serial.begin(2000000); //Инициализируем последовательный порт с символьной скоростью 2 мегабода //Serial.begin(9600); //Ну а это вариант для отладки for (int a=0; a<195; a++) { //Гасим все светодиоды leds[a].r = 0; leds[a].g = 0; leds[a].b = 0; } FastLED.show(); //Синхронизируем табло с массивом leds (обновляем информацию на нём в соответствии с содержимым массива) //Устанавливаем максимальные уровни яркости для каждого цвета в отдельности maxRGB[1]=76; maxRGB[2]=76; maxRGB[3]=76; randomSeed(analogRead(0));//Получаем текущее значение яркости и инициализируем им генератор случайных чисел while (clrs[1]+clrs[2]+clrs[3]!=80) { //Генерируем случайный цвет, но так, чтобы сумма каналов не равнялась 80... Я не помню, почему именно с таким условием. Напищите в комментах! clrs[1]=random(minRGB,maxRGB[1]+1); clrs[2]=random(minRGB,maxRGB[2]+1); clrs[3]=random(minRGB,maxRGB[3]+1); } //Переносим сгенерированный цвет в переменные r,g и b. r=clrs[1]; g=clrs[2]; b=clrs[3]; FastLED.setBrightness(255); //Устанавливаем максимально возможную яркость (диапазон для каждого канала 0-255) if (analogRead(0)<=dimmLux) {dimmed=true; r=dR;g=dG;b=dB;} else {dimmed=false;} //Если в помещении темно - делаем цвета как в ночном режиме } void loop() { //Основной цикл, выполняется бесконечно if (selfPowered) { //Если мы в автономном режиме luxCurr=analogRead(0); //Получаем текущую яркость в помещении с фоторезистора if (luxCurr<=dimmLux) { //Если яркость ниже заданного вначале значения (или равна ему) if (!dimmed) { //И при этом часы всё ещё не притушены dimmed=true; //Запоминаем, что теперь они притушены r=dR;g=dG;b=dB; //Записываем в текущий цвет часов значения для притушенного режима FastLED.setBrightness(255); //Устанавливаем яркость на максимум, чтобы эти значения соответствовали себе (иначе FastLED их пропорционально затемнит) putTime(); //Выводим в leds[] текущее время FastLED.setBrightness(255); //Ещё раз устанавливаем максимальную яркость... Я хз, зачем второй раз это делать, наверное, так стабильнее работает)))) FastLED.show(); //Синхронизируем табло с массивом leds } } else if (luxCurr > lux+brightnessThreshold || luxCurr < lux-brightnessThreshold) { //Если яркость в помещении выше порогового значения изменения яркости qt=true; //Так как это, возможно, была просто вспышка, и на самом деле яркость повышать не надо (для устранения мерцания), проверим её ещё brtRetry раз. //qt в данном случае - контрольная переменная, если в цикле яркость хоть раз вернётся обратно в установленные рамки - мы ничего не меняем for (int l=0;l<brtRetry;l++) {luxCurr=analogRead(0); if (luxCurr < lux+brightnessThreshold && luxCurr > lux-brightnessThreshold) {qt=false; break;}} if (qt) { //Если яркость действительно изменилась более чем на brightnessThreshold lux=luxCurr; //Запоминаем её текущий уровень //Если у нас при этом был ночной режим - ясен пень, вырубаем его и получаем новый цвет, т.к. старый заменен настройками ночного режима! if (dimmed) {getColor(); dimmed=false;} FastLED.setBrightness(lux/4+2); //Опять двойное выставление яркости putTime(); //Выводим время FastLED.setBrightness(lux/4+2);//Ну вы поняли FastLED.show(); //Обновляем инфу на таблЕ } } //Напоминаю, мы всё ещё в автономном режиме time=millis(); //Запоминаем, который час после включения с точностью до миллисекунды //Если устройство не вырубалось почти 50 суток, то millis() пойдёт сначала, предусматриваем это, чтобы ничего в итоге не сбилось. Теперь часы могут работать бесконечно)))) if (time<Time) { Time=k60 - (4294967295-Time); } //Если у нас наступила новая минута или мы вообще впервые включились if (time>Time+k60 || first){ first=false; //Всё, уже не впервые Time=time; //Запоминаем текущее время с момента включения в мс в переменную Time time=(time-syncTime+syncedTime) % 86400000; //Высчитываем из текущего времени с момента запуска устройства и того, что мы получили с компа, текущее время в мс timeX=time/k60; //и переводим его в минуты if (!dimmed) {getColor();} //Если у нас не ночной режим - обновляем текущий цвет (в ночном нельзя, т.к. часы ярко загорятся) putTime(); //Выводим текущее время FastLED.show(); //И обновляем табло } //Оппа, комп врубили!!!! Урааааа! if (pcIsOn()) { delay(1000); //Секунда на медитацию digitalWrite(relayPin,HIGH); //Переключаем релюху на питание от компа digitalWrite(13,HIGH); //Включаем светодиод на шилде selfPowered=false; //Откладываем у себя в памяти, что мы теперь не в автономном режиме FastLED.setBrightness(255); //Врубаем яркость на полную do { //Врубаем световое шоу на табло, пока не запустится винда и на последовательный порт не придут первые данные от драйвера в автозагрузке leds[random(195)].r=random(200); FastLED.show(); leds[random(195)].g=random(200); FastLED.show(); leds[random(195)].b=random(200); FastLED.show(); leds[random(195)].r=0; FastLED.show(); leds[random(195)].g=0; FastLED.show(); leds[random(195)].b=0; FastLED.show(); } while (Serial.available()<=3 && pcIsOn()); } } else { //А это если мы не в автономном режиме if (Serial.available()>0) { //Если на последовательный порт что-то пришло, т.е. в приёмном буфере есть данные bt=Serial.peek(); //Запоминаем в bt байтовое представление первого символа в буфере ch=Serial.read(); //А в ch - символьное, после чего этот символ удаляется из буфера if (n>0) { //Если мы в процессе получения определённого пикселя switch (n) { //В зависимости от текущей стадии получения пикселя (n), в bt будет либо его порядковый номер, либо один из трёх каналов его цвета case 4: //Если мы получаем какой-то пиксель, то первым делом идёт его порядковый номер... a=bt; //... который содержится в байтовом представлении текущего символа, т.е. в bt. Запоминаем его в переменную a. break; //выходим из оператора switch() case 3: //Дальше мы получаем красную составляющую цвета пикселя a leds[a].r=bt; //Устанавливаем пикселю a в массиве leds[] в красный канал значение bt. break; //выходим из свитча case 2: //То же самое с зелёным каналом leds[a].g=bt; break; case 1: //И с красным leds[a].b=bt; break; } n=n-1; //Каждый последующий символ мы уменьшаем n, таким образом 4 последовательынх байта передают нам инфу о номере пикселя (светодиода) и его цвете } else //Если n=0, то есть мы сейчас не в процессе получения какого-то пикселя, а просто ждём сигнала if (ch=='/') { //Это как раз символ начала последовательности получения пикселя. Он означает, что следующие 4 байта будут его описывать (см. выше) n=4; //Поэтому пишем в n четвёрку, так сказать, заводим счётчик } else if (ch=='!') { //Если мы получаем восклицательный знак - это сигнал к обновлению информации на табло FastLED.show(); //Что мы и делаем, получив его. } else if (ch=='*') { //Звёздочка - это сигнал получения времени для синхронизации while (Serial.available()<5) {digitalWrite(13,LOW);digitalWrite(13,HIGH);} //Мигаем светодиодом на шилде, пока в буфере не будет пяти байт, которые и представляют текущее время syncTime=millis(); //Получаем текущее время с момента включения устройства //Получаем текущее время и записываем его в syncedTime. syncedTime=Serial.read(); syncedTime=syncedTime*10+Serial.read(); syncedTime=syncedTime*10+Serial.read(); syncedTime=syncedTime*10+Serial.read(); syncedTime=syncedTime*10+Serial.read(); randomSeed(syncedTime); //Инициализируем им генератор случайных чисел syncedTime=syncedTime*1000; //И превращаем его из миллисекунд в секунды } } else { //Если у нас в буфере последовательного порта отсутствуют данные if (!pcIsOn()) { //Проверяем, не пропало ли питание с компа (может быть, его вырубили нафиг). Если таки вырубили: getColor(); //Получаем текущий цвет часов delay(1000); //Секунда молчания в память о взорванном Звездой Смерти Альдераане digitalWrite(relayPin,LOW); //Переходим на автономное питание digitalWrite(13,LOW); //При этом гася светодиод на шилде selfPowered=true; //Делаем переход в автономный режим официальным delay(1000); //Вспоминаем, что Кайло Рен сделал со своим отцом, и пребываем от этого в шоке одну секунду time=(millis()-syncTime+syncedTime) % 86400000; //Вычисляем текущее время в миллисекундах timeX=time/k60; //Переводя его в минуты putTime(); //Выводим в массив leds[] FastLED.show(); //И, оттуда, соответственно, на экран } else if (n==0) { //Также, если данных всё ещё нет и мы щас не ждём получения очередного пикселя - можем установить текущую яркость табло в соответствии с яркостью в комнате luxCurr=analogRead(0); //Получаем яркость в комнате if (luxCurr > lux+brightnessThreshold || luxCurr < lux-brightnessThreshold) { //Если она изменилась (выше был такой же алгоритм) qt=true; //Проверяем ещё 45 (или сколько там в brtRetry записано раз) for (int l=0;l<brtRetry;l++) {luxCurr=analogRead(0); if ((luxCurr < lux+brightnessThreshold && luxCurr > lux-brightnessThreshold) || Serial.available()>0) {qt=false; break;}} if (qt) { //И если, всё же, изменилась lux=luxCurr; //Запоминаем её FastLED.setBrightness(lux/4+2); //Устанавливаем текущую яркость по заданной формуле if (Serial.available()==0) {FastLED.show();} //И если на входе всё ещё нет данных - обновляем изображение на табло. //Эта проверка нужна, т.к. обновление табло - затратная в плане ресурсов операция и лишнее её выполнение увеличивает риск переполнения буфера последовательного порта. } } } } } }
Драйвер (управляющее ПО для компьютера)
Управляющую прогу я написал на Delphi с использованием библиотеки ComPort Library. По умолчанию она сидит в трее, но при разворачивании выглядит вот так:
Исходники выкладывать не буду, т.к. там всё написано кривовато и на скорую руку, под себя. К тому же я планирую перевести её функции полностью на Arduino, чтобы табло вообще не зависело от компьютера.
Как видите на скрине, в моей проге предусмотрен редактор начертаний всех символов, их можно перерисовывать на лету, причем не только буквы, но и разные пиктограммы:
Можно вывести любой текст или последовательность чисел. Узнаёте эту?
Предусмотрены не только строчные буквы, но и прописные:
Но основное его назначение - вывод текущего трека (Now Playing) с LastFM. Вот так это выглядит вживую (Сектор Газа):
Или вот вариант с зарубежным исполнителем:
Выводится эта бегущая строка каждые 30 секунд.
Также, при просмотре какого-нибудь фильма или сериала в Media Player Clssic - Home Cinema, на экран сначала выводится название видеофайла и путь к нему:
А потом текущее время, состояние воспроизведения и оставшееся время до конца файла:
При этом, разумеется, общая яркость табло снижается, чтобы не отвлекать зрителей от просмотра произведения.
Перспективы
Как я уже сказал, в перспективе я планирую не только отказаться от ПО на компьютере, перенеся все функции на сам микроконтроллер, но и добавить новые, такие, как вывод текущей температуры в помещении и на улице, влажности и т.д. используя замечательный датчик DHT22. Разумеется, я об этом обязательно расскажу!
Позже я планирую сделать аналогичное табло у себя в зале, только длиной оно будет метров 5. Для этого, естественно, понадобится много ленты, но я думаю, что при грамотном управлении и красивом исполнении (встраивании в стену) это табло / бегущая строка, будет очень круто смотреться, особенно с полной интеграцией в систему "умный дом", которую я также планирую внедрять и, конечно же, рассказывать обо всём вам, так что подписывайтесь на новые статьи, чтобы ничего не пропустить!
А, ещё у меня есть идея вывода на это табло бегущей строкой приходящих на Android уведомлений! Было бы чётко!
Заключение
Это очень крутая штука, возможно, даже круче моего предыдущего проекта на ws2812b! Возможности такого табло поистине безграничны, но, как обычно, всё упирается лишь в отсутствие свободного времени.
А вам понравился этот проект? Напишите своё мнение в комментариях, это действительно важно для меня!
Никогда не видела ничего подобного в качестве украшения интерьера. Смотрится очень эффектно и очень красивые переходы, в полутемной комнате вообще сияет! Крутая и полезная штука, услада моих глаз!))
Спасибо, дорогая!
КАК ЗАПИСАТЬ ВРЕМЯ
КАК ЗАПИСАТЬ ВРЕМЯ в ВАШЕ светодиодное табло ,
у меня время считает с 00-00
Ну так время у меня с компа синхронизируется. Смотрите с 338 строчки кода. Там можно вместо подгрузки с компа как-нибудь другим вариантом настраивать.
дело то в чем, прогу в ардуино нано я загрузил все работает ,
а Delphi у меня нет , поэтому у меня и нет синхронизации времени,
а как установить я не знаю ,я не силён в программировании,
спасибо за подсказку - попробую,поколдую с прогой!
всё нашёл ,спасибо!
я меняю при загрузке программы в 24 строке unsigned long syncTime,syncedTime=58000000-это 16-00 час
Отлично! Можно тупо кнопками сделать настройку времени же! Типа нажатие одной добавлет минуты, нажатие второй - часы! Также на базе этой переменной.
Вот отличный скетч нашёл на просторах интернета, всё просто и понятно!
Только нет русских букв , может подскажешь ?
#include
#include
#include //библиотеки для часов
// Adafruit_NeoMatrix example for single NeoPixel Shield.
// Scrolls 'Howdy' across the matrix in a portrait (vertical) orientation.
#include
#include
#include
#include //библиотеки для табло на ws2812b
#ifndef PSTR
#define PSTR // Make Arduino Due happy
#endif
DS1307 rtc(A4,A5);//ноги подключения часов
#define PIN 6 //выход управления на светодиоды
// MATRIX DECLARATION:
// Parameter 1 = width of NeoPixel matrix
// Parameter 2 = height of matrix
// Parameter 3 = pin number (most are valid)
// Parameter 4 = matrix layout flags, add together as needed:
// NEO_MATRIX_TOP, NEO_MATRIX_BOTTOM, NEO_MATRIX_LEFT, NEO_MATRIX_RIGHT:
// Position of the FIRST LED in the matrix; pick two, e.g.
// NEO_MATRIX_TOP + NEO_MATRIX_LEFT for the top-left corner.
// NEO_MATRIX_ROWS, NEO_MATRIX_COLUMNS: LEDs are arranged in horizontal
// rows or in vertical columns, respectively; pick one or the other.
// NEO_MATRIX_PROGRESSIVE, NEO_MATRIX_ZIGZAG: all rows/columns proceed
// in the same order, or alternate lines reverse direction; pick one.
//width-х-горизонталь
//height-у-вертикаль
//NEO_MATRIX_TOP-положение 1го пикселя в матрице (верх)
//NEO_MATRIX_BOTTOM-низ
//NEO_MATRIX_LEFT -слева
//NEO_MATRIX_RIGHT -справа
//NEO_MATRIX_ROWS -столбцы
//NEO_MATRIX_COLUMNS-строка
//NEO_MATRIX_PROGRESSIVE-поочередно друг за другом
//NEO_MATRIX_ZIGZAG -змейкой
// See example below for these values in action.
// Parameter 5 = pixel type flags, add together as needed:
// NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
// NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
// NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products)
// NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
// Example for NeoPixel Shield. In this application we'd like to use it
// as a 5x8 tall matrix, with the USB port positioned at the top of the
// Arduino. When held that way, the first pixel is at the top right, and
// lines are arranged in columns, progressive order. The shield uses
// 800 KHz (v2) pixels that expect GRB color data.
Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(20, 8, PIN,
NEO_MATRIX_BOTTOM + NEO_MATRIX_LEFT +
NEO_MATRIX_COLUMNS + NEO_MATRIX_PROGRESSIVE,
NEO_GRB + NEO_KHZ800);//20 кол,светодиодов по Х,а 8-по У
const uint16_t colors[] = {
matrix.Color(155, 0, 0),
matrix.Color(200, 100, 0),
matrix.Color(155, 155, 0), //цвета строк
matrix.Color(0, 155, 0),
matrix.Color( 0,155, 155),
matrix.Color(0, 0, 155) ,
matrix.Color(100, 0,155 ),
matrix.Color(140, 140, 140) };
void setup() {
matrix.begin();
matrix.setTextWrap(false);
matrix.setBrightness(100); //яркость от 0-255
matrix.setTextColor(colors[0]);
}
int x = matrix.width();
int pass = 0;
void loop() {
matrix.fillScreen(0);
matrix.setCursor(x, 0);
matrix.print(rtc.getTimeStr()); //вывод на табло времени
matrix.print(F(" PRIVET ! "));
matrix.print(rtc.getDateStr()); //вывод на табло даты
if(--x = 8) pass = 0;
matrix.setTextColor(colors[pass]);
}
matrix.show();
delay(80); //скорость движения букв
}
У меня табло пока нету. Но вообще неудивительно, что русского нет - это же надо каждую букву прорисовывать. Я в своей проге на компе собственный шрифт рисовал.
А ещё в начале сплошные инклуды, а что инклудится-то и не написано... Судя по описаниям инклудов, там используются какие-то библиотеки для часов и т.д. - вот в них и задавался шрифт. Судя по всему, только английский.