Skip to content Skip to main navigation Skip to footer

Лампа с управлением жестами

Задача


  • Разработать систему управления цветом и яркостью светодиодов при помощи жестов
  • Режим постоянного цвета, цветовой теплоты, а также анимация огня
  • Настройка цвета и яркости
  • Включение выключение, хранение настроек в памяти

Базовые уроки


Подключение


Библиотеки


  • VirtualButton – виртуальная кнопка
  • GRGB – управление RGB светодиодом, преобразование цвета
  • FastLED – управление адресной светодиодной лентой
  • EEManager – менеджер памяти, хранение настроек

Все библиотеки можно установить через менеджер библиотек Arduino IDE

Программа


Основная структура программы будет следующая:

  • Получить расстояние с дальномера
  • Отфильтровать значения от шумов: реакция на поднесение руки должна быть моментальной, а пока рука удерживается – не должно быть резких изменений сигнала. В то же время когда рука убирается – значение должно резко стать нулевым, чтобы не сдвигать установку
  • Подать расстояние в обработчик виртуальной кнопки VirtualButton – в ней реализована вся самая сложная логика обработки нажатий, удержания, счёт “кликов” и так далее
  • Описать режимы работы и цвет, хранить настройки

Примечание: здесь я использую библиотеку GRGB как генератор цвета, который в “сыром виде” передаётся в FastLED и отправляется на ленту. У FastLED есть свои инструменты для работы с цветом, но у меня была цель сделать проект совместимым с обычными RGB лентами и светодиодами (см. ниже) практически без изменения кода программы.

Полный скетч
#define HC_ECHO 2       // пин Echo
#define HC_TRIG 3       // пин Trig

#define LED_MAX_MA 1500 // ограничение тока ленты, ма
#define LED_PIN 13      // пин ленты
#define LED_NUM 50      // к-во светодиодов

#define VB_DEB 0        // отключаем антидребезг (он есть у фильтра)
#define VB_CLICK 900    // таймаут клика
#include <VirtualButton.h>
VButton gest;

#include <GRGB.h>
GRGB led;

#include <FastLED.h>
CRGB leds[LED_NUM];

// структура настроек
struct Data {
  bool state = 1;     // 0 выкл, 1 вкл
  byte mode = 0;      // 0 цвет, 1 теплота, 2 огонь
  byte bright[3] = {30, 30, 30};  // яркость
  byte value[3] = {0, 0, 0};      // параметр эффекта (цвет...)
};

Data data;

// менеджер памяти
#include <EEManager.h>
EEManager mem(data);

int prev_br;

void setup() {
  Serial.begin(115200);

  pinMode(HC_TRIG, OUTPUT); // trig выход
  pinMode(HC_ECHO, INPUT);  // echo вход

  // FastLED
  FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, LED_NUM);
  FastLED.setMaxPowerInVoltsAndMilliamps(5, LED_MAX_MA);
  FastLED.setBrightness(255);

  led.setBrightness(0);
  led.attach(setLED);
  led.setCRT(1);

  mem.begin(0, 'a');  // запуск и чтение настроек
  applyMode();        // применить режим
}

void loop() {
  mem.tick();   // менеджер памяти
  if (data.state && data.mode == 2) fireTick();   // анимация огня

  // таймер 50мс, опрос датчика и вся основная логика
  static uint32_t tmr;
  if (millis() - tmr >= 50) {
    tmr = millis();

    static uint32_t tout;   // таймаут настройки (удержание)
    static int offset_d;    // оффсеты для настроек
    static byte offset_v;

    int dist = getDist(HC_TRIG, HC_ECHO); // получаем расстояние
    dist = getFilterMedian(dist);         // медиана
    dist = getFilterSkip(dist);           // пропускающий фильтр
    int dist_f = getFilterExp(dist);      // усреднение

    gest.poll(dist);                      // расстояние > 0 - это клик

    // есть клики и прошло 2 секунды после настройки (удержание)
    if (gest.hasClicks() && millis() - tout > 2000) {
      switch (gest.clicks) {
        case 1:
          data.state = !data.state;  // вкл/выкл
          break;
        case 2:
          // если включена И меняем режим (0.. 2)
          if (data.state && ++data.mode >= 3) data.mode = 0;
          break;
      }
      applyMode();
    }

    // клик
    if (gest.click() && data.state) {
      pulse();  // мигнуть яркостью
    }

    // удержание (выполнится однократно)
    if (gest.held() && data.state) {
      pulse();  // мигнуть яркостью
      offset_d = dist_f;    // оффсет расстояния для дальнейшей настройки
      switch (gest.clicks) {
        case 0: offset_v = data.bright[data.mode]; break;   // оффсет яркости
        case 1: offset_v = data.value[data.mode]; break;    // оффсет значения
      }
    }

    // удержание (выполнится пока удерживается)
    if (gest.hold() && data.state) {
      tout = millis();
      // смещение текущей настройки как оффсет + (текущее расстояние - расстояние начала)
      int shift = constrain(offset_v + (dist_f - offset_d), 0, 255);
      
      // применяем
      switch (gest.clicks) {
        case 0: data.bright[data.mode] = shift; break;
        case 1: data.value[data.mode] = shift; break;
      }
      applyMode();
    }

  }
}

// получение расстояния с дальномера
#define HC_MAX_LEN 1000L  // макс. расстояние измерения, мм
int getDist(byte trig, byte echo) {
  digitalWrite(trig, HIGH);
  delayMicroseconds(10);
  digitalWrite(trig, LOW);

  // измеряем время ответного импульса
  uint32_t us = pulseIn(echo, HIGH, (HC_MAX_LEN * 2 * 1000 / 343));

  // считаем расстояние и возвращаем
  return (us * 343L / 2000);
}

// медианный фильтр
int getFilterMedian(int newVal) {
  static int buf[3];
  static byte count = 0;
  buf[count] = newVal;
  if (++count >= 3) count = 0;
  return (max(buf[0], buf[1]) == max(buf[1], buf[2])) ? max(buf[0], buf[2]) : max(buf[1], min(buf[0], buf[2]));
}

// пропускающий фильтр
#define FS_WINDOW 7   // количество измерений, в течение которого значение не будет меняться
#define FS_DIFF 80    // разница измерений, с которой начинается пропуск
int getFilterSkip(int val) {
  static int prev;
  static byte count;

  if (!prev && val) prev = val;   // предыдущее значение 0, а текущее нет. Обновляем предыдущее
  // позволит фильтру резко срабатывать на появление руки

  // разница больше указанной ИЛИ значение равно 0 (цель пропала)
  if (abs(prev - val) > FS_DIFF || !val) {
    count++;
    // счётчик потенциально неправильных измерений
    if (count > FS_WINDOW) {
      prev = val;
      count = 0;
    } else val = prev;
  } else count = 0;   // сброс счётчика
  prev = val;
  
  return val;
}

// экспоненциальный фильтр со сбросом снизу
#define ES_EXP 2L     // коэффициент плавности (больше - плавнее)
#define ES_MULT 16L   // мультипликатор повышения разрешения фильтра
int getFilterExp(int val) {
  static long filt;
  if (val) filt += (val * ES_MULT - filt) / ES_EXP;
  else filt = 0;  // если значение 0 - фильтр резко сбрасывается в 0
  // в нашем случае - чтобы применить заданную установку и не менять её вниз к нулю
  return filt / ES_MULT;
}

#define BR_STEP 4
void applyMode() {
  if (data.state) {
    switch (data.mode) {
      case 0: led.setWheel8(data.value[0]); break;
      case 1: led.setKelvin(data.value[1] * 28); break;
    }

    // плавная смена яркости при ВКЛЮЧЕНИИ и СМЕНЕ РЕЖИМА
    if (prev_br != data.bright[data.mode]) {
      int shift = prev_br > data.bright[data.mode] ? -BR_STEP : BR_STEP;
      while (abs(prev_br - data.bright[data.mode]) > BR_STEP) {
        prev_br += shift;
        led.setBrightness(prev_br);
        delay(10);
      }
      prev_br = data.bright[data.mode];
    }
  } else {
    // плавная смена яркости при ВЫКЛЮЧЕНИИ
    while (prev_br > 0) {
      prev_br -= BR_STEP;
      if (prev_br < 0) prev_br = 0;
      led.setBrightness(prev_br);
      delay(10);
    }
  }
  
  mem.update(); // обновить настройки
}

void setLED() {
  FastLED.showColor(CRGB(led.R, led.G, led.B));
}

// огненный эффект
void fireTick() {
  static uint32_t rnd_tmr, move_tmr;
  static int rnd_val, fil_val;
  
  // таймер 100мс, генерирует случайные значения
  if (millis() - rnd_tmr > 100) {
    rnd_tmr = millis();
    rnd_val = random(0, 13);
  }
  
  // таймер 20мс, плавно движется к rnd_val
  if (millis() - move_tmr > 20) {
    move_tmr = millis();
    // эксп фильтр, на выходе получится число 0..120
    fil_val += (rnd_val * 10 - fil_val) / 5;

    // преобразуем в яркость от 100 до 255
    int br = map(fil_val, 0, 120, 100, 255);

    // преобразуем в цвет как текущий цвет + (0.. 24)
    int hue = data.value[2] + fil_val / 5;
    led.setWheel8(hue, br);
  }
}

// подмигнуть яркостью
void pulse() {
  for (int i = prev_br; i < prev_br + 45; i += 3) {
    led.setBrightness(min(255, i));
    delay(10);
  }
  for (int i = prev_br + 45; i > prev_br; i -= 3) {
    led.setBrightness(min(255, i));
    delay(10);
  }
}

Версия с RGB модулем


В проекте мы уже используем библиотеку GRGB, поэтому для переделки на светодиод достаточно:

  • Удалить всё что связано с библиотекой FastLED
  • Отключить обработчик светодиода
  • Указать пины в GRGB
Windows
#define HC_ECHO 2       // пин Echo
#define HC_TRIG 3       // пин Trig

// светодиод
#define LED_R 9
#define LED_G 10
#define LED_B 11

#define VB_DEB 0        // отключаем антидребезг (он есть у фильтра)
#define VB_CLICK 900    // таймаут клика
#include <VirtualButton.h>
VButton gest;

#include <GRGB.h>
GRGB led(COMMON_CATHODE, 9, 10, 11);

// структура настроек
struct Data {
  bool state = 1;     // 0 выкл, 1 вкл
  byte mode = 0;      // 0 цвет, 1 теплота, 2 огонь
  byte bright[3] = {30, 30, 30};  // яркость
  byte value[3] = {0, 0, 0};      // параметр эффекта (цвет...)
};

Data data;

// менеджер памяти
#include <EEManager.h>
EEManager mem(data);

int prev_br;

void setup() {
  Serial.begin(115200);

  pinMode(HC_TRIG, OUTPUT); // trig выход
  pinMode(HC_ECHO, INPUT);  // echo вход

  led.setBrightness(0);
  led.setCRT(1);

  mem.begin(0, 'a');  // запуск и чтение настроек
  applyMode();        // применить режим
}

void loop() {
  mem.tick();   // менеджер памяти
  if (data.state && data.mode == 2) fireTick();   // анимация огня

  // таймер 50мс, опрос датчика и вся основная логика
  static uint32_t tmr;
  if (millis() - tmr >= 50) {
    tmr = millis();

    static uint32_t tout;   // таймаут настройки (удержание)
    static int offset_d;    // оффсеты для настроек
    static byte offset_v;

    int dist = getDist(HC_TRIG, HC_ECHO); // получаем расстояние
    dist = getFilterMedian(dist);         // медиана
    dist = getFilterSkip(dist);           // пропускающий фильтр
    int dist_f = getFilterExp(dist);      // усреднение

    gest.poll(dist);                      // расстояние > 0 - это клик

    // есть клики и прошло 2 секунды после настройки (удержание)
    if (gest.hasClicks() && millis() - tout > 2000) {
      switch (gest.clicks) {
        case 1:
          data.state = !data.state;  // вкл/выкл
          break;
        case 2:
          // если включена И меняем режим (0.. 2)
          if (data.state && ++data.mode >= 3) data.mode = 0;
          break;
      }
      applyMode();
    }

    // клик
    if (gest.click() && data.state) {
      pulse();  // мигнуть яркостью
    }

    // удержание (выполнится однократно)
    if (gest.held() && data.state) {
      pulse();  // мигнуть яркостью
      offset_d = dist_f;    // оффсет расстояния для дальнейшей настройки
      switch (gest.clicks) {
        case 0: offset_v = data.bright[data.mode]; break;   // оффсет яркости
        case 1: offset_v = data.value[data.mode]; break;    // оффсет значения
      }
    }

    // удержание (выполнится пока удерживается)
    if (gest.hold() && data.state) {
      tout = millis();
      // смещение текущей настройки как оффсет + (текущее расстояние - расстояние начала)
      int shift = constrain(offset_v + (dist_f - offset_d), 0, 255);
      
      // применяем
      switch (gest.clicks) {
        case 0: data.bright[data.mode] = shift; break;
        case 1: data.value[data.mode] = shift; break;
      }
      applyMode();
    }

  }
}

// получение расстояния с дальномера
#define HC_MAX_LEN 1000L  // макс. расстояние измерения, мм
int getDist(byte trig, byte echo) {
  digitalWrite(trig, HIGH);
  delayMicroseconds(10);
  digitalWrite(trig, LOW);

  // измеряем время ответного импульса
  uint32_t us = pulseIn(echo, HIGH, (HC_MAX_LEN * 2 * 1000 / 343));

  // считаем расстояние и возвращаем
  return (us * 343L / 2000);
}

// медианный фильтр
int getFilterMedian(int newVal) {
  static int buf[3];
  static byte count = 0;
  buf[count] = newVal;
  if (++count >= 3) count = 0;
  return (max(buf[0], buf[1]) == max(buf[1], buf[2])) ? max(buf[0], buf[2]) : max(buf[1], min(buf[0], buf[2]));
}

// пропускающий фильтр
#define FS_WINDOW 7   // количество измерений, в течение которого значение не будет меняться
#define FS_DIFF 80    // разница измерений, с которой начинается пропуск
int getFilterSkip(int val) {
  static int prev;
  static byte count;

  if (!prev && val) prev = val;   // предыдущее значение 0, а текущее нет. Обновляем предыдущее
  // позволит фильтру резко срабатывать на появление руки

  // разница больше указанной ИЛИ значение равно 0 (цель пропала)
  if (abs(prev - val) > FS_DIFF || !val) {
    count++;
    // счётчик потенциально неправильных измерений
    if (count > FS_WINDOW) {
      prev = val;
      count = 0;
    } else val = prev;
  } else count = 0;   // сброс счётчика
  prev = val;
  
  return val;
}

// экспоненциальный фильтр со сбросом снизу
#define ES_EXP 2L     // коэффициент плавности (больше - плавнее)
#define ES_MULT 16L   // мультипликатор повышения разрешения фильтра
int getFilterExp(int val) {
  static long filt;
  if (val) filt += (val * ES_MULT - filt) / ES_EXP;
  else filt = 0;  // если значение 0 - фильтр резко сбрасывается в 0
  // в нашем случае - чтобы применить заданную установку и не менять её вниз к нулю
  return filt / ES_MULT;
}

#define BR_STEP 4
void applyMode() {
  if (data.state) {
    switch (data.mode) {
      case 0: led.setWheel8(data.value[0]); break;
      case 1: led.setKelvin(data.value[1] * 28); break;
    }

    // плавная смена яркости при ВКЛЮЧЕНИИ и СМЕНЕ РЕЖИМА
    if (prev_br != data.bright[data.mode]) {
      int shift = prev_br > data.bright[data.mode] ? -BR_STEP : BR_STEP;
      while (abs(prev_br - data.bright[data.mode]) > BR_STEP) {
        prev_br += shift;
        led.setBrightness(prev_br);
        delay(10);
      }
      prev_br = data.bright[data.mode];
    }
  } else {
    // плавная смена яркости при ВЫКЛЮЧЕНИИ
    while (prev_br > 0) {
      prev_br -= BR_STEP;
      if (prev_br < 0) prev_br = 0;
      led.setBrightness(prev_br);
      delay(10);
    }
  }
  
  mem.update(); // обновить настройки
}


// огненный эффект
void fireTick() {
  static uint32_t rnd_tmr, move_tmr;
  static int rnd_val, fil_val;
  
  // таймер 100мс, генерирует случайные значения
  if (millis() - rnd_tmr > 100) {
    rnd_tmr = millis();
    rnd_val = random(0, 13);
  }
  
  // таймер 20мс, плавно движется к rnd_val
  if (millis() - move_tmr > 20) {
    move_tmr = millis();
    // эксп фильтр, на выходе получится число 0..120
    fil_val += (rnd_val * 10 - fil_val) / 5;

    // преобразуем в яркость от 100 до 255
    int br = map(fil_val, 0, 120, 100, 255);

    // преобразуем в цвет как текущий цвет + (0.. 24)
    int hue = data.value[2] + fil_val / 5;
    led.setWheel8(hue, br);
  }
}

// подмигнуть яркостью
void pulse() {
  for (int i = prev_br; i < prev_br + 45; i += 3) {
    led.setBrightness(min(255, i));
    delay(10);
  }
  for (int i = prev_br + 45; i > prev_br; i -= 3) {
    led.setBrightness(min(255, i));
    delay(10);
  }
}

Возможные доработки


  • Использовать лазерный дальномер VL6180/VL53L0X/VL53L1X, он должен работать лучше
  • Помещать ультразвуковой дальномер выше к краю лампы: если он будет слишком глубоко внутри лампы – возможны ложные срабатывания и нестабильная работа

Видео


Полезный пример?

Похожие примеры
Подписаться
Уведомить о
5 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии