Skip to content

Лёгкая и очень функциональная библиотека для энкодера с кнопкой, энкодера или кнопки с Arduino

License

Notifications You must be signed in to change notification settings

GyverLibs/EncButton

Repository files navigation

latest PIO Foo Foo Foo

Foo

EncButton

⚠️⚠️⚠️
Новая версия v3 несовместима с предыдущими, смотри документацию, примеры и краткий гайд по миграции с v2 на v3!
⚠️⚠️⚠️

Лёгкая и очень функциональная библиотека для энкодера с кнопкой, энкодера или кнопки с Arduino

  • Кнопка
    • Обработка событий: нажатие, отпускание, клик, счётчик кликов, удержание, импульсное удержание, время удержания + предварительные клики для всех режимов
    • Программное подавление дребезга
    • Поддержка обработки двух одновременно нажимаемых кнопок как третьей кнопки
  • Энкодер
    • Обработка событий: обычный поворот, нажатый поворот, быстрый поворот
    • Поддержка четырёх типов инкрементальных энкодеров
    • Высокоточный алгоритм определения позиции
    • Буферизация в прерывании
  • Простое и понятное использование
  • Огромное количество возможностей и их комбинаций для разных сценариев использования даже одной кнопки
  • Виртуальный режим (например для работы с расширителем пинов)
  • Оптимизирована для работы в прерывании
  • Максимально быстрое чтение пинов для AVR, esp8266, esp32 (используется GyverIO)
  • Быстрые асинхронные алгоритмы опроса действий с кнопки и энкодера
  • Жёсткая оптимизация и небольшой вес во Flash и SRAM памяти: 5 байт SRAM (на экземпляр) и ~350 байт Flash на обработку кнопки

Примеры сценариев использования:

  • Несколько кликов - включение режима (по кол-ву кликов)
  • Несколько кликов + короткое удержание - ещё вариант включения режима (по кол-ву кликов)
  • Несколько кликов + удержание - постепенное изменение значения выбранной переменной (по кол-ву кликов)
  • Несколько кликов выбирают переменную, энкодер её изменяет
  • Изменение шага изменения переменной при вращении энкодера - например уменьшение при зажатой кнопке и увеличение при быстром вращении
  • Навигация по меню при вращении энкодера, изменение переменной при вращении зажатого энкодера
  • Полноценная навигация по меню при использовании двух кнопок (одновременное удержание для перехода на следующий уровень, одновременное нажатие для возврата на предыдущий)
  • И так далее

Совместимость

Совместима со всеми Arduino платформами (используются Arduino-функции)

Содержание

Установка

  • Для работы требуется библиотека GyverIO
  • Библиотеку можно найти по названию EncButton и установить через менеджер библиотек в:
    • Arduino IDE
    • Arduino IDE v2
    • PlatformIO
  • Скачать библиотеку .zip архивом для ручной установки:
    • Распаковать и положить в C:\Program Files (x86)\Arduino\libraries (Windows x64)
    • Распаковать и положить в C:\Program Files\Arduino\libraries (Windows x32)
    • Распаковать и положить в Документы/Arduino/libraries/
    • (Arduino IDE) автоматическая установка из .zip: Скетч/Подключить библиотеку/Добавить .ZIP библиотеку… и указать скачанный архив
  • Читай более подробную инструкцию по установке библиотек здесь

Обновление

  • Рекомендую всегда обновлять библиотеку: в новых версиях исправляются ошибки и баги, а также проводится оптимизация и добавляются новые фичи
  • Через менеджер библиотек IDE: найти библиотеку как при установке и нажать "Обновить"
  • Вручную: удалить папку со старой версией, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!

Информация

Энкодер

Тип энкодера

Библиотека поддерживает все 4 типа инкрементальных энкодеров, тип можно настроить при помощи setEncType(тип):

  • EB_STEP4_LOW - активный низкий сигнал (подтяжка к VCC). Полный период (4 фазы) за один щелчок. Установлен по умолчанию
  • EB_STEP4_HIGH - активный высокий сигнал (подтяжка к GND). Полный период (4 фазы) за один щелчок
  • EB_STEP2 - половина периода (2 фазы) за один щелчок
  • EB_STEP1 - четверть периода (1 фаза) за один щелчок, а также энкодеры без фиксации

diagram

Рекомендации

Для работы по сценарию "энкодер с кнопкой" рекомендую вот такие (ссылка, ссылка) круглые китайские модули с распаянными цепями антидребезга (имеют тип EB_STEP4_LOW по классификации выше):
scheme

Самостоятельно обвязать энкодер можно по следующей схеме (RC фильтры на каналы энкодера + подтяжка всех пинов к VCC):
scheme

Примечание: по умолчанию в библиотеке пины энкодера настроены на INPUT с расчётом на внешнюю подтяжку. Если у вас энкодер без подтяжки - можно использовать внутреннюю INPUT_PULLUP, указав это при инициализации энкодера (см. документацию ниже).

Кнопка

Уровень кнопки

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

  • HIGH - кнопка подключает VCC. Установлен по умолчанию в Virt-библиотеках
  • LOW - кнопка подключает GND. Установлен по умолчанию в основных библиотеках

scheme

Подтяжка пина

В схемах с микроконтроллерами чаще всего используется подключение кнопки к GND с подтяжкой пина к VCC. Подтяжка может быть внешней (режим пина нужно поставить INPUT) или внутренней (режим пина INPUT_PULLUP). В "реальных" проектах рекомендуется внешняя подтяжка, т.к. она менее подвержена помехам - у внутренней слишком высокое сопротивление.

Документация

Дефайны настроек

Объявлять до подключения библиотеки

// отключить поддержку pressFor/holdFor/stepFor и счётчик степов (экономит 2 байта оперативки)
#define EB_NO_FOR

// отключить обработчик событий attach (экономит 2 байта оперативки)
#define EB_NO_CALLBACK

// отключить счётчик энкодера [VirtEncoder, Encoder, EncButton] (экономит 4 байта оперативки)
#define EB_NO_COUNTER

// отключить буферизацию энкодера (экономит 2 байта оперативки)
#define EB_NO_BUFFER

/*
  Настройка таймаутов для всех классов
  - Заменяет таймауты константами, изменить их из программы (SetXxxTimeout()) будет нельзя
  - Настройка влияет на все объявленные в программе кнопки/энкодеры
  - Экономит 1 байт оперативки на объект за каждый таймаут
  - Показаны значения по умолчанию в мс
  - Значения не ограничены 4000мс, как при установке из программы (SetXxxTimeout())
*/
#define EB_DEB_TIME 50      // таймаут гашения дребезга кнопки (кнопка)
#define EB_CLICK_TIME 500   // таймаут ожидания кликов (кнопка)
#define EB_HOLD_TIME 600    // таймаут удержания (кнопка)
#define EB_STEP_TIME 200    // таймаут импульсного удержания (кнопка)
#define EB_FAST_TIME 30     // таймаут быстрого поворота (энкодер)

Классы

Как работать с документацией: EncButton начиная с версии 3.0 представляет собой несколько библиотек (классов) для различных сценариев использования, они друг друга наследуют для расширения функциональности. Таким образом библиотека представляет собой "луковицу", каждый слой которой имеет доступ к функциям нижних слоёв:

  • Базовые классы:
    • VirtButton - базовый класс виртуальной кнопки, обеспечивает все возможности кнопки
    • VirtEncoder - базовый класс виртуального энкодера, определяет факт и направление вращения энкодера
    • VirtEncButton - базовый класс виртуального энкодера с кнопкой, обеспечивает опрос энкодера с учётом кнопки, наследует VirtButton и VirtEncoder
  • Основные классы:
    • Button, ButtonT - класс кнопки, наследует VirtButton
    • Encoder, EncoderT - класс энкодера, наследует VirtEncoder
    • EncButton, EncButtonT - класс энкодера с кнопкой, наследует VirtEncButton, VirtButton, VirtEncoder

