Skip to content

GyverLibs/GyverMenu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

latest PIO Foo Foo Foo

Foo

GyverMenu

Динамическая система меню для Arduino

  • Сборка меню в "билдере" с возможностью вывода виджетов по условию или в цикле
  • Механизм вложенных страниц с памятью навигации
  • Удобный API для навигации любым количеством кнопок/джойстиком/энкодером
  • Вывод на любой дисплей или в монитор порта
  • Выравнивание значений по правому краю с поддержкой кириллицы
  • 9 встроенных виджетов
  • Создание своих виджетов
  • Механизм обновления виджетов
  • Оптимизация количества перерисовок экрана (настраивается)
  • Из коробки это текстовое меню, но можно сделать свои виджеты для графического дисплея
gmenu2.mp4

Демо проект в симуляторе

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

Совместима со всеми платформами

Зависимости

Содержание

Описание классов

GyverMenu

// столбцов (длина строки), строк
GyverMenu(uint8_t cols, uint8_t rows);

// подключить рендер вида void(const char* str, size_t len). Придёт nullptr после окончания вывода
void onPrint(gm::Menu::PrintCb cb);

// подключить установку курсора вида uint8_t(uint8_t row, bool state)
void onCursor(gm::Menu::CursorCb cb);

// подключить билдер вида void(gm::Builder& b)
void onBuild(gm::Builder::BuildCb cb);

// установить текст кнопки "назад"
void setBackSign(const char* sign);

// обновить строку с переменной
void update(void* var);

// обновить экран
void refresh();

// на предыдущее меню
void back();

// в главное меню
void home();

// кнопка выбора
void set();

// кнопка вверх
void up();

// кнопка вниз
void down();

// уменьшить напрямую
void left();

// увеличить напрямую
void right();

// обновлять экран полностью, например для вывода в консоль (умолч. false)
void setFullRefresh(bool full);

// включить быстрый курсор - рендерить только курсор при смене строки (умолч. true)
void setFastCursor(bool fast);

gm::Builder

Страница

// начать страницу (подменю)
bool PageBegin(uint8_t id, const __FlashStringHelper* label);
bool PageBegin(uint8_t id, const char* label);

// закончить страницу (вызывать внутри условия по PageBegin). back - выводить кнопку "назад"
void PageEnd(bool back = true);

// страница с коллбэком (вместо PageBegin-PageEnd). back - выводить кнопку "назад"
void Page(uint8_t id, const __FlashStringHelper* label, void (*page)(Builder& b), bool back = true);
void Page(uint8_t id, const char* label, void (*page)(Builder& b), bool back = true);

Виджеты

// кнопка
bool Button(const __FlashStringHelper* label, void (*cb)() = nullptr);
bool Button(const char* label, void (*cb)() = nullptr);

// просто текст
void Label(const __FlashStringHelper* line);
void Label(const char* line);

// выключатель
bool Switch(const __FlashStringHelper* label, bool* var, void (*cb)(bool v) = nullptr);
bool Switch(const char* label, bool* var, void (*cb)(bool v) = nullptr);

// выбор пункта. opts - строка с разделителем ';'
bool Select(const __FlashStringHelper* label, uint8_t* var, const __FlashStringHelper* opts, void (*cb)(uint8_t n, const char* str, uint8_t len) = nullptr);
bool Select(const char* label, uint8_t* var, const char* opts, void (*cb)(uint8_t n, const char* str, uint8_t len) = nullptr);

// выбор пункта в стиле "вкладок". tabs - строка с разделителем ';'
bool Tabs(uint8_t* var, const __FlashStringHelper* tabs, void (*cb)(uint8_t n, const char* str, uint8_t len) = nullptr);
bool Tabs(uint8_t* var, const char* tabs, void (*cb)(uint8_t n, const char* str, uint8_t len) = nullptr);

// текстовое значение
void ValueStr(const __FlashStringHelper* label, const char* value);
void ValueStr(const char* label, const char* var);

// int значение
bool ValueInt(const __FlashStringHelper* label, T* var, T minv, T maxv, T step, uint8_t base, const __FlashStringHelper* unit, void (*cb)(T v) = nullptr);
bool ValueInt(const char* label, T* var, T minv, T maxv, T step, uint8_t base = 10, const char* unit = "", void (*cb)(T v) = nullptr);

