|
|
2 bulan lalu | |
|---|---|---|
| .. | ||
| .github | 2 bulan lalu | |
| examples | 2 bulan lalu | |
| src | 2 bulan lalu | |
| .gitattributes | 2 bulan lalu | |
| .piopm | 2 bulan lalu | |
| LICENSE | 2 bulan lalu | |
| README.md | 2 bulan lalu | |
| README_EN.md | 2 bulan lalu | |
| keywords.txt | 2 bulan lalu | |
| library.properties | 2 bulan lalu | |
| ⚠️⚠️⚠️ Новая версия v3 несовместима с предыдущими, смотри документацию, примеры и краткий гайд по миграции с v2 на v3! ⚠️⚠️⚠️ |
|---|
Лёгкая и очень функциональная библиотека для энкодера с кнопкой, энкодера или кнопки с Arduino
Примеры сценариев использования:
Совместима со всеми Arduino платформами (используются Arduino-функции)
Читай более подробную инструкцию по установке библиотек здесь
Рекомендую всегда обновлять библиотеку: в новых версиях исправляются ошибки и баги, а также проводится оптимизация и добавляются новые фичи
Через менеджер библиотек IDE: найти библиотеку как при установке и нажать "Обновить"
Вручную: удалить папку со старой версией, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!
Библиотека поддерживает все 4 типа инкрементальных энкодеров, тип можно настроить при помощи setEncType(тип):
EB_STEP4_LOW - активный низкий сигнал (подтяжка к VCC). Полный период (4 фазы) за один щелчок. Установлен по умолчаниюEB_STEP4_HIGH - активный высокий сигнал (подтяжка к GND). Полный период (4 фазы) за один щелчокEB_STEP2 - половина периода (2 фазы) за один щелчокEB_STEP1 - четверть периода (1 фаза) за один щелчок, а также энкодеры без фиксацииДля работы по сценарию "энкодер с кнопкой" рекомендую вот такие (ссылка, ссылка) круглые китайские модули с распаянными цепями антидребезга (имеют тип EB_STEP4_LOW по классификации выше):

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

