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

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

Содержание

  • Задача
  • Базовые уроки
  • Подключение
  • Библиотеки
  • Программа
  • Версия с RGB модулем
  • Возможные доработки
  • Видео

Задача


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

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

Подключение


Библиотеки


  • 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;
 
voidsetup(){
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(); // применить режим
}
 
voidloop(){
mem.tick(); // менеджер памяти
if(data.state && data.mode == 2)fireTick(); // анимация огня
 
// таймер 50мс, опрос датчика и вся основная логика
staticuint32_t tmr;
if(millis() - tmr >= 50){
tmr = millis();
 
staticuint32_t tout; // таймаут настройки (удержание)
staticint 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 // макс. расстояние измерения, мм
intgetDist(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);
}
 
// медианный фильтр
intgetFilterMedian(int newVal){
staticint 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 // разница измерений, с которой начинается пропуск
intgetFilterSkip(int val){
staticint 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 // мультипликатор повышения разрешения фильтра
intgetFilterExp(int val){
staticlong filt;
if(val) filt += (val * ES_MULT - filt) / ES_EXP;
else filt = 0; // если значение 0 - фильтр резко сбрасывается в 0
// в нашем случае - чтобы применить заданную установку и не менять её вниз к нулю
return filt / ES_MULT;
}
 
#define BR_STEP 4
voidapplyMode(){
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(); // обновить настройки
}
 
voidsetLED(){
FastLED.showColor(CRGB(led.R, led.G, led.B));
}
 
// огненный эффект
voidfireTick(){
staticuint32_t rnd_tmr, move_tmr;
staticint 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);
}
}
 
// подмигнуть яркостью
voidpulse(){
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;
 
voidsetup(){
Serial.begin(115200);
 
pinMode(HC_TRIG, OUTPUT); // trig выход
pinMode(HC_ECHO, INPUT); // echo вход
 
led.setBrightness(0);
led.setCRT(1);
 
mem.begin(0, 'a'); // запуск и чтение настроек
applyMode(); // применить режим
}
 
voidloop(){
mem.tick(); // менеджер памяти
if(data.state && data.mode == 2)fireTick(); // анимация огня
 
// таймер 50мс, опрос датчика и вся основная логика
staticuint32_t tmr;
if(millis() - tmr >= 50){
tmr = millis();
 
staticuint32_t tout; // таймаут настройки (удержание)
staticint 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 // макс. расстояние измерения, мм
intgetDist(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);
}
 
// медианный фильтр
intgetFilterMedian(int newVal){
staticint 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 // разница измерений, с которой начинается пропуск
intgetFilterSkip(int val){
staticint 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 // мультипликатор повышения разрешения фильтра
intgetFilterExp(int val){
staticlong filt;
if(val) filt += (val * ES_MULT - filt) / ES_EXP;
else filt = 0; // если значение 0 - фильтр резко сбрасывается в 0
// в нашем случае - чтобы применить заданную установку и не менять её вниз к нулю
return filt / ES_MULT;
}
 
#define BR_STEP 4
voidapplyMode(){
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(); // обновить настройки
}
 
 
// огненный эффект
voidfireTick(){
staticuint32_t rnd_tmr, move_tmr;
staticint 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);
}
}
 
// подмигнуть яркостью
voidpulse(){
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, он должен работать лучше
  • Помещать ультразвуковой дальномер выше к краю лампы: если он будет слишком глубоко внутри лампы – возможны ложные срабатывания и нестабильная работа

Взято с сайта Лампа с управлением жестами - Arduino набор GyverKIT

https://kit.alexgyver.ru/tutorials/magic-lamp/