Таким образом для изучения всех доступных функций конкретной библиотеки нужно смотреть не только её, но и то что она наследует. Например для обработки кнопки при помощи Button нужно открыть ниже описание Button и VirtButton.

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

T-версии библиотек требуют указания пинов константами (цифрами). Номера пинов будут храниться в памяти программы, это ускоряет работу и делает код легче на 1 байт за каждый пин.

Примечание: #include <EncButton.h> подключает все инструменты библиотеки!

Таблица функций кнопки
VirtButton VirtEncButton Button EncButton
read
readBtn
tickRaw
setHoldTimeout
setStepTimeout
setClickTimeout
setDebTimeout
setBtnLevel
pressISR
reset
clear
skipEvents
attach
detach
press
release
click
pressing
hold
holding
step
hasClicks
getClicks
getSteps
releaseHold
releaseStep
releaseHoldStep
waiting
busy
action
timeout
pressFor
holdFor
stepFor
Таблица функций энкодера
VirtEncoder Encoder VirtEncButton EncButton
readEnc
initEnc
setEncReverse
setEncType
setEncISR
clear
turn
dir
tickRaw
pollEnc
counter
setFastTimeout
turnH
fast
right
left
rightH
leftH
action
timeout
attach
detach
VirtButton
// ================ НАСТРОЙКИ ================
// установить таймаут удержания, умолч. 600 (макс. 4000 мс)
void setHoldTimeout(uint16_t tout);

// установить таймаут импульсного удержания, умолч. 200 (макс. 4000 мс)
void setStepTimeout(uint16_t tout);

// установить таймаут ожидания кликов, умолч. 500 (макс. 4000 мс)
void setClickTimeout(uint16_t tout);

// установить таймаут антидребезга, умолч. 50 (макс. 255 мс)
void setDebTimeout(uint8_t tout);

// установить уровень кнопки (HIGH - кнопка замыкает VCC, LOW - замыкает GND)
// умолч. HIGH, то есть true - кнопка нажата
void setBtnLevel(bool level);

// подключить функцию-обработчик событий
void attach(void (*handler)());

// отключить функцию-обработчик событий
void detach();

// ================== СБРОС ==================
// сбросить системные флаги (принудительно закончить обработку)
void reset();

// принудительно сбросить флаги событий
void clear();

// игнорировать все события до отпускания кнопки
void skipEvents();

// ================ ОБРАБОТКА ================
// обработка кнопки значением
bool tick(bool s);

// обработка виртуальной кнопки как одновременное нажатие двух других кнопок
bool tick(VirtButton& b0, VirtButton& b1);

// кнопка нажата в прерывании кнопки
void pressISR();

// обработка кнопки без сброса событий и вызова коллбэка
bool tickRaw(bool s);

// ================== ОПРОС ==================
// кнопка нажата [событие]
bool press();
bool press(uint8_t clicks);

// кнопка отпущена (в любом случае) [событие]
bool release();
bool release(uint8_t clicks);

// клик по кнопке (отпущена без удержания) [событие]
bool click();
bool click(uint8_t clicks);

// кнопка зажата (между press() и release()) [состояние]
bool pressing();
bool pressing(uint8_t clicks);

// кнопка была удержана (больше таймаута) [событие]
bool hold();
bool hold(uint8_t clicks);

// кнопка удерживается (больше таймаута) [состояние]
bool holding();
bool holding(uint8_t clicks);

// импульсное удержание [событие]
bool step();
bool step(uint8_t clicks);

// зафиксировано несколько кликов [событие]
bool hasClicks();
bool hasClicks(uint8_t clicks);

// кнопка отпущена после удержания [событие]
bool releaseHold();
bool releaseHold(uint8_t clicks);

// кнопка отпущена после импульсного удержания [событие]
bool releaseStep();
bool releaseStep(uint8_t clicks);

// кнопка отпущена после удержания или импульсного удержания [событие]
bool releaseHoldStep();
bool releaseHoldStep(uint8_t clicks);

// получить количество кликов
uint8_t getClicks();

// получить количество степов
uint16_t getSteps();

// кнопка ожидает повторных кликов (между click() и hasClicks()) [состояние]
bool waiting();

// идёт обработка (между первым нажатием и после ожидания кликов) [состояние]
bool busy();

// было действие с кнопки, вернёт код события [событие]
uint16_t action();

// ================== ВРЕМЯ ==================
// после взаимодействия с кнопкой (или энкодером EncButton) прошло указанное время, мс [событие]
bool timeout(uint16_t ms);

// время, которое кнопка удерживается (с начала нажатия), мс
uint16_t pressFor();

// кнопка удерживается дольше чем (с начала нажатия), мс [состояние]
bool pressFor(uint16_t ms);

// время, которое кнопка удерживается (с начала удержания), мс
uint16_t holdFor();

// кнопка удерживается дольше чем (с начала удержания), мс [состояние]
bool holdFor(uint16_t ms);

// время, которое кнопка удерживается (с начала степа), мс
uint16_t stepFor();

// кнопка удерживается дольше чем (с начала степа), мс [состояние]
bool stepFor(uint16_t ms);
VirtEncoder
// ==================== НАСТРОЙКИ ====================
// инвертировать направление энкодера (умолч. 0)
void setEncReverse(bool rev);

// установить тип энкодера (EB_STEP4_LOW, EB_STEP4_HIGH, EB_STEP2, EB_STEP1)
void setEncType(uint8_t type);

// использовать обработку энкодера в прерывании
void setEncISR(bool use);

// инициализация энкодера
void initEnc(bool e0, bool e1);

// инициализация энкодера совмещённым значением
void initEnc(int8_t v);

// сбросить флаги событий
void clear();

// ====================== ОПРОС ======================
// был поворот [событие]
bool turn();

// направление энкодера (1 или -1) [состояние]
int8_t dir();

// счётчик
int32_t counter;

// ==================== ОБРАБОТКА ====================
// опросить энкодер в прерывании. Вернёт 1 или -1 при вращении, 0 при остановке
int8_t tickISR(bool e0, bool e1);
int8_t tickISR(int8_t state);

// опросить энкодер. Вернёт 1 или -1 при вращении, 0 при остановке
int8_t tick(bool e0, bool e1);
int8_t tick(int8_t state);
int8_t tick();  // сама обработка в прерывании

// опросить энкодер без сброса события поворота. Вернёт 1 или -1 при вращении, 0 при остановке
int8_t tickRaw(bool e0, bool e1);
int8_t tickRaw(int8_t state);
int8_t tickRaw();  // сама обработка в прерывании

// опросить энкодер без установки флагов на поворот (быстрее). Вернёт 1 или -1 при вращении, 0 при остановке
int8_t pollEnc(bool e0, bool e1);
int8_t pollEnc(int8_t state);
VirtEncButton
  • Доступны функции из VirtButton
  • Доступны функции из VirtEncoder
// ================== НАСТРОЙКИ ==================
// установить таймаут быстрого поворота, мс
void setFastTimeout(uint8_t tout);

// сбросить флаги энкодера и кнопки
void clear();

// ==================== ОПРОС ====================
// ЛЮБОЙ поворот энкодера [событие]
bool turn();

// нажатый поворот энкодера [событие]
bool turnH();

// быстрый поворот энкодера [состояние]
bool fast();

// ненажатый поворот направо [событие]
bool right();

// ненажатый поворот налево [событие]
bool left();

// нажатый поворот направо [событие]
bool rightH();