Примечание: по умолчанию в библиотеке пины энкодера настроены на
INPUTс расчётом на внешнюю подтяжку. Если у вас энкодер без подтяжки - можно использовать внутреннююINPUT_PULLUP, указав это при инициализации энкодера (см. документацию ниже).
Кнопка может быть подключена к микроконтроллеру двумя способами и давать при нажатии высокий или низкий сигнал. В библиотеке предусмотрена настройка setBtnLevel(уровень), где уровень - активный сигнал кнопки:
HIGH - кнопка подключает VCC. Установлен по умолчанию в Virt-библиотекахLOW - кнопка подключает GND. Установлен по умолчанию в основных библиотекахВ схемах с микроконтроллерами чаще всего используется подключение кнопки к GND с подтяжкой пина к VCC. Подтяжка может быть внешней (режим пина нужно поставить INPUT) или внутренней (режим пина INPUT_PULLUP). В "реальных" проектах рекомендуется внешняя подтяжка, т.к. она менее подвержена помехам - у внутренней слишком высокое сопротивление.
Объявлять до подключения библиотеки
// отключить поддержку pressFor/holdFor/stepFor и счётчик степов (экономит 2 байта оперативки)
#define EB_NO_FOR
// отключить обработчик событий attach (экономит 2 байта оперативки)
#define EB_NO_CALLBACK
// отключить счётчик энкодера [VirtEncoder, Encoder, EncButton] (экономит 4 байта оперативки)
#define EB_NO_COUNTER
// отключить буферизацию энкодера (экономит 2 байта оперативки)
#define EB_NO_BUFFER
/*
Настройка таймаутов для всех классов
- Заменяет таймауты константами, изменить их из программы (SetXxxTimeout()) будет нельзя
- Настройка влияет на все объявленные в программе кнопки/энкодеры
- Экономит 1 байт оперативки на объект за каждый таймаут
- Показаны значения по умолчанию в мс
- Значения не ограничены 4000мс, как при установке из программы (SetXxxTimeout())
*/
#define EB_DEB_TIME 50 // таймаут гашения дребезга кнопки (кнопка)
#define EB_CLICK_TIME 500 // таймаут ожидания кликов (кнопка)
#define EB_HOLD_TIME 600 // таймаут удержания (кнопка)
#define EB_STEP_TIME 200 // таймаут импульсного удержания (кнопка)
#define EB_FAST_TIME 30 // таймаут быстрого поворота (энкодер)
#define EB_TOUT_TIME 1000 // таймаут действия (кнопка и энкодер)
Как работать с документацией: EncButton начиная с версии 3.0 представляет собой несколько библиотек (классов) для различных сценариев использования, они друг друга наследуют для расширения функциональности. Таким образом библиотека представляет собой "луковицу", каждый слой которой имеет доступ к функциям нижних слоёв:
VirtButton - базовый класс виртуальной кнопки, обеспечивает все возможности кнопкиVirtEncoder - базовый класс виртуального энкодера, определяет факт и направление вращения энкодераVirtEncButton - базовый класс виртуального энкодера с кнопкой, обеспечивает опрос энкодера с учётом кнопки, наследует VirtButton и VirtEncoderButton, ButtonT - класс кнопки, наследует VirtButtonEncoder, EncoderT - класс энкодера, наследует VirtEncoderEncButton, EncButtonT - класс энкодера с кнопкой, наследует VirtEncButton, VirtButton, VirtEncoderТаким образом для изучения всех доступных функций конкретной библиотеки нужно смотреть не только её, но и то что она наследует. Например для обработки кнопки при помощи Button нужно открыть ниже описание Button и VirtButton.
Виртуальный - без указания пина микроконтроллера, работает напрямую с переданным значением, например для опроса кнопок-энкодеров через расширители пинов и сдвиговые регистры.
T-версии библиотек требуют указания пинов константами (цифрами). Номера пинов будут храниться в памяти программы, это ускоряет работу и делает код легче на 1 байт за каждый пин.Примечание:
#include <EncButton.h>подключает все инструменты библиотеки!
Во всех библиотеках есть общая функция обработки (тикер tick), которая получает текущий сигнал с кнопки и энкодера
true при наступлении события (для энкодера - 1 или -1 при повороте, 0 при его отсутствии. Таким образом поворот в любую сторону расценивается как true)ISR, см. документацию нижеБиблиотека обрабатывает сигнал внутри этой функции, результат можно получить из функций опроса событий. Они бывают двух типов:
[событие] - функция вернёт true однократно при наступлении события. Сбросится после следующего вызова функции обработки (например клик, поворот энкодера). За исключением события timeout[состояние] - функция возвращает true, пока активно это состояние (например кнопка удерживается)Для простоты восприятия функцию обработки нужно размещать в начале цикла, а опросы делать ниже:
void loop() {
btn.tick(); // опрос
if (btn.click()) Serial.println("click"); // однократно выведет при клике
if (btn.click()) Serial.println("click"); // тот же клик!
}
В отличие от предыдущих версий библиотеки, функции опроса сбрасываются не внутри себя, а внутри функции обработки. Таким образом в примере выше при клике по кнопке в порт дважды выведется сообщение
click(). Это позволяет использовать функции опроса по несколько раз за текущую итерацию цикла для создания сложной логики работы программы.
По очевидным причинам нельзя вызывать функцию обработки больше одного раза за цикл - каждый следующий вызов сбросит события от предыдущего и код будет работать некорректно. Вот так - нельзя:
// так нельзя
void loop() {
btn.tick();
if (btn.click()) ...
// ....
btn.tick();
if (btn.hold()) ...
}
Если очень нужно попасть в глухой цикл и опрашивать там кнопку, то вот так - можно:
// так можно
void loop() {
btn.tick();
if (btn.click()) ...
while (true) {
btn.tick();
if (btn.hold()) ...
if (btn.click()) break;
}
}
Если библиотека используется с подключенным обработчиком событий attach() (см. ниже), то можно вызывать tick() где угодно и сколько угодно раз, события будут обработаны в обработчике:
// так можно
void cb() {
switch (btn.action()) {
// ...
}
}
void setup() {
btn.attach(cb);
}
void loop() {
btn.tick();
// ...
btn.tick();
// ...
btn.tick();
}
Библиотека EncButton - асинхронная: она не ждёт, пока закончится обработка кнопки, а позволяет программе выполняться дальше. Это означает, что для корректной работы библиотеки основной цикл программы должен выполняться как можно быстрее и не содержать задержек и других "глухих" циклов внутри себя. Для обеспечения правильной обработки кнопки не рекомендуется иметь в основном цикле задержки длительностью более 50-100 мс. Несколько советов:
loop()delay() можно сделать асинхронной при помощи millis()if (!button.busy()) { тяжёлый код }yield() (внутри delay())Имеет смысл только при ручном опросе событий! При подключенной функции-обработчике достаточно вызывать обычный
tick()между тяжёлыми участками программы
Также в загруженной программе можно разделить обработку и сброс событий: вместо tick() использовать tickRaw() между тяжёлыми участками кода и ручной сброс clear(). Порядок следующий:
clear()Вызывать tickRaw() между тяжёлыми участками кода
void loop() {
if (btn.click()) ...
if (btn.press()) ...
if (btn.step()) ...
btn.clear();
// ...
btn.tickRaw();
// ...
btn.tickRaw();
// ...
btn.tickRaw();
// ...
}
Это позволит опрашивать кнопку/энкодер в не очень хорошо написанной программе, где основной цикл завален тяжёлым кодом. Внутри tickRaw() накапливаются события, которые раз в цикл разбираются, а затем вручную сбрасываются.
В этом сценарии буферизация энкодера в прерывании не работает и не обрабатываются все события
releaseXxx
Если сложно избавиться от delay() внутри главного цикла программы, то на некоторых платформах можно поместить свой код внутри него. Таким образом можно получить даже обработку энкодера в цикле с дилеями без использования прерываний:
// вставка кода в delay
void yield() {
eb.tickRaw();
}
void loop() {
if (eb.click()) ...
if (btn.turn()) ...
eb.clear();
// ...
delay(10);
// ...
delay(50);
// ...
}
В этом сценарии буферизация энкодера в прерывании не работает и не обрабатываются все события
releaseXxx
Библиотека обрабатывает кнопку следующим образом:
press, состояния pressing и busyhold, состояние holdingstep, срабатывает с периодом step пока кнопка удерживаетсяrelease, снятие состояний pressing и holding
clickreleaseHoldreleaseStepreleaseHold и releaseStep взаимоисключающие, если кнопка была удержана до step - releaseHold уже не сработаетwaitingbusy, обработка закончена
getClicks() сбрасывается после событий releaseHold/releaseStep, которые проверяют предварительные клики. В общем обработчике action() это события EB_REL_HOLD_C или EB_REL_STEP_ChasClicks, а также можно опросить внутри почти всех событий кнопки, которые идут до releaseXxxtimeout - событие timeout периодом из setTimeouttick()Отличие
click(n)отhasClicks(n):click(n)вернётtrueв любом случае при совпадении количества кликов, даже если будет сделано больше кликов.hasClicks(n)вернётtrueтолько в том случае, если было сделано ровно указанное количество кликов и больше кликов не было!Лучше один раз увидеть, чем сто раз прочитать. Запусти пример demo и понажимай на кнопку, или попробуй онлайн-симуляцию в Wokwi
Онлайн-симуляция доступна здесь
tick()Доступ к счётчику энкодера counter - это публичная переменная класса, можно делать с ней всё что угодно:
Serial.println(eb.counter); // читать
eb.counter += 1234; // менять
eb.counter = 0; // обнулять
release. Состояния нажатой кнопки не изменяютсяtimeout()) - сработает через указанное время после поворота энкодераБиблиотека считает количество кликов по кнопке и некоторые функции опроса могут отдельно обрабатываться с предварительными кликами. Например 3 клика, затем удержание. Это очень сильно расширяет возможности одной кнопки. Есть два варианта работы с такими событиями:
// 1
if (btn.hold()) {
if (btn.getClicks() == 2) Serial.println("hold 2 clicks");
}
// 2
if (btn.hold(2)) Serial.println("hold 2 clicks");
В первом варианте можно получить количество кликов для дальнейшей обработки вручную, а во втором - библиотека сделает это сама, если количество кликов для действия заранее известно.
В некоторых сценариях бывает нужно получить состояние кнопки "здесь и сейчас", например определить удерживается ли кнопка сразу после запуска микроконтроллера (старта программы). Функцию tick() нужно вызывать постоянно в цикле, чтобы шла обработка кнопки с гашением дребезга контактов и прочими расчётами, поэтому конструкция следующего вида работать не будет:
void setup() {
btn.tick();
if (btn.press()) Serial.println("Кнопка нажата при старте");
}
Для таких сценариев помогут следующие функции, возвращают true если кнопка нажата:
read() для библиотек Button и ButtonTreadBtn() для библиотек EncButton и EncButtonTОпрос кнопки выполняется с учётом настроенного ранее уровня кнопки (setBtnLevel)! Вручную дополнительно инвертировать логику не нужно:
void setup() {
// btn.setBtnLevel(LOW); // можно настроить уровень
if (btn.read()) Serial.println("Кнопка нажата при старте");
}
Допустим нужно обработать кнопку синхронно и с гашением дребезга. Например если кнопка зажата при старте микроконтроллера - получить её удержание или даже импульсное удержание внутри блока setup, то есть до начала выполнения основной программы. Можно воспользоваться состоянием busy и опрашивать кнопку из цикла:
void setup() {
Serial.begin(115200);
btn.tick();
while (btn.busy()) {
btn.tick();
if (btn.hold()) Serial.println("hold");
if (btn.step()) Serial.println("step");
}
Serial.println("program start");
}
Как это работает: первый тик опрашивает кнопку, если кнопка нажата - сразу же активируется состояние busy и система попадает в цикл while. Внутри него продолжаем тикать и получать события с кнопки. Когда кнопка будет отпущена и сработают все события - флаг busy опустится и программа автоматически покинет цикл. Можно переписать эту конструкцию на цикл с постусловием, более красиво:
do {
btn.tick();
if (btn.hold()) Serial.println("hold");
if (btn.step()) Serial.println("step");
} while (btn.busy());
В связанных с кнопкой классах (Button, EncButton) есть функция timeout() - она однократно вернёт true, если после окончания действий с кнопкой/энкодером прошло указанное в setTimeout время. Это можно использовать для сохранения параметров после ввода, например:
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() возвращает true, пока идёт обработка кнопки, т.е. пока система ожидает действий и выхода таймаутов. Это можно использовать для оптимизации кода, например избегать каких то долгих и тяжёлых частей программы на время обработки кнопки:
void loop() {
eb.tick();
// ...
if (!eb.busy()) {
// потенциально долгий и тяжёлый код
}
}
Доступно во всех классах с кнопкой:
VirtButtonButtonVirtEncButtonEncButtonФункция 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:
switch (eb.action()) {
case EB_PRESS:
// ...
break;
case EB_HOLD:
// ...
break;
// ...
}
Есть аналогичная функция getAction(), вернёт то же самое, но с более читаемыми константами (удобно с автодополнением):
EBAction::PressEBAction::HoldEBAction::StepEBAction::ReleaseEBAction::ClickEBAction::ClicksEBAction::TurnEBAction::ReleaseHoldEBAction::ReleaseHoldClicksEBAction::ReleaseStepEBAction::ReleaseStepClicksEBAction::TimeoutПолученный код события можно обработать через switch:
switch (eb.getAction()) {
case EBAction::Press:
// ...
break;
case EBAction::Hold:
// ...
break;
// ...
}
Результат функций
action()/getAction()сбрасывается после следующего вызоваtick(), то есть доступен на всей текущей итерации основного цикла
Для максимального уменьшения веса библиотеки (в частности в оперативной памяти) нужно задавать тайматуы константами через define (экономия 1 байт за таймаут), отключить обработчик событий, счётчики-буферы и использовать T-класс (экономия 1 байт за пин):
#define EB_NO_FOR
#define EB_NO_CALLBACK
#define EB_NO_COUNTER
#define EB_NO_BUFFER
#define EB_DEB_TIME 50 // таймаут гашения дребезга кнопки (кнопка)
#define EB_CLICK_TIME 500 // таймаут ожидания кликов (кнопка)
#define EB_HOLD_TIME 600 // таймаут удержания (кнопка)
#define EB_STEP_TIME 200 // таймаут импульсного удержания (кнопка)
#define EB_FAST_TIME 30 // таймаут быстрого поворота (энкодер)
#define EB_TOUT_TIME 1000 // таймаут действия (кнопка и энкодер)
#include <EncButton.h>
EncButtonT<2, 3, 4> eb;
В таком случае энкодер с кнопкой займёт в SRAM всего 8 байт, а просто кнопка - 5.
Чтобы сократить время на проверку системных флагов событий (незначительно, но приятно) можно поместить все опросы в условие по tick(), так как tick() возвращает true только при наступлении события:
void loop() {
if (eb.tick()) {
if (eb.turn()) ...;
if (eb.click()) ...;
}
}
Также опрос событий при помощи функции action() выполняется быстрее, чем ручной опрос отдельных функций событий, поэтому максимально эффективно библиотека будет работать вот в таком формате:
void loop() {
if (eb.tick()) {
switch (eb.action()) {
case EB_PRESS:
// ...
break;
case EB_HOLD:
// ...
break;
// ...
}
}
}
Для опроса состояний кнопки pressing(), holding(), waiting() можно поместить их вовнутрь условия по busy(), чтобы не опрашивать состояния пока их гарантированно нет:
if (btn.busy()) {
if (btn.pressing())...
if (btn.holding())...
if (btn.waiting())...
}
Можно подключить внешнюю функцию-обрбаотчик события, она будет вызвана при наступлении любого события. Данная возможность работает во всех классах с кнопкой:
VirtButtonButtonVirtEncButtonEncButtonВнутри коллбэка можно получить указатель на текущий объект (который вызвал коллбэк) из переменной
void* EB_self
EncButton eb(2, 3, 4);
void callback() {
switch (eb.action()) {
case EB_PRESS:
// ...
break;
case EB_HOLD:
// ...
break;
// ...
}
// здесь EB_self указатель на eb
}
void setup() {
eb.attach(callback);
}
void loop() {
eb.tick();
}
Библиотека нативно поддерживает работу с двумя одновременно нажатыми кнопками как с третьей кнопкой. Для этого нужно:
MultiButtonVirtButton, Button, EncButton + их T-версии). Мульти-кнопка сама опросит обе кнопки!Опрашивать события или слушать обработчик
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. Таким образом получается полноценная третья кнопка из двух других с удобным опросом.
Для обработки энкодера в загруженной программе нужно:
CHANGEsetEncISR(true)Основной тикер также нужно вызывать в loop для корреткной работы - события генерируются в основном тикере:
// пример для ATmega328 и EncButton
EncButton eb(2, 3, 4);
/*
// esp8266/esp32
IRAM_ATTR void isr() {
eb.tickISR();
}
*/
void isr() {
eb.tickISR();
}
void setup() {
attachInterrupt(0, isr, CHANGE);
attachInterrupt(1, isr, CHANGE);
eb.setEncISR(true);
}
void loop() {
eb.tick();
}
Примечание: использование работы в прерывании позволяет корректно обрабатывать позицию энкодера и не пропустить новый поворот. Событие с поворотом, полученное из прерывания, станет доступно после вызова tick в основном цикле программы, что позволяет не нарушать последовательность работы основного цикла:
turn активируется только один раз, независимо от количества щелчков энкодера, совершённых между двумя вызовами tick (щелчки обработаны в прерывании)turn будет вызвано столько раз, сколько реально было щелчков энкодера, это позволяет вообще не пропускать повороты и не нагружать систему в прерывании. Размер буфера - 5 необработанных щелчков энкодераПримечания:
setEncISR работает только в не виртуальных классах. Если он включен - основной тикер tick просто не опрашивает пины энкодера, что экономит процессорное время. Обработка происходит только в прерыванииIRAM_ATTR, см. документацию на свою платформу!)attach(), будет вызван из tick(), то есть не из прерывания!В виртуальных есть тикер, в который не нужно передавать состояние энкодера, если он обрабатывается в прерывании, это позволяет не опрашивать пины в холостую. Например:
VirtEncoder e;
void isr() {
e.tickISR(digitalRead(2), digitalRead(3));
}
void setup() {
attachInterrupt(0, isr, CHANGE);
attachInterrupt(1, isr, CHANGE);
e.setEncISR(1);
}
void loop() {
e.tick(); // не передаём состояния пинов
}
Для обработки кнопки в прерывании нужно:
LOW - прерывание FALLINGHIGH - прерывание RISINGВызывать pressISR() в обработчике прерывания
Button b(2);
/*
// esp8266/esp32
IRAM_ATTR void isr() {
b.pressISR();
}
*/
void isr() {
b.pressISR();
}
void setup() {
attachInterrupt(0, isr, FALLING);
}
void loop() {
b.tick();
}
Примечание: кнопка обрабатывается в основном tick(), а функция pressISR() всего лишь сообщает библиотеке, что кнопка была нажата вне tick(). Это позволяет не пропустить нажатие кнопки, пока программа была занята чем-то другим.
Создать массив можно только из нешаблонных классов (без буквы T), потому что номера пинов придётся указать уже в рантайме далее в программе. Например:
Button btns[5];
EncButton ebs[3];
void setup() {
btns[0].init(2); // указать пин
btns[1].init(5);
btns[2].init(10);
// ...
ebs[0].init(11, 12, 13, INPUT);
ebs[1].init(14, 15, 16);
// ...
}
void loop() {
for (int i = 0; i < 5; i++) btns[i].tick();
for (int i = 0; i < 3; i++) ebs[i].tick();
if (btns[2].click()) Serial.println("btn2 click");
// ...
}
Библиотека поддерживает задание своих функций для чтения пина и получения времени без редактирования файлов библиотеки. Для этого нужно реализовать соответствующую функцию в своём .cpp или .ino файле:
bool EB_read(uint8_t pin) - для своей функции чтения пинаvoid EB_mode(uint8_t pin, uint8_t mode) - для своего аналога pinModeuint32_t EB_uptime() - для своего аналога millis()Пример:
#include <EncButton.h>
bool EB_read(uint8_t pin) {
return digitalRead(pin);
}
void EB_mode(uint8_t pin, uint8_t mode) {
pinMode(pin, mode);
}
uint32_t EB_uptime() {
return millis();
}
Иногда может понадобиться вызывать tick() не на каждой итерации, а по таймеру. Например для виртуальной кнопки с расширителя пинов, когда чтение расширителя пинов - долгая операция, и вызывать её часто не имеет смысла. Вот так делать нельзя, события будут активны в течение периода таймера!
void loop() {
// таймер на 50 мс
static uint32_t tmr;
if (millis() - tmr >= 50) {
tmr = millis();
btn.tick(readSomePin());
}
// будет активно в течение 50 мс!!!
if (btn.click()) foo();
}
В данной ситуации нужно поступить так: тикать по таймеру, там же обрабатывать события и сбрасывать флаги в конце:
void loop() {
// таймер на 50 мс
static uint32_t tmr;
if (millis() - tmr >= 50) {
tmr = millis();
// тик
btn.tick(readSomePin());
// разбор событий
if (btn.click()) foo();
// сброс флагов
btn.clear();
}
}
Либо можно подключить обработчик и вызывать clear() в конце функции:
void callback() {
switch (btn.action()) {
// ...
}
// сброс флагов
btn.clear();
}
void loop() {
// таймер на 50 мс
static uint32_t tmr;
if (millis() - tmr >= 50) {
tmr = millis();
btn.tick(readSomePin());
}
}
В случае с вызовом по таймеру антидребезг будет частично обеспечиваться самим таймером и в библиотеке его можно отключить (поставить период 0).
Для корректной работы таймаутов, состояний и счётчика кликов нужен другой подход: буферизировать прочитанные по таймеру состояния и передавать их в тик в основном цикле. Например так:
bool readbuf = 0; // буфер пина
void loop() {
// таймер на 50 мс
static uint32_t tmr;
if (millis() - tmr >= 50) {
tmr = millis();
readbuf = readSomePin(); // чтение в буфер
}
// тик из буфера
btn.tick(readbuf);
if (btn.click()) foo();
}
EncButton позволяет кнопке работать в паре с энкодером для корректного отслеживания нажатых поворотов - при нажатом повороте события с кнопки будут пропущены, т.е. не обработается удержание и клик. Допустим кнопок несколько: они могут выполнять действия как сами по себе, так и в паре с энкодером (кнопка зажата и крутится энкодер, в программе меняется выбранное кнопкой значение). Чтобы при удержании кнопка не генерировала события (удержание, степ, клики...) можно включить пропуск событий. Он будет действовать до отпускания кнопки:
if (btn.pressing() && enc.turn()) {
btn.skipEvents(); // зафиксирован поворот. Пропускаем события
// нажатый поворот
}
if (btn.click()) {
// просто клик
}
// меняем значения переменных
// поворот энкодера
if (enc.turn()) {
// меняем с шагом 5
var += 5 * enc.dir();
// меняем с шагом 1 при обычном повороте, 10 при быстром
var += enc.fast() ? 10 : 1;
// меняем с шагом 1 при обычном повороте, 10 при нажатом
var += enc.pressing() ? 10 : 1;
// меняем одну переменную при повороте, другую - при нажатом повороте
if (enc.pressing()) var0++;
else var1++;
// если кнопка нажата - доступны предварительные клики
// Выбираем переменную для изменения по предв. кликам
if (enc.pressing()) {
switch (enc.getClicks()) {
case 1: var0 += enc.dir();
break;
case 2: var1 += enc.dir();
break;
case 3: var2 += enc.dir();
break;
}
}
}
// импульсное удержание на каждом шаге инкрементирует переменную
if (btn.step()) var++;
// смена направления изменения переменной после отпускания из step
if (btn.step()) var += dir;
if (btn.releaseStep()) dir = -dir;
// изменение выбранной переменной при помощи step
if (btn.step(1)) var1++; // клик-удержание
if (btn.step(2)) var2++; // клик-клик-удержание
if (btn.step(3)) var3++; // клик-клик-клик-удержание
// если держать step больше 2 секунд - инкремент +5, пока меньше - +1
if (btn.step()) {
if (btn.stepFor(2000)) var += 5;
else var += 1;
}
// включение режима по количеству кликов
if (btn.hasClicks()) mode = btn.getClicks();
// включение режима по нескольким кликам и удержанию
if (btn.hold(1)) mode = 1; // клик-удержание
if (btn.hold(2)) mode = 2; // клик-клик-удержание
if (btn.hold(3)) mode = 3; // клик-клик-клик-удержание
// или так
if (btn.hold()) mode = btn.getClicks();
// кнопка отпущена, смотрим сколько её удерживали
if (btn.release()) {
// от 1 до 2 секунд
if (btn.pressFor() > 1000 && btn.pressFor() <= 2000) mode = 1;
// от 2 до 3 секунд
else if (btn.pressFor() > 2000 && btn.pressFor() <= 3000) mode = 2;
}
// ВИРТУАЛЬНЫЕ
VirtEncButton eb; // энкодер с кнопкой
VirtButton b; // кнопка
VirtEncoder e; // энкодер
// РЕАЛЬНЫЕ
// энкодер с кнопкой
EncButton eb(enc0, enc1, btn); // пины энкодера и кнопки
EncButton eb(enc0, enc1, btn, modeEnc); // + режим пинов энкодера (умолч. INPUT)
EncButton eb(enc0, enc1, btn, modeEnc, modeBtn); // + режим пина кнопки (умолч. INPUT_PULLUP)
EncButton eb(enc0, enc1, btn, modeEnc, modeBtn, btnLevel); // + уровень кнопки (умолч. LOW)
// шаблонный
EncButton<enc0, enc1, btn> eb; // пины энкодера и кнопки
EncButton<enc0, enc1, btn> eb(modeEnc); // + режим пинов энкодера (умолч. INPUT)
EncButton<enc0, enc1, btn> eb(modeEnc, modeBtn); // + режим пина кнопки (умолч. INPUT_PULLUP)
EncButton<enc0, enc1, btn> eb(modeEnc, modeBtn, btnLevel); // + уровень кнопки (умолч. LOW)
// кнопка
Button b(pin); // пин
Button b(pin, mode); // + режим пина кнопки (умолч. INPUT_PULLUP)
Button b(pin, mode, btnLevel); // + уровень кнопки (умолч. LOW)
// шаблонный
ButtonT<pin> b; // пин
ButtonT<pin> b(mode); // + режим пина кнопки (умолч. INPUT_PULLUP)
ButtonT<pin> b(mode, btnLevel); // + уровень кнопки (умолч. LOW)
// энкодер
Encoder e(enc0, enc1); // пины энкодера
Encoder e(enc0, enc1, mode); // + режим пинов энкодера (умолч. INPUT)
// шаблонный
EncoderT<enc0, enc1> e; // пины энкодера
EncoderT<enc0, enc1> e(mode); // + режим пинов энкодера (умолч. INPUT)
| v2 | v3 |
|---|---|
held() |
hold() |
hold() |
holding() |
state() |
pressing() |
setPins() |
init() |
clearFlags() заменена на clear() (сбросить флаги событий) и reset() (сбросить системные флаги обработки, закончить обработку)В v3 функции опроса событий (click, turn...) не сбрасываются сразу после своего вызова - они сбрасываются при следующем вызове tick(), таким образом сохраняют своё значение во всех последующих вызовах на текущей итерации главного цикла программы. Поэтому tick() нужно вызывать только 1 раз за цикл, иначе будут пропуски действий! Читай об этом выше.
Остальные примеры смотри в examples!
Полное демо EncButton
// #define EB_NO_FOR // отключить поддержку pressFor/holdFor/stepFor и счётчик степов (экономит 2 байта оперативки)
// #define EB_NO_CALLBACK // отключить обработчик событий attach (экономит 2 байта оперативки)
// #define EB_NO_COUNTER // отключить счётчик энкодера (экономит 4 байта оперативки)
// #define EB_NO_BUFFER // отключить буферизацию энкодера (экономит 1 байт оперативки)
// #define EB_DEB_TIME 50 // таймаут гашения дребезга кнопки (кнопка)
// #define EB_CLICK_TIME 500 // таймаут ожидания кликов (кнопка)
// #define EB_HOLD_TIME 600 // таймаут удержания (кнопка)
// #define EB_STEP_TIME 200 // таймаут импульсного удержания (кнопка)
// #define EB_FAST_TIME 30 // таймаут быстрого поворота (энкодер)
// #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());
}
}
#include <EncButton.h>
EncButton eb(2, 3, 4);
void callback() {
Serial.print("callback: ");
switch (eb.action()) {
case EB_PRESS:
Serial.println("press");
break;
case EB_HOLD:
Serial.println("hold");
break;
case EB_STEP:
Serial.println("step");
break;
case EB_RELEASE:
Serial.println("release");
break;
case EB_CLICK:
Serial.println("click");
break;
case EB_CLICKS:
Serial.print("clicks ");
Serial.println(eb.getClicks());
break;
case EB_TURN:
Serial.print("turn ");
Serial.print(eb.dir());
Serial.print(" ");
Serial.print(eb.fast());
Serial.print(" ");
Serial.println(eb.pressing());
break;
case EB_REL_HOLD:
Serial.println("release hold");
break;
case EB_REL_HOLD_C:
Serial.print("release hold clicks ");
Serial.println(eb.getClicks());
break;
case EB_REL_STEP:
Serial.println("release step");
break;
case EB_REL_STEP_C:
Serial.print("release step clicks ");
Serial.println(eb.getClicks());
break;
}
}
void setup() {
Serial.begin(115200);
eb.attach(callback);
}
void loop() {
eb.tick();
}
#include <EncButton.h>
Button btn(4);
ButtonT<5> btnt;
VirtButton btnv;
void setup() {
Serial.begin(115200);
}
void loop() {
// Button
btn.tick();
if (btn.click()) Serial.println("btn click");
// ButtonT
btnt.tick();
if (btnt.click()) Serial.println("btnt click");
// VirtButton
btnv.tick(!digitalRead(4)); // передать логическое значение
if (btnv.click()) Serial.println("btnv click");
}
#include <EncButton.h>
Encoder enc(2, 3);
EncoderT<5, 6> enct;
VirtEncoder encv;
void setup() {
Serial.begin(115200);
}
void loop() {
// опрос одинаковый для всех, 3 способа:
// 1
// tick вернёт 1 или -1, значит это шаг
if (enc.tick()) Serial.println(enc.counter);
// 2
// можно опросить через turn()
enct.tick();
if (enct.turn()) Serial.println(enct.dir());
// 3
// можно не использовать опросные функции, а получить направление напрямую
int8_t v = encv.tick(digitalRead(2), digitalRead(3));
if (v) Serial.println(v); // выведет 1 или -1
}
При нахождении багов создавайте Issue, а лучше сразу пишите на почту alex@alexgyver.ru
Библиотека открыта для доработки и ваших Pull Request'ов!
При сообщении о багах или некорректной работе библиотеки нужно обязательно указывать: