Номинант на премию «БОМЖ-ПРОЕКТ 2020»

Введите описание картинки
Введите описание картинки

В этой части наконец-то будет завершено монументальное строительство сего щщедевра! Для тех, кто пропустил — вот первая и вторая части сей трилогии.

Скорее под кат, там куча картинок (и даже видосики)!

В прошлый раз целиком и полностью (до)разобрались с тем, каким же в итоге получился корпус светильника. В этот раз поговорим про электронную начинку.

  • Итак, были закуплены следующие детали: esp8266 (с переводом её в категорию маломобильной (читай «без ног»)), кнопочный тумблер а-ля «бабушкина радость», зарядник на 2А и USB-B разъём, чтобы простым и удобным кабелем от принтера запитать всё это великолепие. ESP8266 был куплен у ребят из Мытищ, цена за плату и доставка порадовали.

  • P.S. От разъёма в дальнейшем отказался, так как расстояние в комнате заказчика от розетки до светильника составляло порядка 4 метров, поэтому был спаян хороший медный провод (1 мм2) длиной в 5 метров от БП до разъёма на светильнике.

Первоначальный набор электроники
Первоначальный набор электроники
  • Лента была выбрана самая обычная WS2812B, на 5В, 60 диодов на метр, без какой-либо защиты (заказывал ещё давно вот тут, была самая демократичная цена)

Светодиодная лента ws2812B
Светодиодная лента ws2812B
  • На скорую руку спаял всё это дело, чтобы замерить потребление ленты на отрезке в 1 метр:

Времянка во всей своей красе. А ряженка тут добавлена от сглазу завистливого!
Времянка во всей своей красе. А ряженка тут добавлена от сглазу завистливого!
  • Потребление в красном цвете было около 1,3А на метр. При включении белого ток лез вверх до 3.5-4А. Для тех, кто в танке: внутри rgb-светодиода находится 3 (микро)светодиода, каждый из которых потребляет ток около 20мА. Все вместе они потребляют под 60мА — это для ситуации, когда вы врубаете на полную катушку белый цвет. БП на 2А брался исключительно для тестов, для работы самого светильника был приобретён БП на 5В/7А в красивом металлическом корпусе с перфорацией :) Весной в наличии «прям сейчас, а лучше вчера» это всё было лишь у ЧиПиДиПа, у них и купил. Там же захватил БП и на 5В/10А на случай, если надо будет (НЕТ, НУ А ВДРУГ?!) увеличить количество светодиодов.

Гори та лента сини..эээ..красным пламенем!
Гори та лента сини..эээ..красным пламенем!
  • На этой же ленте сразу были проверены некоторые из будущих эффектов светильника:

  • P.S.S со звуком на видео всё плохо, ощущение будто я в зоопарке это снимал. Поэтому предупреждаю: звук можно и не включать, получите усладу только для глаз!

  • Эффект старта светильника (из финального кода был убран по желанию эксплуататора заказчика):

  • Красный цвет:

  • Бегущая волна:

  • Шрапнель:

  • Переключение цвета (позже добавились все цвета, которые «каждый охотник желает…»):

  • Стробоскоп:

  • В этом месте была допущена ошибка: обман был скрыт в том, что я накинул корпус ПОВЕРХ ленты и решил, что всё супер гуд!

Ляпота!
Ляпота!
  • Однако, когда куски светодиодной ленты были приклеены на корпус, спаяны и светильник был повешена на стену, оказалось, что отражённый от стены свет через эпоксидку не пробивается, а только создаёт мощное световое пятно вокруг. Пришлось колхозить создавать второй слой светильника, о котором я говорил в прошлый раз (будем для удобства звать его Мелким) и переносить светодиодную ленту на него:

Не обращайте внимание на бандитскую пулю на правом крыле Мелкого - она была зашпаклёвана, а потом всё было покрашено в красивый белый цвет
Не обращайте внимание на бандитскую пулю на правом крыле Мелкого - она была зашпаклёвана, а потом всё было покрашено в красивый белый цвет
Маленький брат Большого брата
Маленький брат Большого брата
  • Теперь свет бил ровно так, как было надо (на основной корпус были установлены бобышки, а в Мелком просверлены отверстия под саморезы). После проклейки и пайки считаем количество светодиодов: 91шт (для вписывания в будущий код). Итого получилось полтора метра ленты:

В финале, повторюсь, задник был повторно выкрашен в белый цвет (а то начнётся щас…)
В финале, повторюсь, задник был повторно выкрашен в белый цвет (а то начнётся щас…)
Рождение птеродактиля
Рождение птеродактиля
  • Итак, шорты превращаются…

