[![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 // таймаут быстрого поворота (энкодер) #define EB_TOUT_TIME 1000 // таймаут действия (кнопка и энкодер) ``` ### Классы Как работать с документацией: 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 | ✔ | ✔ | ✔ | ✔ | | 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 | ✔ | ✔ | ✔ | ✔ |
Таблица функций энкодера | | 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 | | | ✔ | ✔ |
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); // установить время таймаута, умолч. 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); ```
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 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); 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` однократно при наступлении события. Сбросится после следующего вызова функции обработки (например клик, поворот энкодера). За исключением события `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()`) - сработает через указанное время после поворота энкодера - Счётчик кликов доступен при нажатом повороте: несколько кликов, зажатие кнопки, поворот ### Предварительные клики Библиотека считает количество кликов по кнопке и некоторые функции опроса могут отдельно обрабатываться с *предварительными кликами*. Например 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()` - она однократно вернёт `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()`. ### 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` - кнопка отпущена после степа с предв. кликами - `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()`, то есть доступен на всей текущей итерации основного цикла ### Оптимизация #### Вес библиотеки Для максимального уменьшения веса библиотеки (в частности в оперативной памяти) нужно задавать тайматуы константами через 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 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 // таймаут быстрого поворота (энкодер) // #define EB_TOUT_TIME 1000 // таймаут действия (кнопка и энкодер) #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()) 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**, а лучше сразу пишите на почту [alex@alexgyver.ru](mailto:alex@alexgyver.ru) Библиотека открыта для доработки и ваших **Pull Request**'ов! При сообщении о багах или некорректной работе библиотеки нужно обязательно указывать: - Версия библиотеки - Какой используется МК - Версия SDK (для ESP) - Версия Arduino IDE - Корректно ли работают ли встроенные примеры, в которых используются функции и конструкции, приводящие к багу в вашем коде - Какой код загружался, какая работа от него ожидалась и как он работает в реальности - В идеале приложить минимальный код, в котором наблюдается баг. Не полотно из тысячи строк, а минимальный код