// float значение
bool ValueFloat(const __FlashStringHelper* label, float* var, float minv, float maxv, float step, uint8_t dec, const __FlashStringHelper* unit, void (*cb)(float v) = nullptr);
bool ValueFloat(const char* label, float* var, float minv, float maxv, float step, uint8_t dec = 2, const char* unit = "", void (*cb)(float v) = nullptr);

Системное

// обновить экран после работы билдера
void refresh();

// было действие с каким-то из виджетов выше
bool wasSet();

// сбросить флаг чтения wasSet
void clearSet();

API

// начать виджет. true если разрешено
bool beginWidget();

// начать вывод виджета. true если разрешено
// targetVar - указатель на переменную виджета
// wCursor - рисовать ли курсор
bool beginRender(void* targetVar = nullptr, bool wCursor = true);

// получить действие виджета
Action getAction();

// поднять флаг изменения (влияет на wasSet())
void change();

// печатать в onPrint
void menu.print(char c);
void menu.print(const char* str);
void menu.print(const char* str, uint8_t len, uint8_t letters = 0);

// заполнить пробелами
void menu.pad(int8_t n);

// заполнить пробелами до конца
void menu.pad();

// переключить активное состояние (isActive)
void menu.toggle();

// текущая строка меню
uint8_t menu.currentRow();

// виджет активен (кнопкой set)
bool menu.isActive();

// виджет выбран курсором
bool menu.isChosen();

// виджет в видимой области экрана
bool menu.isVisible();

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

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

#define GM_MAX_DEPTH 5  // макс. вложенность меню (умолч. 5)
#define GM_NO_PAGES     // отключить вложенные меню (облегчает библиотеку)

Использование

Note

Документация в разработке!

Как это работает

Библиотека осуществляет навигацию по виртуальному меню, которое задаётся в билдере. В качестве точек входа библиотека имеет 5 функций виртуальных кнопок, которые нужно вызывать из основной программы по нажатию физических кнопок или другим событиям. В качестве точек выхода - три функции-обработчика: билдер, установка курсора и печать текста. Таким образом, библиотека вообще не привязана к конкретным дисплеям и способам ввода и даже к Arduino фреймворку - её можно запустить теоретически на любой платформе, само меню например выводить текстом в консоль, а кнопки "вверх" и "вниз" сделать голосовым вводом.

Кнопки

Библиотека предусматривает виртуальных 5 кнопок управления + отдельные команды "вернуться на предыдущее меню" и "на главный экран", но на практике это может быть любое другое количество кнопок - хоть одна, хоть энкодер. Логика работы такая:

  • menu.set() - центральная кнопка выбора. Она нажимает виджет Button на экране, переключает состояние виджета Switch, обрабатывает нажатие по кнопке возврата в предыдущее меню, а для остальных виджетов - переводит их в режим изменения
  • menu.up() - переместить курсор на предыдущий пункт. Если активен режим изменения - вместо этого будет вызвано увеличение значения
  • menu.down() - переместить курсор на следующий пункт. Если активен режим изменения - вместо этого будет вызвано уменьшение значения
  • menu.left() - напрямую уменьшает значение текущего виджета независимо от режима изменения. Также нажимает кнопку "назад" для возврата в предыдущее меню
  • menu.right() - напрямую увеличивает значение текущего виджета независимо от режима изменения. Также нажимает кнопку входа в подменю и виджет Button
  • menu.back() - возвращает на предыдущее меню
  • menu.home() - возвращает в главное меню

Таким образом, сценарии навигации могут быть:

  • 5 кнопок или джойстик с центральной кнопкой
  • 4 кнопки (все кроме центральной) или джойстик без центральной кнопки
  • 3 кнопки: верх + центр + низ
  • 2 кнопки: верх + низ +
    • Одновременное нажатие вызывает set
    • Удержание вниз вызывает set
    • Удержание вверх вызывает back
  • Энкодер: up и down на вращение, set на клик
  • Энкодер: up и down на вращение, set на клик, зажатый поворот как left и right
    • Удержание кнопки - back. Очень долгое удержание - home

В общем можно придумать очень много вариантов, используйте EncButton для обработки кнопок и энкодеров и вдохновляйтесь китайскими цифровыми паяльниками.

Обработчики

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

Билдер

void builder(gm::Builder& b) {
    // b - объект для вызова виджетов
}
menu.onBuild(builder);

Печать

void printer(const char* str, size_t len) {
    // str - строка для вывода на дисплей
    // если str == nullptr - отрисовка меню закончена (для случаев когда нужно обновить экран)
    // len - кол-во символов в строке
}
menu.onPrint(printer);