Введите описание картинки
Введите описание картинки
  • …превращаются шорты….

Введите описание картинки
Введите описание картинки
  • …в тыкву! Как видно из конструкции, снизу Мелкого был закреплён стандартный круглый разъём питания. Это сделано для того, чтобы в дальнейшем при смене блока питания (например, на такой) ничего не надо было менять в конструкции светильника. Сама плата была установлена в распор на короткие саморезы (всегда можно снять + по высоте есть доступ для кабеля microUSB любой толщины).

  • Теперь тестируем работу кода:

Факир был пьян…
Факир был пьян…

Навеяло:

Введите описание картинки
Введите описание картинки
  • Празднично вешаем светильник на засратую тестовую дверь. Вспоминаем, что забыли указать токен Blynk. Празднично снимаем светильник, заливаем обновлённую прошивку и снова вешаем светильник на стену, но уже чуть менее празднично. Что? Откуда в двери дыра? Ээээ…а дыра в двери возникла давным-давно. Правда-правда. И вообще, я художник, я так вижу!

P.S.S.S Как вы уже наверняка догадались, порядок подачи материала немного отличается от фактического течения всего процесса. Понятное дело, что сначала вы скачаете программу, выясните все необходимые значения токена, номера каналов, какие значения к вам прилетают и прочее. Но мы будем более творческими и объединим настройку на стороне мобилки в отдельную подглаву.

Погнали, всё будет офигенски!©

  • Теперь, пока светильник висит на своём месте (будь оно не ладно), можно быстренько настроить работу смартфона. Качаем приложение Blynk, запускаем (регаемся, получаем токен, указываем с какой платой работаем, ну вот это вот всё) и видим голое поле. Переходим в режим настройки и закидываем на рабочее поле все необходимые кнопки-шуршалки в рамках «бюджета» программы:

Если что, это скрин с телефона. Мало ли…
Если что, это скрин с телефона. Мало ли…
  • Переходим в режим настройки каждого элемента интерфейса:

Введите описание картинки
Введите описание картинки
  • Если залезть в «кишки» зеАБЫРВАЛГы, то там мы увидим следующее:

Введите описание картинки
Введите описание картинки
  • Настроим отправку массива с 3 аргументами (R, G, B) по нужному каналу (V4) и укажем диапазон от 0 до 255 (ШИМ-сигнал). По образу и подобию указываем номер канала для отправки управляющего сигнала от других кнопочек-ползунков.

  • Настройки яркости: номер канала(V1), диапазон (0-255)

Введите описание картинки
Введите описание картинки
  • Изменение скорости анимации: канал (V2), значение задержки в секундах (0-1023, но тут каждый может сам себе найти отрезок времени по душе)

Введите описание картинки
Введите описание картинки
  • Настройка кнопки питания: канал (V3), значение (0/1), режим включения кнопки (тумблер)

Введите описание картинки
Введите описание картинки
  • Настройка выпадающего списка режимов работы светильника

Введите описание картинки
Введите описание картинки
Введите описание картинки
Введите описание картинки
  • Настройка кнопки сохранения настроек (если хочется после выключения/включения наблюдать тот же эффект)

Введите описание картинки
Введите описание картинки
  • Получаем в итоге примерно такую картину Репина:

Введите описание картинки
Введите описание картинки
  • Как и полагается жлобам специалистам, из дефолтного набора баллов выбираем максимальное количество опций (и ещё 200 баллов осталось про запас):

Введите описание картинки
Введите описание картинки
  • Теперь загрузим код:

Код
#include "SPI.h"
#include "BlynkSimpleEsp8266.h"
#include "ESP8266WiFi.h"
#include "EEPROM.h"
#include "Adafruit_NeoPixel.h"

#define PIN                     D3
#define PIXELS_NUM              91
Adafruit_NeoPixel pxls = Adafruit_NeoPixel(PIXELS_NUM, PIN, NEO_GRB + NEO_KHZ800);
//==================================================================================================//
#define BLYNK_PRINT             Serial

#define RAINBOW_MODE            1
#define RANDOM_COLORS_MODE      2
#define COLOR_WHEEL_MODE        3
#define STROBE_MODE             4
#define SLOW_MOTION_MODE        5
#define CUSTOM_MODE             6

#define RED_VAL                 0
#define GREEN_VAL               1
#define BLUE_VAL                2
#define BRIGHTNESS_VAL          3
#define LED_EFFECT_VAL          4
#define BRIGHTNESS              127
#define R_MAX                   255
#define G_MAX                   255
#define B_MAX                   255
#define R_OFF                   0
#define G_OFF                   0
#define B_OFF                   0
#define NUMBER_OF_LED_COLORS    3

