Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/GyverLibs/EncButton

Лёгкая и очень функциональная библиотека для энкодера с кнопкой, энкодера или кнопки с Arduino
https://github.com/GyverLibs/EncButton

arduino arduino-library button encoder

Last synced: about 2 months ago
JSON representation

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

Awesome Lists containing this project

README

        

[![latest](https://img.shields.io/github/v/release/GyverLibs/EncButton.svg?color=brightgreen)](https://github.com/GyverLibs/EncButton/releases/latest/download/EncButton.zip)
[![PIO](https://badges.registry.platformio.org/packages/gyverlibs/library/EncButton.svg)](https://registry.platformio.org/libraries/gyverlibs/EncButton)
[![Foo](https://img.shields.io/badge/Website-AlexGyver.ru-blue.svg?style=flat-square)](https://alexgyver.ru/)
[![Foo](https://img.shields.io/badge/%E2%82%BD%24%E2%82%AC%20%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B0%D1%82%D1%8C-%D0%B0%D0%B2%D1%82%D0%BE%D1%80%D0%B0-orange.svg?style=flat-square)](https://alexgyver.ru/support_alex/)
[![Foo](https://img.shields.io/badge/README-ENGLISH-blueviolet.svg?style=flat-square)](https://github-com.translate.goog/GyverLibs/EncButton?_x_tr_sl=ru&_x_tr_tl=en)

[![Foo](https://img.shields.io/badge/ПОДПИСАТЬСЯ-НА%20ОБНОВЛЕНИЯ-brightgreen.svg?style=social&logo=telegram&color=blue)](https://t.me/GyverLibs)

# EncButton

| ⚠️⚠️⚠️
**Новая версия v3 несовместима с предыдущими, смотри [документацию](#docs), [примеры](#example) и краткий [гайд по миграции](#migrate) с v2 на v3!**
⚠️⚠️⚠️ |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

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

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

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

## Содержание
- [Установка](#install)
- [Информация](#info)
- [Документация](#docs)
- [Настройки компиляции](#config)
- [Полное описание классов](#class)
- [Обработка и опрос](#tick)
- [Предварительные клики](#preclicks)
- [Прямое чтение кнопки](#btnread)
- [Погружение в цикл](#loop)
- [Timeout](#timeout)
- [Busy](#busy)
- [Получение события](#actions)
- [Оптимизация](#optimise)
- [Коллбэки](#callback)
- [Одновременное нажатие](#double)
- [Прерывания](#isr)
- [Массив кнопок/энкодеров](#array)
- [Кастомные функции](#custom)
- [Опрос по таймеру](#timer)
- [Мини примеры, сценарии](#examples-mini)
- [Миграция с v2](#migrate)
- [Примеры](#example)
- [Версии](#versions)
- [Баги и обратная связь](#feedback)


## Установка
- Для работы требуется библиотека [GyverIO](https://github.com/GyverLibs/GyverIO)
- Библиотеку можно найти по названию **EncButton** и установить через менеджер библиотек в:
- Arduino IDE
- Arduino IDE v2
- PlatformIO
- [Скачать библиотеку](https://github.com/GyverLibs/EncButton/archive/refs/heads/main.zip) .zip архивом для ручной установки:
- Распаковать и положить в *C:\Program Files (x86)\Arduino\libraries* (Windows x64)
- Распаковать и положить в *C:\Program Files\Arduino\libraries* (Windows x32)
- Распаковать и положить в *Документы/Arduino/libraries/*
- (Arduino IDE) автоматическая установка из .zip: *Скетч/Подключить библиотеку/Добавить .ZIP библиотеку…* и указать скачанный архив
- Читай более подробную инструкцию по установке библиотек [здесь](https://alexgyver.ru/arduino-first/#%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0_%D0%B1%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA)
### Обновление
- Рекомендую всегда обновлять библиотеку: в новых версиях исправляются ошибки и баги, а также проводится оптимизация и добавляются новые фичи
- Через менеджер библиотек IDE: найти библиотеку как при установке и нажать "Обновить"
- Вручную: **удалить папку со старой версией**, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!

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

![diagram](/doc/enc_type.png)

#### Рекомендации
Для работы по сценарию "энкодер с кнопкой" рекомендую вот такие ([ссылка](https://ali.ski/cmPI2), [ссылка](https://ali.ski/sZbTK)) круглые китайские модули с распаянными цепями антидребезга (имеют тип `EB_STEP4_LOW` по классификации выше):
![scheme](/doc/encAli.png)

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

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

### Кнопка
#### Уровень кнопки
Кнопка может быть подключена к микроконтроллеру двумя способами и давать при нажатии высокий или низкий сигнал. В библиотеке предусмотрена настройка `setBtnLevel(уровень)`, где уровень - активный сигнал кнопки:
- `HIGH` - кнопка подключает VCC. Установлен по умолчанию в `Virt`-библиотеках
- `LOW` - кнопка подключает GND. Установлен по умолчанию в основных библиотеках

![scheme](/doc/btn_scheme.png)

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

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

### Дефайны настроек
Объявлять до подключения библиотеки

```cpp

// отключить поддержку 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 ` подключает все инструменты библиотеки!

Таблица функций кнопки

| | 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

```cpp
// ================ НАСТРОЙКИ ================
// установить таймаут удержания, умолч. 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

```cpp
// ==================== НАСТРОЙКИ ====================
// инвертировать направление энкодера (умолч. 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`

```cpp
// ================== НАСТРОЙКИ ==================
// установить таймаут быстрого поворота, мс
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`

```cpp
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)
```
```cpp
// указать пин и его режим работы
void init(uint8_t npin, uint8_t mode);

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

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

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

ButtonT

- Доступны функции из `VirtButton`
- Режим кнопки по умолчанию - `LOW`

```cpp
ButtonT; // с указанием пина
ButtonT (uint8_t mode); // + режим работы (умолч. INPUT_PULLUP)
ButtonT (uint8_t mode, uint8_t btnLevel); // + уровень кнопки (умолч. LOW)
```
```cpp
// указать режим работы
void init(uint8_t mode);

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

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

Encoder

- Доступны функции из `VirtEncoder`

```cpp
Encoder;
Encoder(uint8_t encA, uint8_t encB); // с указанием пинов
Encoder(uint8_t encA, uint8_t encB, uint8_t mode); // + режим работы (умолч. INPUT)
```
```cpp
// указать пины и их режим работы
void init(uint8_t encA, uint8_t encB, uint8_t mode);

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

// функция обработки для вызова в loop
int8_t tick();
```

EncoderT

- Доступны функции из `VirtEncoder`

```cpp
EncoderT; // с указанием пинов
EncoderT (uint8_t mode); // + режим работы (умолч. INPUT)
```
```cpp
// указать режим работы пинов
void init(uint8_t mode);

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

// функция обработки для вызова в loop
int8_t tick();
```

EncButton

- Доступны функции из `VirtButton`
- Доступны функции из `VirtEncoder`
- Доступны функции из `VirtEncButton`

```cpp
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);
```
```cpp
// настроить пины (энк, энк, кнопка, 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`

```cpp
// с указанием пинов
EncButtonT;

// + режим работы пинов, уровень кнопки
EncButtonT (uint8_t modeEnc = INPUT, uint8_t modeBtn = INPUT_PULLUP, uint8_t btnLevel = LOW);
```
```cpp
// настроить режим работы пинов, уровень кнопки
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`, пока активно это состояние (например кнопка удерживается)

Для простоты восприятия функцию обработки нужно размещать в начале цикла, а опросы делать ниже:
```cpp
void loop() {
btn.tick(); // опрос

if (btn.click()) Serial.println("click"); // однократно выведет при клике
if (btn.click()) Serial.println("click"); // тот же клик!
}
```
> В отличие от предыдущих версий библиотеки, функции опроса сбрасываются не внутри себя, а *внутри функции обработки*. Таким образом в примере выше при клике по кнопке в порт дважды выведется сообщение `click()`. Это позволяет использовать функции опроса по несколько раз за текущую итерацию цикла для создания сложной логики работы программы.

#### Несколько функций обработки
По очевидным причинам нельзя вызывать функцию обработки больше одного раза за цикл - каждый следующий вызов сбросит события от предыдущего и код будет работать некорректно. Вот так - нельзя:
```cpp
// так нельзя
void loop() {
btn.tick();
if (btn.click()) ...

// ....

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

Если очень нужно попасть в глухой цикл и опрашивать там кнопку, то вот так - можно:
```cpp
// так можно
void loop() {
btn.tick();
if (btn.click()) ...

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

Если библиотека используется с подключенным обработчиком событий `attach()` (см. ниже), то можно вызывать `tick()` где угодно и сколько угодно раз, события будут обработаны в обработчике:
```cpp
// так можно
void cb() {
switch (btn.action()) {
// ...
}
}

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

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

#### "Загруженная" программа
Библиотека EncButton - **асинхронная**: она не ждёт, пока закончится обработка кнопки, а позволяет программе выполняться дальше. Это означает, что для корректной работы библиотеки основной цикл программы должен выполняться как можно быстрее и не содержать задержек и других "глухих" циклов внутри себя. Для обеспечения правильной обработки кнопки не рекомендуется иметь в основном цикле задержки длительностью более 50-100 мс. Несколько советов:
- Новичкам: изучить цикл уроков [как написать скетч](https://alexgyver.ru/lessons/how-to-sketch/)
- Писать асинхронный код в `loop()`
- Любую синхронную конструкцию на `delay()` можно сделать асинхронной при помощи `millis()`
- Если в программе *каждая* итерация главного цикла выполняется дольше 50-100мс - в большинстве случаев программа написана неправильно, за исключением каких-то особых случаев
- Подключить кнопку на аппаратное прерывание (см. ниже)
- Избегать выполнения "тяжёлых" участков кода, пока идёт обработка кнопки, например поместив их в условие `if (!button.busy()) { тяжёлый код }`
- Если оптимизировать основной цикл невозможно - вызывать тикер в другом "потоке" и использовать функцию-обработчик:
- В прерывании таймера с периодом ~50мс или чаще
- На другом ядре (например ESP32)
- В другом таске FreeRTOS
- Внутри `yield()` (внутри `delay()`)

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

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

```cpp
void loop() {
if (btn.click()) ...
if (btn.press()) ...
if (btn.step()) ...

btn.clear();

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

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

#### Обработка внутри delay
Если сложно избавиться от `delay()` внутри главного цикла программы, то на некоторых платформах можно поместить свой код внутри него. Таким образом можно получить даже обработку энкодера в цикле с дилеями без использования прерываний:
```cpp
// вставка кода в 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](https://wokwi.com/projects/373591584298469377)

##### Click
![click](/doc/click.gif)

##### Hold
![hold](/doc/hold.gif)

##### Step
![step](/doc/step.gif)

Онлайн-симуляция доступна [здесь](https://wokwi.com/projects/373591584298469377)

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

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

### Предварительные клики
Библиотека считает количество кликов по кнопке и некоторые функции опроса могут отдельно обрабатываться с *предварительными кликами*. Например 3 клика, затем удержание. Это очень сильно расширяет возможности одной кнопки. Есть два варианта работы с такими событиями:
```cpp
// 1
if (btn.hold()) {
if (btn.getClicks() == 2) Serial.println("hold 2 clicks");
}

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

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

### Прямое чтение кнопки
В некоторых сценариях бывает нужно получить состояние кнопки "здесь и сейчас", например определить удерживается ли кнопка сразу после запуска микроконтроллера (старта программы). Функцию `tick()` нужно вызывать постоянно в цикле, чтобы шла обработка кнопки с гашением дребезга контактов и прочими расчётами, поэтому конструкция следующего вида **работать не будет**:
```cpp
void setup() {
btn.tick();
if (btn.press()) Serial.println("Кнопка нажата при старте");
}
```

Для таких сценариев помогут следующие функции, возвращают `true` если кнопка нажата:
- `read()` для библиотек Button и ButtonT
- `readBtn()` для библиотек EncButton и EncButtonT

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

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

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

### Погружение в цикл
Допустим нужно обработать кнопку синхронно и с гашением дребезга. Например если кнопка зажата при старте микроконтроллера - получить её удержание или даже импульсное удержание внутри блока `setup`, то есть до начала выполнения основной программы. Можно воспользоваться состоянием `busy` и опрашивать кнопку из цикла:
```cpp
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 опустится и программа автоматически покинет цикл. Можно переписать эту конструкцию на цикл с постусловием, более красиво:
```cpp
do {
btn.tick();
if (btn.hold()) Serial.println("hold");
if (btn.step()) Serial.println("step");
} while (btn.busy());
```

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

// ...

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

### Busy
Функция `busy()` возвращает `true`, пока идёт обработка кнопки, т.е. пока система ожидает действий и выхода таймаутов. Это можно использовать для оптимизации кода, например избегать каких то долгих и тяжёлых частей программы на время обработки кнопки:
```cpp
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`:
```cpp
switch (eb.action()) {
case EB_PRESS:
// ...
break;
case EB_HOLD:
// ...
break;
// ...
}
```

### Оптимизация
#### Вес библиотеки
Для максимального уменьшения веса библиотеки (в частности в оперативной памяти) нужно задавать тайматуы константами через define (экономия 1 байт за таймаут), отключить обработчик событий, счётчики-буферы и использовать T-класс (экономия 1 байт за пин):
```cpp
#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
EncButtonT<2, 3, 4> eb;
```
В таком случае энкодер с кнопкой займёт в SRAM всего 8 байт, а просто кнопка - 5.

#### Скорость выполнения
Чтобы сократить время на проверку системных флагов событий (незначительно, но приятно) можно поместить все опросы в условие по `tick()`, так как `tick()` возвращает `true` только при наступлении **события**:
```cpp
void loop() {
if (eb.tick()) {
if (eb.turn()) ...;
if (eb.click()) ...;
}
}
```

Также опрос событий при помощи функции `action()` выполняется быстрее, чем ручной опрос отдельных функций событий, поэтому максимально эффективно библиотека будет работать вот в таком формате:
```cpp
void loop() {
if (eb.tick()) {
switch (eb.action()) {
case EB_PRESS:
// ...
break;
case EB_HOLD:
// ...
break;
// ...
}
}
}
```

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

### Коллбэки
Можно подключить внешнюю функцию-обрбаотчик события, она будет вызвана при наступлении любого события. Данная возможность работает во всех классах **с кнопкой**:
- `VirtButton`
- `Button`
- `VirtEncButton`
- `EncButton`

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

```cpp
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. Опрашивать события или слушать обработчик

```cpp
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` для корреткной работы - события генерируются в основном тикере:
```cpp
// пример для 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()`, то есть *не из прерывания*!

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

```cpp
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()` в обработчике прерывания

```cpp
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`), потому что номера пинов придётся указать уже в рантайме далее в программе. Например:
```cpp
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()

Пример:

```cpp
#include

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()` не на каждой итерации, а по таймеру. Например для виртуальной кнопки с расширителя пинов, когда чтение расширителя пинов - долгая операция, и вызывать её часто не имеет смысла. Вот так делать нельзя, события будут активны в течение периода таймера!
```cpp
void loop() {
// таймер на 50 мс
static uint32_t tmr;
if (millis() - tmr >= 50) {
tmr = millis();
btn.tick(readSomePin());
}

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

В данной ситуации нужно поступить так: тикать по таймеру, там же обрабатывать события и сбрасывать флаги в конце:
```cpp
void loop() {
// таймер на 50 мс
static uint32_t tmr;
if (millis() - tmr >= 50) {
tmr = millis();
// тик
btn.tick(readSomePin());

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

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

Либо можно подключить обработчик и вызывать `clear()` в конце функции:
```cpp
void callback() {
switch (btn.action()) {
// ...
}

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

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

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

Для корректной работы таймаутов, состояний и счётчика кликов нужен другой подход: буферизировать прочитанные по таймеру состояния и передавать их в тик в основном цикле. Например так:
```cpp
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 позволяет кнопке работать в паре с энкодером для корректного отслеживания *нажатых поворотов* - при нажатом повороте события с кнопки будут пропущены, т.е. не обработается удержание и клик. Допустим кнопок несколько: они могут выполнять действия как сами по себе, так и в паре с энкодером (кнопка зажата и крутится энкодер, в программе меняется выбранное кнопкой значение). Чтобы при удержании кнопка не генерировала события (удержание, степ, клики...) можно включить пропуск событий. Он будет действовать **до отпускания кнопки**:

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

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

### Мини примеры, сценарии
```cpp
// меняем значения переменных

// поворот энкодера
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
### Инициализация
```cpp
// ВИРТУАЛЬНЫЕ
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 eb; // пины энкодера и кнопки
EncButton eb(modeEnc); // + режим пинов энкодера (умолч. INPUT)
EncButton eb(modeEnc, modeBtn); // + режим пина кнопки (умолч. INPUT_PULLUP)
EncButton eb(modeEnc, modeBtn, btnLevel); // + уровень кнопки (умолч. LOW)

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

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

### Функции
| v2 | v3 |
| ----------- | ------------ |
| `held()` | `hold()` |
| `hold()` | `holding()` |
| `state()` | `pressing()` |
| `setPins()` | `init()` |

- Изменился порядок указания пинов (см. доку выше)
- `clearFlags()` заменена на `clear()` (сбросить флаги событий) и `reset()` (сбросить системные флаги обработки, закончить обработку)

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


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

Полное демо EncButton

```cpp
// #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 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());
}
}
```

Подключение обработчика

```cpp
#include
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();
}
```

Все типы кнопок

```cpp
#include

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");
}
```

Все типы энкодеров

```cpp
#include

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]](mailto:[email protected])
Библиотека открыта для доработки и ваших **Pull Request**'ов!

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