// нажатый поворот налево [событие]
bool leftH();

// было действие с кнопки или энкодера, вернёт код события [событие]
uint16_t action();

// ==================== ОБРАБОТКА ====================
// обработка в прерывании (только энкодер). Вернёт 0 в покое, 1 или -1 при повороте
int8_t tickISR(bool e0, bool e1);
int8_t tickISR(int8_t e01);

// обработка энкодера и кнопки
bool tick(bool e0, bool e1, bool btn);
bool tick(int8_t e01, bool btn);
bool tick(bool btn);  // энкодер в прерывании

// обработка энкодера и кнопки без сброса флагов и вызова коллбэка
bool tickRaw(bool e0, bool e1, bool btn);
bool tickRaw(int8_t e01, bool btn);
bool tickRaw(bool btn);  // энкодер в прерывании
Button
  • Доступны функции из VirtButton
  • Режим кнопки по умолчанию - LOW
Button;
Button(uint8_t pin);                // с указанием пина
Button(uint8_t npin, uint8_t mode); // + режим работы (умолч. INPUT_PULLUP)
Button(uint8_t npin, uint8_t mode, uint8_t btnLevel); // + уровень кнопки (умолч. LOW)
// указать пин и его режим работы
void init(uint8_t npin, uint8_t mode);

// прочитать текущее значение кнопки (без дебаунса) с учётом setBtnLevel
bool read();

// функция обработки, вызывать в loop
bool tick();

// обработка кнопки без сброса событий и вызова коллбэка
bool tickRaw();
ButtonT
  • Доступны функции из VirtButton
  • Режим кнопки по умолчанию - LOW
ButtonT<uint8_t pin>;                 // с указанием пина
ButtonT<uint8_t pin> (uint8_t mode);  // + режим работы (умолч. INPUT_PULLUP)
ButtonT<uint8_t pin> (uint8_t mode, uint8_t btnLevel); // + уровень кнопки (умолч. LOW)
// указать режим работы
void init(uint8_t mode);

// прочитать текущее значение кнопки (без дебаунса) с учётом setBtnLevel
bool read();

// функция обработки, вызывать в loop
bool tick();
Encoder
  • Доступны функции из VirtEncoder
Encoder;
Encoder(uint8_t encA, uint8_t encB);                // с указанием пинов
Encoder(uint8_t encA, uint8_t encB, uint8_t mode);  // + режим работы (умолч. INPUT)
// указать пины и их режим работы
void init(uint8_t encA, uint8_t encB, uint8_t mode);

// функция обработки для вызова в прерывании энкодера
int8_t tickISR();

// функция обработки для вызова в loop
int8_t tick();
EncoderT
  • Доступны функции из VirtEncoder
EncoderT<uint8_t encA, uint8_t encB>;                 // с указанием пинов
EncoderT<uint8_t encA, uint8_t encB> (uint8_t mode);  // + режим работы (умолч. INPUT)
// указать режим работы пинов
void init(uint8_t mode);

// функция обработки для вызова в прерывании энкодера
int8_t tickISR();

// функция обработки для вызова в loop
int8_t tick();
EncButton
  • Доступны функции из VirtButton
  • Доступны функции из VirtEncoder
  • Доступны функции из VirtEncButton
EncButton;

// настроить пины (энк, энк, кнопка)
EncButton(uint8_t encA, uint8_t encB, uint8_t btn);

// настроить пины (энк, энк, кнопка, pinmode энк, pinmode кнопка, уровень кнопки)
EncButton(uint8_t encA, uint8_t encB, uint8_t btn, uint8_t modeEnc = INPUT, uint8_t modeBtn = INPUT_PULLUP, uint8_t btnLevel = LOW);
// настроить пины (энк, энк, кнопка, pinmode энк, pinmode кнопка, уровень кнопки)
void init(uint8_t encA, uint8_t encB, uint8_t btn, uint8_t modeEnc = INPUT, uint8_t modeBtn = INPUT_PULLUP, uint8_t btnLevel = LOW);

// функция обработки для вызова в прерывании энкодера
int8_t tickISR();

// функция обработки, вызывать в loop
bool tick();

// прочитать значение кнопки с учётом setBtnLevel
bool readBtn();

// прочитать значение энкодера
int8_t readEnc();
EncButtonT
  • Доступны функции из VirtButton
  • Доступны функции из VirtEncoder
  • Доступны функции из VirtEncButton
// с указанием пинов
EncButtonT<uint8_t encA, uint8_t encB, uint8_t btn>;

// + режим работы пинов, уровень кнопки
EncButtonT<uint8_t encA, uint8_t encB, uint8_t btn> (uint8_t modeEnc = INPUT, uint8_t modeBtn = INPUT_PULLUP, uint8_t btnLevel = LOW);
// настроить режим работы пинов, уровень кнопки
void init(uint8_t modeEnc = INPUT, uint8_t modeBtn = INPUT_PULLUP, uint8_t btnLevel = LOW);

// функция обработки для вызова в прерывании энкодера
int8_t tickISR();

// функция обработки, вызывать в loop
bool tick();

// прочитать значение кнопки
bool readBtn();

// прочитать значение энкодера
int8_t readEnc();

Обработка и опрос

Во всех библиотеках есть общая функция обработки (тикер tick), которая получает текущий сигнал с кнопки и энкодера

  • Эту функцию нужно однократно вызывать в основном цикле программы (для виртуальных - с передачей значения)
  • Функция возвращает true при наступлении события (для энкодера - 1 или -1 при повороте, 0 при его отсутствии. Таким образом поворот в любую сторону расценивается как true)
  • Есть отдельные функции для вызова в прерывании, они имеют суффикс ISR, см. документацию ниже

Библиотека обрабатывает сигнал внутри этой функции, результат можно получить из функций опроса событий. Они бывают двух типов:

  • [событие] - функция вернёт true однократно при наступлении события. Сбросится после следующего вызова функции обработки (например клик, поворот энкодера)
  • [состояние] - функция возвращает true, пока активно это состояние (например кнопка удерживается)

Для простоты восприятия функцию обработки нужно размещать в начале цикла, а опросы делать ниже:

void loop() {
  btn.tick();   // опрос

  if (btn.click()) Serial.println("click"); // однократно выведет при клике
  if (btn.click()) Serial.println("click"); // тот же клик!
}

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

Несколько функций обработки

По очевидным причинам нельзя вызывать функцию обработки больше одного раза за цикл - каждый следующий вызов сбросит события от предыдущего и код будет работать некорректно. Вот так - нельзя:

// так нельзя
void loop() {
  btn.tick();
  if (btn.click()) ...

  // ....

  btn.tick();
  if (btn.hold()) ...
}

Если очень нужно попасть в глухой цикл и опрашивать там кнопку, то вот так - можно:

// так можно
void loop() {
  btn.tick();
  if (btn.click()) ...

  while (true) {
    btn.tick();
    if (btn.hold()) ...
    if (btn.click()) break;
  }
}

Если библиотека используется с подключенным обработчиком событий attach() (см. ниже), то можно вызывать tick() где угодно и сколько угодно раз, события будут обработаны в обработчике:

// так можно
void cb() {
  switch (btn.action()) {
    // ...
  }
}

void setup() {
  btn.attach(cb);
}

void loop() {
  btn.tick();
  // ...
  btn.tick();
  // ...
  btn.tick();
}

"Загруженная" программа

Библиотека EncButton - асинхронная: она не ждёт, пока закончится обработка кнопки, а позволяет программе выполняться дальше. Это означает, что для корректной работы библиотеки основной цикл программы должен выполняться как можно быстрее и не содержать задержек и других "глухих" циклов внутри себя. Для обеспечения правильной обработки кнопки не рекомендуется иметь в основном цикле задержки длительностью более 50-100 мс. Несколько советов:

  • Новичкам: изучить цикл уроков как написать скетч
    • Писать асинхронный код в loop()
    • Любую синхронную конструкцию на delay() можно сделать асинхронной при помощи millis()
    • Если в программе каждая итерация главного цикла выполняется дольше 50-100мс - в большинстве случаев программа написана неправильно, за исключением каких-то особых случаев
  • Подключить кнопку на аппаратное прерывание (см. ниже)
  • Избегать выполнения "тяжёлых" участков кода, пока идёт обработка кнопки, например поместив их в условие if (!button.busy()) { тяжёлый код }
  • Если оптимизировать основной цикл невозможно - вызывать тикер в другом "потоке" и использовать функцию-обработчик:
    • В прерывании таймера с периодом ~50мс или чаще
    • На другом ядре (например ESP32)
    • В другом таске FreeRTOS
    • Внутри yield() (внутри delay())

Раздельная обработка

Имеет смысл только при ручном опросе событий! При подключенной функции-обработчике достаточно вызывать обычный tick() между тяжёлыми участками программы

Также в загруженной программе можно разделить обработку и сброс событий: вместо tick() использовать tickRaw() между тяжёлыми участками кода и ручной сброс clear(). Порядок следующий:

  • Опросить действия (click, press, turn...)
  • Вызвать clear()
  • Вызывать tickRaw() между тяжёлыми участками кода
void loop() {
  if (btn.click()) ...
  if (btn.press()) ...
  if (btn.step()) ...

  btn.clear();

  // ...
  btn.tickRaw();
  // ...
  btn.tickRaw();
  // ...
  btn.tickRaw();
  // ...
}

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

В этом сценарии буферизация энкодера в прерывании не работает и не обрабатываются все события releaseXxx

Обработка внутри delay

Если сложно избавиться от delay() внутри главного цикла программы, то на некоторых платформах можно поместить свой код внутри него. Таким образом можно получить даже обработку энкодера в цикле с дилеями без использования прерываний:

// вставка кода в delay
void yield() {
  eb.tickRaw();
}

void loop() {
  if (eb.click()) ...
  if (btn.turn()) ...

  eb.clear();

  // ...
  delay(10);
  // ...
  delay(50);
  // ...
}

В этом сценарии буферизация энкодера в прерывании не работает и не обрабатываются все события releaseXxx

Обработка кнопки

Библиотека обрабатывает кнопку следующим образом:

  • Нажатие с программным подавлением дребезга (удержание дольше таймаута deb), результат - событие press, состояния pressing и busy
  • Удержание дольше таймаута удержания hold - событие hold, состояние holding
  • Удержание дольше таймаута удержания hold + таймаута степ - импульсное событие step, срабатывает с периодом step пока кнопка удерживается
  • Отпускание кнопки, результат - событие release, снятие состояний pressing и holding
    • Отпускание до таймаута удержания - событие click
    • Отпускание после удержания - событие releaseHold
    • Отпускание после импульсного удержания - событие releaseStep
    • События releaseHold и releaseStep взаимоисключающие, если кнопка была удержана до step - releaseHold уже не сработает
  • Ожидание нового клика в течение таймаута click, состояние waiting
  • Если нового клика нет - снятие состоятия busy, обработка закончена
    • Если кнопка снова нажата - обработка нового клика
    • Счётчик кликов getClicks() сбрасывается после событий releaseHold/releaseStep, которые проверяют предварительные клики. В общем обработчике action() это события EB_REL_HOLD_C или EB_REL_STEP_C
    • Количество сделанных кликов нужно проверять по событию hasClicks, а также можно опросить внутри почти всех событий кнопки, которые идут до releaseXxx
  • Если ожидается timeout - событие timeout с указанным периодом от текущего момента
  • Обработка кнопки в прерывании сообщает библиотеке о факте нажатия, вся остальная обработка выполняется штатно в tick()

Отличие click(n) от hasClicks(n): click(n) вернёт true в любом случае при совпадении количества кликов, даже если будет сделано больше кликов. hasClicks(n) вернёт true только в том случае, если было сделано ровно указанное количество кликов и больше кликов не было!

Лучше один раз увидеть, чем сто раз прочитать. Запусти пример demo и понажимай на кнопку, или попробуй онлайн-симуляцию в Wokwi

Click

click

Hold

hold

Step

step

Онлайн-симуляция доступна здесь

Обработка энкодера

  • "Быстрым" поворотом считается поворот, совершённый менее чем за настроенный таймаут от предыдущего поворота
  • Обработанные в прерывании повороты становятся активными (вызывают события) после вызова tick()
  • Доступ к счётчику энкодера counter - это публичная переменная класса, можно делать с ней всё что угодно:
Serial.println(eb.counter); // читать
eb.counter += 1234;         // менять
eb.counter = 0;             // обнулять

Обработка энкодера с кнопкой

  • Поворот энкодера при зажатой кнопке снимает и блокирует все последующие события и клики, за исключением события release. Состояния нажатой кнопки не изменяются
  • Поворот энкодера также влияет на системный таймаут (функция timeout()) - сработает через указанное время после поворота энкодера
  • Счётчик кликов доступен при нажатом повороте: несколько кликов, зажатие кнопки, поворот

Предварительные клики

Библиотека считает количество кликов по кнопке и некоторые функции опроса могут отдельно обрабатываться с предварительными кликами. Например 3 клика, затем удержание. Это очень сильно расширяет возможности одной кнопки. Есть два варианта работы с такими событиями:

  // 1
  if (btn.hold()) {
    if (btn.getClicks() == 2) Serial.println("hold 2 clicks");
  }

  // 2
  if (btn.hold(2)) Serial.println("hold 2 clicks");

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

Прямое чтение кнопки

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

void setup() {
  btn.tick();
  if (btn.press()) Serial.println("Кнопка нажата при старте");
}

Для таких сценариев помогут следующие функции, возвращают true если кнопка нажата:

  • read() для библиотек Button и ButtonT
  • readBtn() для библиотек EncButton и EncButtonT

Опрос кнопки выполняется с учётом настроенного ранее уровня кнопки (setBtnLevel)! Вручную дополнительно инвертировать логику не нужно:

void setup() {
  // btn.setBtnLevel(LOW); // можно настроить уровень

  if (btn.read()) Serial.println("Кнопка нажата при старте");
}

Погружение в цикл

Допустим нужно обработать кнопку синхронно и с гашением дребезга. Например если кнопка зажата при старте микроконтроллера - получить её удержание или даже импульсное удержание внутри блока setup, то есть до начала выполнения основной программы. Можно воспользоваться состоянием busy и опрашивать кнопку из цикла:

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

  btn.tick();
  while (btn.busy()) {
    btn.tick();
    if (btn.hold()) Serial.println("hold");
    if (btn.step()) Serial.println("step");
  }

  Serial.println("program start");
}

Как это работает: первый тик опрашивает кнопку, если кнопка нажата - сразу же активируется состояние busy и система попадает в цикл while. Внутри него продолжаем тикать и получать события с кнопки. Когда кнопка будет отпущена и сработают все события - флаг busy опустится и программа автоматически покинет цикл. Можно переписать эту конструкцию на цикл с постусловием, более красиво:

do {
  btn.tick();
  if (btn.hold()) Serial.println("hold");
  if (btn.step()) Serial.println("step");
} while (btn.busy());

Timeout

В связанных с кнопкой классах (Button, EncButton) есть функция timeout(time) - она однократно вернёт true, если после окончания действий с кнопкой/энкодером прошло указанное время. Это можно использовать для сохранения параметров после ввода, например:

void loop() {
  eb.tick();

  // ...

  if (eb.timeout(2000)) {
    // после взаимодействия с энкодером прошло 2 секунды
    // EEPROM.put(0, settings);
  }
}

Busy

Функция busy() возвращает true, пока идёт обработка кнопки, т.е. пока система ожидает действий и выхода таймаутов. Это можно использовать для оптимизации кода, например избегать каких то долгих и тяжёлых частей программы на время обработки кнопки:

void loop() {
  eb.tick();

  // ...

  if (!eb.busy()) {
    // потенциально долгий и тяжёлый код
  }
}

Получение события

Доступно во всех классах с кнопкой:

  • VirtButton
  • Button
  • VirtEncButton
  • EncButton

Функция action() при наступлении события возвращает код события (отличный от нуля, что само по себе является индикацией наличия события):

  • EB_PRESS - нажатие на кнопку
  • EB_HOLD - кнопка удержана
  • EB_STEP - импульсное удержание
  • EB_RELEASE - кнопка отпущена
  • EB_CLICK - одиночный клик
  • EB_CLICKS - сигнал о нескольких кликах
  • EB_TURN - поворот энкодера
  • EB_REL_HOLD - кнопка отпущена после удержания
  • EB_REL_HOLD_C - кнопка отпущена после удержания с предв. кликами
  • EB_REL_STEP - кнопка отпущена после степа
  • EB_REL_STEP_C - кнопка отпущена после степа с предв. кликами

Результат функции action() сбрасывается после следующего вызова tick(), то есть доступен на всей текущей итерации основного цикла

Полученный код события можно обработать через switch:

switch (eb.action()) {
  case EB_PRESS:
    // ...
    break;
  case EB_HOLD:
    // ...
    break;
  // ...
}

Оптимизация

Вес библиотеки

Для максимального уменьшения веса библиотеки (в частности в оперативной памяти) нужно задавать тайматуы константами через define (экономия 1 байт за таймаут), отключить обработчик событий, счётчики-буферы и использовать T-класс (экономия 1 байт за пин):

#define EB_NO_FOR
#define EB_NO_CALLBACK
#define EB_NO_COUNTER
#define EB_NO_BUFFER
#define EB_DEB_TIME 50     // таймаут гашения дребезга кнопки (кнопка)
#define EB_CLICK_TIME 500  // таймаут ожидания кликов (кнопка)
#define EB_HOLD_TIME 600   // таймаут удержания (кнопка)
#define EB_STEP_TIME 200   // таймаут импульсного удержания (кнопка)
#define EB_FAST_TIME 30    // таймаут быстрого поворота (энкодер)
#include <EncButton.h>
EncButtonT<2, 3, 4> eb;

В таком случае энкодер с кнопкой займёт в SRAM всего 8 байт, а просто кнопка - 5.

Скорость выполнения

Чтобы сократить время на проверку системных флагов событий (незначительно, но приятно) можно поместить все опросы в условие по tick(), так как tick() возвращает true только при наступлении события:

void loop() {
  if (eb.tick()) {
    if (eb.turn()) ...;
    if (eb.click()) ...;
  }
}

Также опрос событий при помощи функции action() выполняется быстрее, чем ручной опрос отдельных функций событий, поэтому максимально эффективно библиотека будет работать вот в таком формате:

void loop() {
  if (eb.tick()) {
    switch (eb.action()) {
      case EB_PRESS:
        // ...
        break;
      case EB_HOLD:
        // ...
        break;
      // ...
    }
  }
}

Для опроса состояний кнопки pressing(), holding(), waiting() можно поместить их вовнутрь условия по busy(), чтобы не опрашивать состояния пока их гарантированно нет:

if (btn.busy()) {
  if (btn.pressing())...
  if (btn.holding())...
  if (btn.waiting())...
}

Коллбэки

Можно подключить внешнюю функцию-обрбаотчик события, она будет вызвана при наступлении любого события. Данная возможность работает во всех классах с кнопкой:

  • VirtButton
  • Button
  • VirtEncButton
  • EncButton

Внутри коллбэка можно получить указатель на текущий объект (который вызвал коллбэк) из переменной void* EB_self

EncButton eb(2, 3, 4);

void callback() {
  switch (eb.action()) {
    case EB_PRESS:
      // ...
      break;
    case EB_HOLD:
      // ...
      break;
    // ...
  }

  // здесь EB_self указатель на eb
}

void setup() {
  eb.attach(callback);
}

void loop() {
  eb.tick();
}

Одновременное нажатие

Библиотека нативно поддерживает работу с двумя одновременно нажатыми кнопками как с третьей кнопкой. Для этого нужно:

  1. Cоздать специальную кнопку MultiButton
  2. Передать виртуальной кнопке в обработку свои кнопки (это могут быть объекты классов VirtButton, Button, EncButton + их T-версии). Мульти-кнопка сама опросит обе кнопки!
  3. Опрашивать события или слушать обработчик
Button b0(4);
Button b1(5);
MultiButton b2;  // 1

void loop() {
  b2.tick(b0, b1);  // 2

  // 3
  if (b0.click()) Serial.println("b0 click");
  if (b1.click()) Serial.println("b1 click");
  if (b2.click()) Serial.println("b0+b1 click");
}

Библиотека сама "сбросит" лишние события с реальных кнопок, если они были нажаты вместе, за исключением события press. Таким образом получается полноценная третья кнопка из двух других с удобным опросом.

Прерывания

Энкодер

Для обработки энкодера в загруженной программе нужно:

  • Подключить оба его пина на аппаратные прерывания по CHANGE
  • Установить setEncISR(true)
  • Вызывать в обработчике специальный тикер для прерывания
  • Основной тикер также нужно вызывать в loop для корреткной работы - события генерируются в основном тикере:
// пример для ATmega328 и EncButton
EncButton eb(2, 3, 4);

/*
// esp8266/esp32
IRAM_ATTR void isr() {
  eb.tickISR();
}
*/

void isr() {
  eb.tickISR();
}
void setup() {
  attachInterrupt(0, isr, CHANGE);
  attachInterrupt(1, isr, CHANGE);
  eb.setEncISR(true);
}
void loop() {
  eb.tick();
}

Примечание: использование работы в прерывании позволяет корректно обрабатывать позицию энкодера и не пропустить новый поворот. Событие с поворотом, полученное из прерывания, станет доступно после вызова tick в основном цикле программы, что позволяет не нарушать последовательность работы основного цикла:

  • Буферизация отключена: событие turn активируется только один раз, независимо от количества щелчков энкодера, совершённых между двумя вызовами tick (щелчки обработаны в прерывании)
  • Буферизация включена: событие turn будет вызвано столько раз, сколько реально было щелчков энкодера, это позволяет вообще не пропускать повороты и не нагружать систему в прерывании. Размер буфера - 5 необработанных щелчков энкодера

Примечания:

  • Функция setEncISR работает только в не виртуальных классах. Если он включен - основной тикер tick просто не опрашивает пины энкодера, что экономит процессорное время. Обработка происходит только в прерывании
  • Счётчик энкодера всегда имеет актуальное значение и может опережать буферизированные повороты в программе с большими задержками в основном цикле!
  • На разных платформах прерывания могут работать по разному (например на ESPxx - нужно добавить функции аттрибут IRAM_ATTR, см. документацию на свою платформу!)
  • Обработчик, подключенный в attach(), будет вызван из tick(), то есть не из прерывания!

Виртуальные классы

В виртуальных есть тикер, в который не нужно передавать состояние энкодера, если он обрабатывается в прерывании, это позволяет не опрашивать пины в холостую. Например:

VirtEncoder e;