#define LED_EFFECT_NUM          CUSTOM_MODE
#define STROBE_COUNT            5

#define CHECKPOINT_EEPROM_BYTE  50
#define EEPROM_TIME_DELAY       1000
#define EEPROM_UPDATE_TIME      30000
#define FULL_EEPROM_ARRAY_SIZE  512
#define USER_VAL                1
//==================================================================================================//
byte DefaultSettings[] = {R_MAX, G_MAX, B_MAX, BRIGHTNESS, LED_EFFECT_NUM};
byte Settings[] = {0, 0, 0, 0, 0};
byte ColorWheelArr[PIXELS_NUM][NUMBER_OF_LED_COLORS];

bool EepromUpdateFlag = true;
uint32_t EepromSaveTimer = 0;

uint32_t LedEffectTimer;
uint8_t TemporaryEffectNumber;
uint16_t LedEffectTimeDelay = 25;
uint8_t RainbowCycle, WheelPos;
uint8_t HsvAngle = 0;

bool PowerButtonState = false;
bool AnimationReverseFlag = false;
bool RandomColorsState = true;

char auth[] = "***********************";
char ssid[] = "***********************";
char pass[] = "***********************";
//==================================================================================================//
void eeprom_starting_settings();
void rainbow_mode_func();
void random_colors_mode_func();
void color_wheel_mode_func();
void strobe_mode_func();
void slow_motion_mode_func();
void pixels_off();
void set_each_pixel_color_func(byte, byte, byte, byte, byte);
void set_all_pixels_color_func(byte, byte, byte, byte);
//==================================================================================================//
//==================================================================================================//
void setup() {
  Serial.begin(115200);
  Blynk.begin(auth, ssid, pass);
  pxls.begin();
  randomSeed(analogRead(A0));
  EEPROM.begin(FULL_EEPROM_ARRAY_SIZE);
  eeprom_starting_settings();
  TemporaryEffectNumber = Settings[LED_EFFECT_VAL];
  pixels_off();
}

void loop() {
  Blynk.run();
  if(Settings[LED_EFFECT_VAL] != CUSTOM_MODE && PowerButtonState){
    if(Settings[LED_EFFECT_VAL] == RAINBOW_MODE      ) rainbow_mode_func();       else
    if(Settings[LED_EFFECT_VAL] == RANDOM_COLORS_MODE) random_colors_mode_func(); else
    if(Settings[LED_EFFECT_VAL] == COLOR_WHEEL_MODE  ) color_wheel_mode_func();   else
    if(Settings[LED_EFFECT_VAL] == STROBE_MODE       ) strobe_mode_func();        else
    if(Settings[LED_EFFECT_VAL] == SLOW_MOTION_MODE  ) slow_motion_mode_func();
  }
}
//==================================================================================================//
//==================================================================================================//
void eeprom_starting_settings(){
  if (EEPROM.read(CHECKPOINT_EEPROM_BYTE) == USER_VAL) {
    for (int i = 0; i < (sizeof(Settings) / sizeof(Settings[0])); i++) {
      byte EepromCurrentValue = EEPROM.read(i);
      Settings[i] = EepromCurrentValue;
    }
  }
  else {
    for (int i = 0; i < (sizeof(Settings) / sizeof(Settings[0])); i++) {
      Settings[i] = DefaultSettings[i];
    }
  }
}
//==================================================================================================//
void set_each_pixel_color_func(byte WorkingMode, byte Brightness, byte R = Settings[RED_VAL], byte G = Settings[GREEN_VAL], byte B = Settings[BLUE_VAL]){
  for (int i = 0; i < PIXELS_NUM; i++) {
    if(WorkingMode == RAINBOW_MODE){
      WheelPos=((uint16_t)(i*256 / PIXELS_NUM) + RainbowCycle);
      if(WheelPos < 85) {                 R = WheelPos*3; G = 255 - R;    B = 0;         } else
      if(WheelPos < 170){WheelPos -= 85;  R = 255 - B;    G = 0;          B = WheelPos*3;} else
                        {WheelPos -= 170; R = 0;          G = WheelPos*3; B = 255 - G;   }
    }
    if(WorkingMode == RANDOM_COLORS_MODE){
      int8_t n = random(3);
      if(n == 0){R = 0;} else {R = random(255);}
      if(n == 1){G = 0;} else {G = random(255);}
      if(n == 2){B = 0;} else {B = random(255);}
    }
    if(WorkingMode == SLOW_MOTION_MODE){
      R = ColorWheelArr[i][RED_VAL];
      G = ColorWheelArr[i][GREEN_VAL];
      B = ColorWheelArr[i][BLUE_VAL];
    }
    pxls.setPixelColor(i,
                       pxls.Color(map(R, 0, 255, 0, Brightness),
                                  map(G, 0, 255, 0, Brightness),
                                  map(B, 0, 255, 0, Brightness)));
  }
}
//==================================================================================================//
void set_all_pixels_color_func(byte R = Settings[RED_VAL], byte G = Settings[GREEN_VAL], byte B = Settings[BLUE_VAL], byte Brightness = Settings[BRIGHTNESS_VAL]){
  pxls.fill(pxls.Color(map(R, 0, 255, 0, Brightness),
                       map(G, 0, 255, 0, Brightness),
                       map(B, 0, 255, 0, Brightness)));
  pxls.show();
}
//==================================================================================================//
void pixels_off(){
  set_each_pixel_color_func(CUSTOM_MODE, Settings[BRIGHTNESS_VAL], R_OFF, G_OFF, B_OFF);
  pxls.show();
}
//==================================================================================================//
void rainbow_mode_func(){
  if(millis() - LedEffectTimer > LedEffectTimeDelay){
    LedEffectTimer = millis();
    RainbowCycle++;
    set_each_pixel_color_func(RAINBOW_MODE, Settings[BRIGHTNESS_VAL]);
    pxls.show();
  }
}
//==================================================================================================//
void random_colors_mode_func(){
  if(millis() - LedEffectTimer > LedEffectTimeDelay){
    LedEffectTimer = millis();
    set_each_pixel_color_func(RANDOM_COLORS_MODE, Settings[BRIGHTNESS_VAL]);
    pxls.show();
  }
}
//==================================================================================================//
void color_wheel_mode_func(){
  if(millis() - LedEffectTimer > LedEffectTimeDelay*2){
    LedEffectTimer = millis();
    if(HsvAngle == 0  ){HsvAngle = 42; } else
    if(HsvAngle == 42 ){HsvAngle = 85; } else
    if(HsvAngle == 85 ){HsvAngle = 127;} else
    if(HsvAngle == 127){HsvAngle = 170;} else
    if(HsvAngle == 170){HsvAngle = 212;} else
    if(HsvAngle == 212){HsvAngle = 0;  }
    if(HsvAngle < 85)  {Settings[BLUE_VAL]  = 0; Settings[RED_VAL]   = HsvAngle*3; Settings[GREEN_VAL] = 255 - Settings[RED_VAL];  } else
    if(HsvAngle < 170) {Settings[GREEN_VAL] = 0; Settings[BLUE_VAL]  = HsvAngle*3; Settings[RED_VAL]   = 255 - Settings[BLUE_VAL]; } else
                       {Settings[RED_VAL]   = 0; Settings[GREEN_VAL] = HsvAngle*3; Settings[BLUE_VAL]  = 255 - Settings[GREEN_VAL];}
    set_all_pixels_color_func();
  }
}
//==================================================================================================//
void strobe_mode_func(){
  if(millis() - LedEffectTimer > LedEffectTimeDelay){
    LedEffectTimer = millis();
    for(int j = 0; j < STROBE_COUNT; j++) {
      set_all_pixels_color_func(R_MAX, G_MAX, B_MAX);
      delay(LedEffectTimeDelay / 2);
      set_all_pixels_color_func(R_OFF, G_OFF, B_OFF);
      delay(LedEffectTimeDelay / 2);
    }
  }
}
//==================================================================================================//
void slow_motion_mode_func(){
  if(millis() - LedEffectTimer > LedEffectTimeDelay){
    LedEffectTimer = millis();
    if(RandomColorsState){
      RandomColorsState = false;
      for (int i = 0; i < PIXELS_NUM; i++) {
        int8_t RandomNumber = random(3);
        if(RandomNumber == 0){ColorWheelArr[i][RED_VAL]   = 0;} else {ColorWheelArr[i][RED_VAL]   = random(255);}
        if(RandomNumber == 1){ColorWheelArr[i][GREEN_VAL] = 0;} else {ColorWheelArr[i][GREEN_VAL] = random(255);}
        if(RandomNumber == 2){ColorWheelArr[i][BLUE_VAL]  = 0;} else {ColorWheelArr[i][BLUE_VAL]  = random(255);}
      }
    }
    if(!AnimationReverseFlag){
      for(int BrightnessUp = 0; BrightnessUp <= Settings[BRIGHTNESS_VAL]; BrightnessUp++){
       if (BrightnessUp == Settings[BRIGHTNESS_VAL]) {AnimationReverseFlag = true;}
       set_each_pixel_color_func(SLOW_MOTION_MODE, BrightnessUp);
       pxls.show();
       delay(LedEffectTimeDelay / 2);
      }
    }
    else {
      for(int BrightnessDown = Settings[BRIGHTNESS_VAL]; BrightnessDown >= 0; BrightnessDown--){
       if (BrightnessDown == 0) {AnimationReverseFlag = false; RandomColorsState = true;}
       set_each_pixel_color_func(SLOW_MOTION_MODE, BrightnessDown);
       pxls.show();
       delay(LedEffectTimeDelay / 2);
      }
    }
  }
}
//==================================================================================================//
//==================================================================================================//
BLYNK_WRITE(V1) { // BRIGHTNESS HORIZONTAL SLIDER
  Settings[BRIGHTNESS_VAL] = param.asInt();
  if (PowerButtonState) {
    set_each_pixel_color_func(CUSTOM_MODE, Settings[BRIGHTNESS_VAL]);
    pxls.show();
  }
}
//==================================================================================================//
BLYNK_WRITE(V2) { // ANIMATION SPEED HORIZONTAL SLIDER
  LedEffectTimeDelay = param.asInt();
}
//==================================================================================================//
BLYNK_WRITE(V3) { // POWER BUTTON
  PowerButtonState = param.asInt();
  if (PowerButtonState) {
    Settings[LED_EFFECT_VAL] = TemporaryEffectNumber;
    if(Settings[LED_EFFECT_VAL] == CUSTOM_MODE){
      set_each_pixel_color_func(CUSTOM_MODE, Settings[BRIGHTNESS_VAL]);
      pxls.show();
    }
    else { LedEffectTimer = millis() - LedEffectTimeDelay;}
  }
  else {
    TemporaryEffectNumber = Settings[LED_EFFECT_VAL];
    Settings[LED_EFFECT_VAL] = CUSTOM_MODE;
    pixels_off();
  }
}
//==================================================================================================//
BLYNK_WRITE(V4) { // ZeRGBa
  Settings[RED_VAL]   = param[0].asInt();
  Settings[GREEN_VAL] = param[1].asInt();
  Settings[BLUE_VAL]  = param[2].asInt();
  if (PowerButtonState) {
    set_each_pixel_color_func(CUSTOM_MODE, Settings[BRIGHTNESS_VAL]);
    pxls.show();
  }
}
//==================================================================================================//
BLYNK_WRITE(V5) { // LED EFFECTS MENU
  switch (param.asInt()) {
    case 1: Settings[LED_EFFECT_VAL] = RAINBOW_MODE;       LedEffectTimer = millis() - LedEffectTimeDelay; break;
    case 2: Settings[LED_EFFECT_VAL] = RANDOM_COLORS_MODE; LedEffectTimer = millis() - LedEffectTimeDelay; break;
    case 3: Settings[LED_EFFECT_VAL] = COLOR_WHEEL_MODE;   LedEffectTimer = millis() - LedEffectTimeDelay; break;
    case 4: Settings[LED_EFFECT_VAL] = STROBE_MODE;        LedEffectTimer = millis() - LedEffectTimeDelay; break;
    case 5: Settings[LED_EFFECT_VAL] = SLOW_MOTION_MODE;   LedEffectTimer = millis() - LedEffectTimeDelay; break;
    case 6: Settings[LED_EFFECT_VAL] = CUSTOM_MODE;        LedEffectTimer = millis() - LedEffectTimeDelay; break;
  }
}
//==================================================================================================//
BLYNK_WRITE(V6) { // EEPROM WRITE
  int pinValue = param.asInt();
  if (pinValue && PowerButtonState && (millis() - EepromSaveTimer > EEPROM_TIME_DELAY)) {
    EepromSaveTimer = millis();
    EEPROM.write(CHECKPOINT_EEPROM_BYTE, 1);
    for (int i = 0; i < (sizeof(Settings) / sizeof(Settings[0])); i++) {
      byte EepromCurrentValue = Settings[i];
      EEPROM.write(i, EepromCurrentValue);
    }
    EEPROM.commit();
  }
}
//==================================================================================================//

  • Вот и всё, можно переходить к тестированию!

Вечер отличных историй…
Вечер отличных историй…
  • Ну и маленькое видео одного из эффектов:

P.S.S.S.S. Код сего девайса вы также найдёте тут: GitHub. Вдруг пригодится?!

Вот и сказочке конец. Спасибо за внимание!

Засим откланиваюсь. Ваш А.С.