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

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

Лёгкая и очень функциональная библиотека для энкодера с кнопкой, энкодера или кнопки с 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)

<a id="install"></a>
## Установка
- Для работы требуется библиотека [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: найти библиотеку как при установке и нажать "Обновить"
- Вручную: **удалить папку со старой версией**, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!

<a id="info"></a>

## Информация
### Энкодер
#### Тип энкодера
Библиотека поддерживает все 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`). В "реальных" проектах рекомендуется внешняя подтяжка, т.к. она менее подвержена помехам - у внутренней слишком высокое сопротивление.

<a id="docs"></a>

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

<a id="config"></a>

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

```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     // таймаут быстрого поворота (энкодер)
#define EB_TOUT_TIME 1000   // таймаут действия (кнопка и энкодер)
```

<a id="class"></a>

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

<details>
<summary>Таблица функций кнопки</summary>

|                   | VirtButton | VirtEncButton | Button | EncButton |
| ----------------- | :--------: | :-----------: | :----: | :-------: |
| read              |            |               |   ✔    |           |
| readBtn           |            |               |        |     ✔     |
| tickRaw           |     ✔      |       ✔       |   ✔    |     ✔     |
| setHoldTimeout    |     ✔      |       ✔       |   ✔    |     ✔     |
| setStepTimeout    |     ✔      |       ✔       |   ✔    |     ✔     |
| setClickTimeout   |     ✔      |       ✔       |   ✔    |     ✔     |
| setDebTimeout     |     ✔      |       ✔       |   ✔    |     ✔     |
| setTimeout        |     ✔      |       ✔       |   ✔    |     ✔     |
| setBtnLevel       |     ✔      |       ✔       |   ✔    |     ✔     |
| pressISR          |     ✔      |       ✔       |   ✔    |     ✔     |
| reset             |     ✔      |       ✔       |   ✔    |     ✔     |
| clear             |     ✔      |       ✔       |   ✔    |     ✔     |
| skipEvents        |     ✔      |       ✔       |   ✔    |     ✔     |
| attach            |     ✔      |       ✔       |   ✔    |     ✔     |
| detach            |     ✔      |       ✔       |   ✔    |     ✔     |
| press             |     ✔      |       ✔       |   ✔    |     ✔     |
| release           |     ✔      |       ✔       |   ✔    |     ✔     |
| click             |     ✔      |       ✔       |   ✔    |     ✔     |
| pressing          |     ✔      |       ✔       |   ✔    |     ✔     |
| hold              |     ✔      |       ✔       |   ✔    |     ✔     |
| holding           |     ✔      |       ✔       |   ✔    |     ✔     |
| step              |     ✔      |       ✔       |   ✔    |     ✔     |
| hasClicks         |     ✔      |       ✔       |   ✔    |     ✔     |
| getClicks         |     ✔      |       ✔       |   ✔    |     ✔     |
| getSteps          |     ✔      |       ✔       |   ✔    |     ✔     |
| releaseHold       |     ✔      |       ✔       |   ✔    |     ✔     |
| releaseStep       |     ✔      |       ✔       |   ✔    |     ✔     |
| releaseHoldStep   |     ✔      |       ✔       |   ✔    |     ✔     |
| waiting           |     ✔      |       ✔       |   ✔    |     ✔     |
| busy              |     ✔      |       ✔       |   ✔    |     ✔     |
| action            |     ✔      |       ✔       |   ✔    |     ✔     |
| getAction         |     ✔      |       ✔       |   ✔    |     ✔     |
| timeout           |     ✔      |       ✔       |   ✔    |     ✔     |
| pressFor          |     ✔      |       ✔       |   ✔    |     ✔     |
| holdFor           |     ✔      |       ✔       |   ✔    |     ✔     |
| stepFor           |     ✔      |       ✔       |   ✔    |     ✔     |
</details>

<details>
<summary>Таблица функций энкодера</summary>

|                | VirtEncoder | Encoder | VirtEncButton | EncButton |
| -------------- | :---------: | :-----: | :-----------: | :-------: |
| readEnc        |             |         |               |     ✔     |
| initEnc        |      ✔      |    ✔    |       ✔       |     ✔     |
| setEncReverse  |      ✔      |    ✔    |       ✔       |     ✔     |
| setEncType     |      ✔      |    ✔    |       ✔       |     ✔     |
| setEncISR      |      ✔      |    ✔    |       ✔       |     ✔     |
| clear          |      ✔      |    ✔    |       ✔       |     ✔     |
| turn           |      ✔      |    ✔    |       ✔       |     ✔     |
| dir            |      ✔      |    ✔    |       ✔       |     ✔     |
| tickRaw        |      ✔      |    ✔    |       ✔       |     ✔     |
| pollEnc        |      ✔      |    ✔    |       ✔       |     ✔     |
| counter        |      ✔      |    ✔    |       ✔       |     ✔     |
| setFastTimeout |             |         |       ✔       |     ✔     |
| turnH          |             |         |       ✔       |     ✔     |
| fast           |             |         |       ✔       |     ✔     |
| right          |             |         |       ✔       |     ✔     |
| left           |             |         |       ✔       |     ✔     |
| rightH         |             |         |       ✔       |     ✔     |
| leftH          |             |         |       ✔       |     ✔     |
| action         |             |         |       ✔       |     ✔     |
| getAction      |             |         |       ✔       |     ✔     |
| timeout        |             |         |       ✔       |     ✔     |
| attach         |             |         |       ✔       |     ✔     |
| detach         |             |         |       ✔       |     ✔     |
</details>

<details>
<summary>VirtButton</summary>

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

// установить время таймаута, умолч. 1000 (макс. 4000 мс)
void setTimeout(const uint16_t tout);

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

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

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

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

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

// игнорировать все события до отпускания кнопки
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();
EBAction getAction();

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

// после взаимодействия с кнопкой (или энкодером EncButton) время setTimeout, мс [состояние]
bool timeoutState();

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

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

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

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

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

// кнопка удерживается дольше чем (с начала степа), мс [состояние]
bool stepFor(uint16_t ms);
```
</details>
<details>
<summary>VirtEncoder</summary>

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

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

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

// опросить энкодер без установки флагов на поворот (быстрее). Вернёт 1 или -1 при вращении, 0 при остановке
int8_t pollEnc(bool e0, bool e1);
```
</details>
<details>
<summary>VirtEncButton</summary>

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

```cpp
// ================== НАСТРОЙКИ ==================
// установить таймаут быстрого поворота, мс
void setFastTimeout(uint8_t tout);

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

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

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

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

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

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

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

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

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

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

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

// обработка энкодера и кнопки без сброса флагов и вызова коллбэка
bool tickRaw(bool e0, bool e1, bool btn);
bool tickRaw(bool btn);  // энкодер в прерывании
```
</details>
<details>
<summary>Button</summary>

- Доступны функции из `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();
```
</details>
<details>
<summary>ButtonT</summary>

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

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

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

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

- Доступны функции из `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();
```
</details>
<details>
<summary>EncoderT</summary>

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

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

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

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

- Доступны функции из `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();
```
</details>
<details>
<summary>EncButtonT</summary>

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

```cpp
// с указанием пинов
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);
```
```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();
```
</details>

<a id="tick"></a>

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

Библиотека обрабатывает сигнал внутри этой функции, результат можно получить из **функций опроса** событий. Они бывают двух типов:
- `[событие]` - функция вернёт `true` однократно при наступлении события. Сбросится после следующего вызова функции обработки (например клик, поворот энкодера). За исключением события `timeout`
- `[состояние]` - функция возвращает `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 периодом из `setTimeout`
- Обработка кнопки в прерывании сообщает библиотеке о факте нажатия, вся остальная обработка выполняется штатно в `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()`) - сработает через указанное время после поворота энкодера
- Счётчик кликов доступен при нажатом повороте: несколько кликов, зажатие кнопки, поворот

<a id="preclicks"></a>

### Предварительные клики
Библиотека считает количество кликов по кнопке и некоторые функции опроса могут отдельно обрабатываться с *предварительными кликами*. Например 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");
```

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

<a id="btnread"></a>

### Прямое чтение кнопки
В некоторых сценариях бывает нужно получить состояние кнопки "здесь и сейчас", например определить удерживается ли кнопка сразу после запуска микроконтроллера (старта программы). Функцию `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("Кнопка нажата при старте");
}
```

<a id="loop"></a>

### Погружение в цикл
Допустим нужно обработать кнопку синхронно и с гашением дребезга. Например если кнопка зажата при старте микроконтроллера - получить её удержание или даже импульсное удержание внутри блока `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());
```

<a id="timeout"></a>

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

  // ...

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

В текущей версии обработка таймаута реализована не очень красиво ради экономии места: системный флаг таймаута активен всё время (`action` будет возвращать событие таймаута), сбрасывается в следующих случаях:

- Вызов `timeout()` - данный метод однократно вернёт `true`, последующие вызовы будут возвращать `false` до нового действия
- Автоматически сбросится после вызова обработчика, если он подключен
- При вызове `clear(true)` - с флагом очистки таймаута
- При вызове `reset()`

Если нужно пробросить опрос таймаута через несколько вызовов - можно использовать `timeoutState()`, но последний вызов должен быть `timeout()`.

<a id="busy"></a>

### Busy
Функция `busy()` возвращает `true`, пока идёт обработка кнопки, т.е. пока система ожидает действий и выхода таймаутов. Это можно использовать для оптимизации кода, например избегать каких то долгих и тяжёлых частей программы на время обработки кнопки:
```cpp
void loop() {
  eb.tick();

  // ...

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

<a id="actions"></a>

### Получение события
Доступно во всех классах **с кнопкой**:
- `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` - кнопка отпущена после степа с предв. кликами
- `EB_TIMEOUT` - прошёл таймаут после нажатия кнопки или поворота энкодера

Полученный код события можно обработать через `switch`:
```cpp
switch (eb.action()) {
  case EB_PRESS:
    // ...
    break;
  case EB_HOLD:
    // ...
    break;
  // ...
}
```

Есть аналогичная функция `getAction()`, вернёт то же самое, но с более читаемыми константами (удобно с автодополнением):

- `EBAction::Press`
- `EBAction::Hold`
- `EBAction::Step`
- `EBAction::Release`
- `EBAction::Click`
- `EBAction::Clicks`
- `EBAction::Turn`
- `EBAction::ReleaseHold`
- `EBAction::ReleaseHoldClicks`
- `EBAction::ReleaseStep`
- `EBAction::ReleaseStepClicks`
- `EBAction::Timeout`

Полученный код события можно обработать через `switch`:
```cpp
switch (eb.getAction()) {
  case EBAction::Press:
    // ...
    break;
  case EBAction::Hold:
    // ...
    break;
  // ...
}
```

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

<a id="optimise"></a>

### Оптимизация
#### Вес библиотеки
Для максимального уменьшения веса библиотеки (в частности в оперативной памяти) нужно задавать тайматуы константами через 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    // таймаут быстрого поворота (энкодер)
#define EB_TOUT_TIME 1000  // таймаут действия (кнопка и энкодер)
#include <EncButton.h>
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())...
}
```

<a id="callback"></a>

### Коллбэки
Можно подключить внешнюю функцию-обрбаотчик события, она будет вызвана при наступлении любого события. Данная возможность работает во всех классах **с кнопкой**:
- `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();
}
```

<a id="double"></a>

### Одновременное нажатие
Библиотека нативно поддерживает работу с двумя одновременно нажатыми кнопками как с третьей кнопкой. Для этого нужно:
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`. Таким образом получается полноценная третья кнопка из двух других с удобным опросом.

<a id="isr"></a>

### Прерывания
#### Энкодер
Для обработки энкодера в загруженной программе нужно:
- Подключить оба его пина на аппаратные прерывания по `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()`. Это позволяет не пропустить нажатие кнопки, пока программа была занята чем-то другим.

<a id="array"></a>

### Массив кнопок/энкодеров
Создать массив можно только из нешаблонных классов (без буквы `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");
  // ...
}
```

<a id="custom"></a>

### Кастомные функции
Библиотека поддерживает задание своих функций для чтения пина и получения времени без редактирования файлов библиотеки. Для этого нужно реализовать соответствующую функцию в своём .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 <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();
}
```

<a id="timer"></a>

### Опрос по таймеру
Иногда может понадобиться вызывать `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()) {
  // просто клик
}
```

<a id="examples-mini"></a>

### Мини примеры, сценарии
```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;
}
```

<a id="migrate"></a>

## Гайд по миграции с 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<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 раз за цикл, иначе будут пропуски действий!** Читай об этом выше.

<a id="example"></a>
## Примеры
Остальные примеры смотри в **examples**!
<details>
<summary>Полное демо EncButton</summary>

```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     // таймаут быстрого поворота (энкодер)
// #define EB_TOUT_TIME 1000   // таймаут действия (кнопка и энкодер)

#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()) 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());
    }
}
```
</details>
<details>
<summary>Подключение обработчика</summary>

```cpp
#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();
}
```
</details>
<details>
<summary>Все типы кнопок</summary>

```cpp
#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");
}
```
</details>
<details>
<summary>Все типы энкодеров</summary>

```cpp
#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
}
```
</details>

<a id="versions"></a>
## Версии
<details>
<summary>Старые</summary>

- 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 упразднён
    - Мелкие улучшения и оптимизация
</details>

- 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 для корректного опроса нескольких кнопок с вызовом обработчика
  - Добавлено подключение обработчика с передачей указателя на объект

<a id="feedback"></a>
## Баги и обратная связь
При нахождении багов создавайте **Issue**, а лучше сразу пишите на почту [alex@alexgyver.ru](mailto:alex@alexgyver.ru)  
Библиотека открыта для доработки и ваших **Pull Request**'ов!

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