void isr() {
    e.tickISR(digitalRead(2), digitalRead(3));
}
void setup() {
    attachInterrupt(0, isr, CHANGE);
    attachInterrupt(1, isr, CHANGE);

    e.setEncISR(1);
}
void loop() {
    e.tick();   // не передаём состояния пинов
}

Кнопка

Для обработки кнопки в прерывании нужно:

  • Подключить прерывание на нажатие кнопки с учётом её физического подключения и уровня:
    • Если кнопка замыкает LOW - прерывание FALLING
    • Если кнопка замыкает HIGH - прерывание RISING
  • Вызывать pressISR() в обработчике прерывания
Button b(2);

/*
// esp8266/esp32
IRAM_ATTR void isr() {
  b.pressISR();
}
*/

void isr() {
  b.pressISR();
}
void setup() {
  attachInterrupt(0, isr, FALLING);
}
void loop() {
  b.tick();
}

Примечание: кнопка обрабатывается в основном tick(), а функция pressISR() всего лишь сообщает библиотеке, что кнопка была нажата вне tick(). Это позволяет не пропустить нажатие кнопки, пока программа была занята чем-то другим.

Массив кнопок/энкодеров

Создать массив можно только из нешаблонных классов (без буквы T), потому что номера пинов придётся указать уже в рантайме далее в программе. Например:

Button btns[5];
EncButton ebs[3];

void setup() {
  btns[0].init(2);  // указать пин
  btns[1].init(5);
  btns[2].init(10);
  // ...

  ebs[0].init(11, 12, 13, INPUT);
  ebs[1].init(14, 15, 16);
  // ...
}
void loop() {
  for (int i = 0; i < 5; i++) btns[i].tick();
  for (int i = 0; i < 3; i++) ebs[i].tick();

  if (btns[2].click()) Serial.println("btn2 click");
  // ...
}

Кастомные функции

Библиотека поддерживает задание своих функций для чтения пина и получения времени без редактирования файлов библиотеки. Для этого нужно реализовать соответствующую функцию в своём .cpp или .ino файле:

  • bool EB_read(uint8_t pin) - для своей функции чтения пина
  • void EB_mode(uint8_t pin, uint8_t mode) - для своего аналога pinMode
  • uint32_t EB_uptime() - для своего аналога millis()

Пример:

#include <EncButton.h>

bool EB_read(uint8_t pin) {
    return digitalRead(pin);
}

void EB_mode(uint8_t pin, uint8_t mode) {
    pinMode(pin, mode);
}

uint32_t EB_uptime() {
    return millis();
}

Опрос по таймеру

Иногда может понадобиться вызывать tick() не на каждой итерации, а по таймеру. Например для виртуальной кнопки с расширителя пинов, когда чтение расширителя пинов - долгая операция, и вызывать её часто не имеет смысла. Вот так делать нельзя, события будут активны в течение периода таймера!

void loop() {
  // таймер на 50 мс
  static uint32_t tmr;
  if (millis() - tmr >= 50) {
    tmr = millis();
    btn.tick(readSomePin());
  }

  // будет активно в течение 50 мс!!!
  if (btn.click()) foo();
}

В данной ситуации нужно поступить так: тикать по таймеру, там же обрабатывать события и сбрасывать флаги в конце:

void loop() {
  // таймер на 50 мс
  static uint32_t tmr;
  if (millis() - tmr >= 50) {
    tmr = millis();
    // тик
    btn.tick(readSomePin());

    // разбор событий
    if (btn.click()) foo();

    // сброс флагов
    btn.clear();
  }
}

Либо можно подключить обработчик и вызывать clear() в конце функции:

void callback() {
  switch (btn.action()) {
    // ...
  }

  // сброс флагов
  btn.clear();
}

void loop() {
  // таймер на 50 мс
  static uint32_t tmr;
  if (millis() - tmr >= 50) {
    tmr = millis();
    btn.tick(readSomePin());
  }
}

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

Для корректной работы таймаутов, состояний и счётчика кликов нужен другой подход: буферизировать прочитанные по таймеру состояния и передавать их в тик в основном цикле. Например так:

bool readbuf = 0;  // буфер пина

void loop() {
  // таймер на 50 мс
  static uint32_t tmr;
  if (millis() - tmr >= 50) {
    tmr = millis();
    readbuf = readSomePin();  // чтение в буфер
  }

  // тик из буфера
  btn.tick(readbuf);

  if (btn.click()) foo();
}

Пропуск событий

EncButton позволяет кнопке работать в паре с энкодером для корректного отслеживания нажатых поворотов - при нажатом повороте события с кнопки будут пропущены, т.е. не обработается удержание и клик. Допустим кнопок несколько: они могут выполнять действия как сами по себе, так и в паре с энкодером (кнопка зажата и крутится энкодер, в программе меняется выбранное кнопкой значение). Чтобы при удержании кнопка не генерировала события (удержание, степ, клики...) можно включить пропуск событий. Он будет действовать до отпускания кнопки:

if (btn.pressing() && enc.turn()) {
  btn.skipEvents();  // зафиксирован поворот. Пропускаем события
  // нажатый поворот
}

if (btn.click()) {
  // просто клик
}

Мини примеры, сценарии

// меняем значения переменных

// поворот энкодера
if (enc.turn()) {
  // меняем с шагом 5
  var += 5 * enc.dir();

  // меняем с шагом 1 при обычном повороте, 10 при быстром
  var += enc.fast() ? 10 : 1;

  // меняем с шагом 1 при обычном повороте, 10 при нажатом
  var += enc.pressing() ? 10 : 1;

  // меняем одну переменную при повороте, другую - при нажатом повороте
  if (enc.pressing()) var0++;
  else var1++;

  // если кнопка нажата - доступны предварительные клики
  // Выбираем переменную для изменения по предв. кликам
  if (enc.pressing()) {
    switch (enc.getClicks()) {
      case 1: var0 += enc.dir();
        break;
      case 2: var1 += enc.dir();
        break;
      case 3: var2 += enc.dir();
        break;
    }
  }
}

// импульсное удержание на каждом шаге инкрементирует переменную
if (btn.step()) var++;

// смена направления изменения переменной после отпускания из step
if (btn.step()) var += dir;
if (btn.releaseStep()) dir = -dir;

// изменение выбранной переменной при помощи step
if (btn.step(1)) var1++;  // клик-удержание
if (btn.step(2)) var2++;  // клик-клик-удержание
if (btn.step(3)) var3++;  // клик-клик-клик-удержание

// если держать step больше 2 секунд - инкремент +5, пока меньше - +1
if (btn.step()) {
  if (btn.stepFor(2000)) var += 5;
  else var += 1;
}

// включение режима по количеству кликов
if (btn.hasClicks()) mode = btn.getClicks();

// включение режима по нескольким кликам и удержанию
if (btn.hold(1)) mode = 1;  // клик-удержание
if (btn.hold(2)) mode = 2;  // клик-клик-удержание
if (btn.hold(3)) mode = 3;  // клик-клик-клик-удержание

// или так
if (btn.hold()) mode = btn.getClicks();

// кнопка отпущена, смотрим сколько её удерживали
if (btn.release()) {
  // от 1 до 2 секунд
  if (btn.pressFor() > 1000 && btn.pressFor() <= 2000) mode = 1;
  // от 2 до 3 секунд
  else if (btn.pressFor() > 2000 && btn.pressFor() <= 3000) mode = 2;
}

Гайд по миграции с v2 на v3

Инициализация

// ВИРТУАЛЬНЫЕ
VirtEncButton eb; // энкодер с кнопкой
VirtButton b;     // кнопка
VirtEncoder e;    // энкодер

// РЕАЛЬНЫЕ
// энкодер с кнопкой
EncButton eb(enc0, enc1, btn);                    // пины энкодера и кнопки
EncButton eb(enc0, enc1, btn, modeEnc);           // + режим пинов энкодера (умолч. INPUT)
EncButton eb(enc0, enc1, btn, modeEnc, modeBtn);  // + режим пина кнопки (умолч. INPUT_PULLUP)
EncButton eb(enc0, enc1, btn, modeEnc, modeBtn, btnLevel);  // + уровень кнопки (умолч. LOW)
// шаблонный
EncButton<enc0, enc1, btn> eb;                    // пины энкодера и кнопки
EncButton<enc0, enc1, btn> eb(modeEnc);           // + режим пинов энкодера (умолч. INPUT)
EncButton<enc0, enc1, btn> eb(modeEnc, modeBtn);  // + режим пина кнопки (умолч. INPUT_PULLUP)
EncButton<enc0, enc1, btn> eb(modeEnc, modeBtn, btnLevel);  // + уровень кнопки (умолч. LOW)

// кнопка
Button b(pin);                  // пин
Button b(pin, mode);            // + режим пина кнопки (умолч. INPUT_PULLUP)
Button b(pin, mode, btnLevel);  // + уровень кнопки (умолч. LOW)
// шаблонный
ButtonT<pin> b;                 // пин
ButtonT<pin> b(mode);           // + режим пина кнопки (умолч. INPUT_PULLUP)
ButtonT<pin> b(mode, btnLevel); // + уровень кнопки (умолч. LOW)

// энкодер
Encoder e(enc0, enc1);          // пины энкодера
Encoder e(enc0, enc1, mode);    // + режим пинов энкодера (умолч. INPUT)
// шаблонный
EncoderT<enc0, enc1> e;         // пины энкодера
EncoderT<enc0, enc1> e(mode);   // + режим пинов энкодера (умолч. INPUT)

Функции

v2 v3
held() hold()
hold() holding()
state() pressing()
setPins() init()
  • Изменился порядок указания пинов (см. доку выше)
  • clearFlags() заменена на clear() (сбросить флаги событий) и reset() (сбросить системные флаги обработки, закончить обработку)

Логика работы

В v3 функции опроса событий (click, turn...) не сбрасываются сразу после своего вызова - они сбрасываются при следующем вызове tick(), таким образом сохраняют своё значение во всех последующих вызовах на текущей итерации главного цикла программы. Поэтому tick() нужно вызывать только 1 раз за цикл, иначе будут пропуски действий! Читай об этом выше.

Примеры

Остальные примеры смотри в examples!

Полное демо EncButton
// #define EB_NO_FOR           // отключить поддержку pressFor/holdFor/stepFor и счётчик степов (экономит 2 байта оперативки)
// #define EB_NO_CALLBACK      // отключить обработчик событий attach (экономит 2 байта оперативки)
// #define EB_NO_COUNTER       // отключить счётчик энкодера (экономит 4 байта оперативки)
// #define EB_NO_BUFFER        // отключить буферизацию энкодера (экономит 1 байт оперативки)

// #define EB_DEB_TIME 50      // таймаут гашения дребезга кнопки (кнопка)
// #define EB_CLICK_TIME 500   // таймаут ожидания кликов (кнопка)
// #define EB_HOLD_TIME 600    // таймаут удержания (кнопка)
// #define EB_STEP_TIME 200    // таймаут импульсного удержания (кнопка)
// #define EB_FAST_TIME 30     // таймаут быстрого поворота (энкодер)

#include <EncButton.h>
EncButton eb(2, 3, 4);
//EncButton eb(2, 3, 4, INPUT); // + режим пинов энкодера
//EncButton eb(2, 3, 4, INPUT, INPUT_PULLUP); // + режим пинов кнопки
//EncButton eb(2, 3, 4, INPUT, INPUT_PULLUP, LOW);  // + уровень кнопки

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

    // показаны значения по умолчанию
    eb.setBtnLevel(LOW);
    eb.setClickTimeout(500);
    eb.setDebTimeout(50);
    eb.setHoldTimeout(600);
    eb.setStepTimeout(200);

    eb.setEncReverse(0);
    eb.setEncType(EB_STEP4_LOW);
    eb.setFastTimeout(30);

    // сбросить счётчик энкодера
    eb.counter = 0;
}

void loop() {
    eb.tick();

    // обработка поворота общая
    if (eb.turn()) {
        Serial.print("turn: dir ");
        Serial.print(eb.dir());
        Serial.print(", fast ");
        Serial.print(eb.fast());
        Serial.print(", hold ");
        Serial.print(eb.pressing());
        Serial.print(", counter ");
        Serial.print(eb.counter);
        Serial.print(", clicks ");
        Serial.println(eb.getClicks());
    }

    // обработка поворота раздельная
    if (eb.left()) Serial.println("left");
    if (eb.right()) Serial.println("right");
    if (eb.leftH()) Serial.println("leftH");
    if (eb.rightH()) Serial.println("rightH");

    // кнопка
    if (eb.press()) Serial.println("press");
    if (eb.click()) Serial.println("click");

    if (eb.release()) {
      Serial.println("release");

      Serial.print("clicks: ");
      Serial.print(eb.getClicks());
      Serial.print(", steps: ");
      Serial.print(eb.getSteps());
      Serial.print(", press for: ");
      Serial.print(eb.pressFor());
      Serial.print(", hold for: ");
      Serial.print(eb.holdFor());
      Serial.print(", step for: ");
      Serial.println(eb.stepFor());
    }

    // состояния
    // Serial.println(eb.pressing());
    // Serial.println(eb.holding());
    // Serial.println(eb.busy());
    // Serial.println(eb.waiting());

    // таймаут
    if (eb.timeout(1000)) Serial.println("timeout!");

    // удержание
    if (eb.hold()) Serial.println("hold");
    if (eb.hold(3)) Serial.println("hold 3");

    // импульсное удержание
    if (eb.step()) Serial.println("step");
    if (eb.step(3)) Serial.println("step 3");

    // отпущена после импульсного удержания
    if (eb.releaseStep()) Serial.println("release step");
    if (eb.releaseStep(3)) Serial.println("release step 3");

    // отпущена после удержания
    if (eb.releaseHold()) Serial.println("release hold");
    if (eb.releaseHold(2)) Serial.println("release hold 2");

    // проверка на количество кликов
    if (eb.hasClicks(3)) Serial.println("has 3 clicks");

    // вывести количество кликов
    if (eb.hasClicks()) {
        Serial.print("has clicks: ");
        Serial.println(eb.getClicks());
    }
}
Подключение обработчика
#include <EncButton.h>
EncButton eb(2, 3, 4);

void callback() {
    Serial.print("callback: ");
    switch (eb.action()) {
        case EB_PRESS:
            Serial.println("press");
            break;
        case EB_HOLD:
            Serial.println("hold");
            break;
        case EB_STEP:
            Serial.println("step");
            break;
        case EB_RELEASE:
            Serial.println("release");
            break;
        case EB_CLICK:
            Serial.println("click");
            break;
        case EB_CLICKS:
            Serial.print("clicks ");
            Serial.println(eb.getClicks());
            break;
        case EB_TURN:
            Serial.print("turn ");
            Serial.print(eb.dir());
            Serial.print(" ");
            Serial.print(eb.fast());
            Serial.print(" ");
            Serial.println(eb.pressing());
            break;
        case EB_REL_HOLD:
            Serial.println("release hold");
            break;
        case EB_REL_HOLD_C:
            Serial.print("release hold clicks ");
            Serial.println(eb.getClicks());
            break;
        case EB_REL_STEP:
            Serial.println("release step");
            break;
        case EB_REL_STEP_C:
            Serial.print("release step clicks ");
            Serial.println(eb.getClicks());
            break;
    }
}

void setup() {
    Serial.begin(115200);
    eb.attach(callback);
}

void loop() {
    eb.tick();
}
Все типы кнопок
#include <EncButton.h>

Button btn(4);
ButtonT<5> btnt;
VirtButton btnv;

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

void loop() {
    // Button
    btn.tick();
    if (btn.click()) Serial.println("btn click");

    // ButtonT
    btnt.tick();
    if (btnt.click()) Serial.println("btnt click");

    // VirtButton
    btnv.tick(!digitalRead(4));  // передать логическое значение
    if (btnv.click()) Serial.println("btnv click");
}
Все типы энкодеров
#include <EncButton.h>

Encoder enc(2, 3);
EncoderT<5, 6> enct;
VirtEncoder encv;

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

void loop() {
    // опрос одинаковый для всех, 3 способа:

    // 1
    // tick вернёт 1 или -1, значит это шаг
    if (enc.tick()) Serial.println(enc.counter);

    // 2
    // можно опросить через turn()
    enct.tick();
    if (enct.turn()) Serial.println(enct.dir());

    // 3
    // можно не использовать опросные функции, а получить направление напрямую
    int8_t v = encv.tick(digitalRead(2), digitalRead(3));
    if (v) Serial.println(v);  // выведет 1 или -1
}

Версии

Старые
  • v1.1 - пуллап отдельныи методом
  • v1.2 - можно передать конструктору параметр INPUT_PULLUP / INPUT(умолч)
  • v1.3 - виртуальное зажатие кнопки энкодера вынесено в отдельную функцию + мелкие улучшения
  • v1.4 - обработка нажатия и отпускания кнопки
  • v1.5 - добавлен виртуальный режим
  • v1.6 - оптимизация работы в прерывании
  • v1.6.1 - подтяжка по умолчанию INPUT_PULLUP
  • v1.7 - большая оптимизация памяти, переделан FastIO
  • v1.8 - индивидуальная настройка таймаута удержания кнопки (была общая на всех)
  • v1.8.1 - убран FastIO
  • v1.9 - добавлена отдельная отработка нажатого поворота и запрос направления
  • v1.10 - улучшил обработку released, облегчил вес в режиме callback и исправил баги
  • v1.11 - ещё больше всякой оптимизации + настройка уровня кнопки
  • v1.11.1 - совместимость Digispark
  • v1.12 - добавил более точный алгоритм энкодера EB_BETTER_ENC
  • v1.13 - добавлен экспериментальный EncButton2
  • v1.14 - добавлена releaseStep(). Отпускание кнопки внесено в дебаунс
  • v1.15 - добавлен setPins() для EncButton2
  • v1.16 - добавлен режим EB_HALFSTEP_ENC для полушаговых энкодеров
  • v1.17 - добавлен step с предварительными кликами
  • v1.18 - не считаем клики после активации step. hold() и held() тоже могут принимать предварительные клики. Переделан и улучшен дебаунс
  • v1.18.1 - исправлена ошибка в releaseStep() (не возвращала результат)
  • v1.18.2 - fix compiler warnings
  • v1.19 - оптимизация скорости, уменьшен вес в sram
  • v1.19.1 - ещё чутка увеличена производительность
  • v1.19.2 - ещё немного увеличена производительность, спасибо XRay3D
  • v1.19.3 - сделал высокий уровень кнопки по умолчанию в виртуальном режиме
  • v1.19.4 - фикс EncButton2
  • v1.20 - исправлена критическая ошибка в EncButton2
  • v1.21 - EB_HALFSTEP_ENC теперь работает для обычного режима
  • v1.22 - улучшен EB_HALFSTEP_ENC для обычного режима
  • v1.23 - getDir() заменил на dir()
  • v2.0
    • Алгоритм EB_BETTER_ENC оптимизирован и установлен по умолчанию, дефайн EB_BETTER_ENC упразднён
    • Добавлен setEncType() для настройки типа энкодера из программы, дефайн EB_HALFSTEP_ENC упразднён
    • Добавлен setEncReverse() для смены направления энкодера из программы
    • Добавлен setStepTimeout() для установки периода импульсного удержания, дефайн EB_STEP упразднён
    • Мелкие улучшения и оптимизация
  • v3.0
    • Библиотека переписана с нуля, с предыдущими версиями несовместима!
      • Полностью другая инициализация объекта
      • Переименованы: hold()->holding(), held()->hold()
    • Оптимизация Flash памяти: библиотека весит меньше, в некоторых сценариях - на несколько килобайт
    • Оптимизация скорости выполнения кода, в том числе в прерывании
    • На несколько байт меньше оперативной памяти, несколько уровней оптимизации на выбор
    • Более простое, понятное и удобное использование
    • Более читаемый исходный код
    • Разбитие на классы для использования в разных сценариях
    • Новые функции, возможности и обработчики для кнопки и энкодера
    • Буферизация энкодера в прерывании
    • Нативная обработка двух одновременно нажимаемых кнопок как третьей кнопки
    • Поддержка 4-х типов энкодеров
    • Переписана документация
    • EncButton теперь заменяет GyverLibs/VirtualButton (архивирована)
  • v3.1
    • Расширена инициализация кнопки
    • Убраны holdEncButton() и toggleEncButton()
    • Добавлен turnH()
    • Оптимизированы прерывания энкодера, добавлена setEncISR()
    • Буферизация направления и быстрого поворота
    • Сильно оптимизирована скорость работы action() (общий обработчик)
    • Добавлено подключение внешней функции-обработчика событий
    • Добавлена обработка кнопки в прерывании - pressISR()
  • v3.2
    • Добавлены функции tickRaw() и clear() для всех классов. Позволяет проводить раздельную обработку (см. доку)
    • Улучшена обработка кнопки с использованием прерываний
  • v3.3
    • Добавлены функции получения времени удержания pressFor(), holdFor(), stepFor() (отключаемые)
    • Добавлен счётчик степов getSteps() (отключаемый)
  • v3.4
    • Доступ к счётчику кликов во время нажатого поворота
    • Добавлена функция detach()
  • v3.5
    • Добавлена зависимость GyverIO (ускорен опрос пинов)
    • Добавлена возможность задать свои функции аптайма и чтения пина
  • v3.5.2
    • Оптимизация
    • Упрощена замена кастомных функций
    • Исправлена ошибка компиляции при использовании библиотеки в нескольких .cpp файлах
  • v3.5.3
    • Добавлено количество кликов в опрос press/release/click/pressing
  • v3.5.5 - коллбэк на базе std::function для ESP
  • v3.5.8 - добавлен метод releaseHoldStep()
  • v3.5.11 - добавлен метод skipEvents() для игнорирования событий кнопки в сложных сценариях использования
  • v3.6.0
    • Добавлен класс MultiButton для корректного опроса нескольких кнопок с вызовом обработчика
    • Добавлено подключение обработчика с передачей указателя на объект

Баги и обратная связь

При нахождении багов создавайте Issue, а лучше сразу пишите на почту [email protected]
Библиотека открыта для доработки и ваших Pull Request'ов!

При сообщении о багах или некорректной работе библиотеки нужно обязательно указывать:

  • Версия библиотеки
  • Какой используется МК
  • Версия SDK (для ESP)
  • Версия Arduino IDE
  • Корректно ли работают ли встроенные примеры, в которых используются функции и конструкции, приводящие к багу в вашем коде
  • Какой код загружался, какая работа от него ожидалась и как он работает в реальности
  • В идеале приложить минимальный код, в котором наблюдается баг. Не полотно из тысячи строк, а минимальный код

About

Лёгкая и очень функциональная библиотека для энкодера с кнопкой, энкодера или кнопки с Arduino

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published