Курсор

uint8_t cursor(uint8_t row, bool chosen, bool active) {
    // row - текущая строка дисплея
    // chosen - выбран ли текущий виджет курсором
    // active - находится ли текущий виджет в режиме изменения
    // return количество символов (столбцов), которое занял курсор (если занял)
}
menu.onCursor(cursor);

Примеры обработчиков для LCD дисплея со стандартной библиотекой, курсор вида >, отсутствие курсора - пробел :

menu.onPrint([](const char* str, size_t len) {
    if (str) lcd.Print::write(str, len);
});

menu.onCursor([](uint8_t row, bool chosen, bool active) -> uint8_t {
    lcd.setCursor(0, row);
    lcd.print(chosen && !active ? '>' : ' ');
    return 1;
});

Обработчик курсора вызывается не только перед началом отрисовки строки, но и при смене строки. Именно для этого он сделан отдельным - при смене курсора перерисовывается только курсор, а не всё меню, что повышает скорость и отзывчивость системы. Отключить такое поведение и всегда перерисовывать строку полностью можно при помощи menu.setFastCursor(true).

Кажется что это слишком хитро - но зато гибко. Например, для OLED дисплея можно не рисовать курсор, а инвертировать вывод текста для текущей строки:

menu.onCursor([](uint8_t row, bool chosen, bool active) -> uint8_t {
    oled.setCursor(0, row);
    oled.invertText(chosen);
    return 0;
});
menu.setFastCursor(true);

Билдер и виджеты

Билдер - обработчик, который вызывается библиотекой для:

  • Отрисовки текущего выбранного виджета
  • Отрисовки всего экрана
  • Поиска и отрисовки виджета для обновления его значения из апдейта

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

Вывод

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

menu.onBuild([](gm::Builder& b) {
    b.Button("Button 1");
    b.Button("Button 2");
});

Виджеты можно создавать динамически, например 5 кнопок:

menu.onBuild([](gm::Builder& b) {
    for (int i = 0; i < 5; i++) {
        b.Button(String16("Button ") + i);
    }
});

Виджеты можно выводить по условию:

menu.onBuild([](gm::Builder& b) {
    if (foo) {
        b.Button("Button 1");
        b.Button("Button 2");
    }
});

Действия

Встроенные в библиотеку виджеты имеют три механизма взаимодействия:

  • Функция активного виджета возвращает true при изменении значения (и клик для виджета кнопки)
  • К активным виджетам можно подключить функцию-обработчик, которая будет вызвана при изменении значения (также в неё будет отправлено новое значение)
  • К виджетам со значением подключается переменная, которая будет автоматически изменяться библиотекой
void clickHandler() {
    Serial.println("Btn 2 click");
}

bool sw;

menu.onBuild([](gm::Builder& b) {
    // условие
    if (b.Button("Button 1")) {
        Serial.println("Btn 1 click");
    }

    // обработчик
    b.Button("Button 2", clickHandler);

    // подключение переменной
    b.Switch("Switch 1", &sw);
    
    // подключение переменной + обработчик
    b.Switch("Switch 2", &sw, [](bool v) { Serial.println(v); });
});

По действию с виджета можно обновить меню, например выключатель отвечает за вывод нескольких кнопок. Кнопки активные, обработка клика работает даже при выводе через цикл:

bool btns;

menu.onBuild([](gm::Builder& b) {
    if (b.Switch("Buttons", &btns)) b.refresh();    // обновить при клике

    if (btns) {
        for (int i = 0; i < 5; i++) {
            if (b.Button(String16("Button ") + i)) {
                Serial.println(String16("Button") + i);
            }
        }
    }
});

Трюки

У виджетов с настройкой шага изменения шаг можно задавать динамически, например от скорости вращения энкодера. С библиотекой EncButton это может выглядеть так:

int vali;

menu.onBuild([](gm::Builder& b) {
    b.ValueInt<int>("ValueInt", &vali, -100, 100, encb.fast() ? 10 : 1);
});

Т.е. при быстром вращении энкодера шаг будет 10 (грубый), а при медленном - 1 (точный). Билдер вызывается на каждом действии изменения значения, поэтому шаг будет выбираться для каждого клика.

Примеры

Версии

  • v1.0

Установка

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

Обновление

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

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

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

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

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

About

Динамическая система меню для Arduino

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages