Лампа с управлением жестами
Содержание
ToggleЗадача
- Разработать систему управления цветом и яркостью светодиодов при помощи жестов
- Режим постоянного цвета, цветовой теплоты, а также анимация огня
- Настройка цвета и яркости
- Включение выключение, хранение настроек в памяти
Базовые уроки
Подключение
Библиотеки
- 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
Код программы
#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, он должен работать лучше
- Помещать ультразвуковой дальномер выше к краю лампы: если он будет слишком глубоко внутри лампы – возможны ложные срабатывания и нестабильная работа