Лампа с управлением жестами
Содержание
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, он должен работать лучше
- Помещать ультразвуковой дальномер выше к краю лампы: если он будет слишком глубоко внутри лампы — возможны ложные срабатывания и нестабильная работа

