понедельник, 7 сентября 2020 г.

LiquidCrystal_I2C_Menu - библиотека для создания меню на ЖК дисплее и Ардуино. Новые возможности

Меню на Ардуино


В одной из прошлых публикаций я писал о своей библиотеке LiquidCrystal_I2C_Ext для создания меню на текстовом LCD дисплее и энкодере вращения. Библиотека получилась довольно интересная. Только название, как выяснилось впоследствии, уже используется. Поэтому я принял решение переименовать её в LiquidCrystal_I2C_Menu. Кроме нового имени в ней появились и новые возможности. А недавно я сделал версию библиотеки для управления с помощью кнопок - LiquidCrystal_I2C_Menu_Btns. Итого на ваш выбор представлены две библиотеки, о которых пойдет речь в сегодняшней статье.

Где качать

Библиотеки находятся на github по ссылкам:
https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu - управление энкодером

Чтобы скачать интересующую библиотеку необходимо нажать на кнопку Code, затем Download ZIP:





Для установки библиотеки в среду Arduino распакуйте содержимое архива в папку Arduino_dir\libraries\ и удалите из названия "-master".

Как подключать

Библиотеки предназначены для работы с дисплеями LCD2004 и LCD1602 с I2C интерфейсом. Для управления потребуются либо энкодер вращения с кнопкой, например, KY-040, либо 3-4 кнопки. Схемы их подключения к Ардуино приведены ниже:

Схема подключения дисплея и энкодера к Ардуино для работы с LiquidCrystal_I2C_Menu

Схема подключения дисплея и кнопок к Ардуино для работы с LiquidCrystal_I2C_Menu_Btns

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

Для библиотеки LiquidCrystal_I2C_Menu_Btns достаточно трёх кнопок. Четвертая опциональная, используется для отмены действий и возврата из функций.

Как использовать

Обе библиотеки одинаковы в использовании, за исключением функций подключения и опроса элементов управления (attachEncoder / attachButtons и getEncoderState / getButtonsState). В остальном они взаимозаменяемы. Помимо унаследованных из LiquidCrystal_I2C функций в них реализованы следующие:

  • printAt(x, y, text) – вывод текста на дисплей с указанной позиции.
  • printf(format, …) – форматированный вывод текста. Лично мне очень не хватало этой функции. Если раньше приходилось делать несколько вызовов print, или выводить текст в промежуточный буфер функцией sprintf, то сейчас достаточно одного вызова printf.
  • printfAt(x, y, format, …) – форматированный вывод с указанной позиции.
  • attachEncoder(pinA, pinB, pinBtn) – сообщает библиотеке, к каким выводам Ардуино подключен энкодер. Только для LiquidCrystal_I2C_Menu.
  • getEncoderState() – опрос состояния энкодера. Возвращает значение типа eEncoderState (перечисляемый тип, описан в библиотеке как {eNone, eLeft, eRight, eButton}). Только для LiquidCrystal_I2C_Menu.
  • attachButtons(pinLeft, pinRight, pinEnter, [pinBack]) – сообщает библиотеке, к каким выводам Ардуино подключены кнопки. Только для LiquidCrystal_I2C_Menu_Btns.
  • getButtonsState() – опрос состояния кнопок. Возвращает значение типа eButtonsState (перечисляемый тип, описан в библиотеке как {eNone, eLeft, eRight, eButton, eBack}). Только для LiquidCrystal_I2C_Menu_Btns.
  • printMultiline(text) – вывод длинного текста с возможностью вертикальной прокрутки. Возврат из функции осуществляется при нажатии кнопки.
  • inputVal(title, min, max, default, [step = 1], [*onChangeFunc = NULL]) – ввод числового значения. title – заголовок; параметры min и max задают диапазон, в котором может изменяться значение; default – начальное значение; step – величина приращения, по умолчанию равна 1; необязательный параметр onChangeFunc – указатель на функцию, которая должна вызываться при изменении значения.
  • inputValAt(x, y, min, max, default, [step = 1], [*onChangeFunc = NULL]) – аналогична функции inputVal, но в отличие от нее не очищает дисплей при вызове и ввод значения осуществляется с указанной позиции.
  • inputValBitwise(title, value, precision, [scale = 0], [signed = false]) – позволяет вводить числовые значения путем редактирования отдельных разрядов числа. Параметр title определяет заголовок; value – ссылка на переменную, в которую будет помещен результат ввода; precision – общее количество разрядов в числе; scale – количество разрядов после запятой, значение по умолчанию 0; signed – разрешает (при значении true) или запрещает (при значении false – по умолчанию) ввод отрицательных чисел. Функция возвращает true, если пользователь подтвердил ввод, false, если отказался.
  • inputStrVal(title, buffer, length, available_symbols) – аналогично функции inputValBitwise предоставляет возможность поразрядного ввода, но кроме цифр могут быть введены и другие символы. Параметр title определяет заголовок; buffer – ссылка на символьный буфер, в который будет помещен результат ввода; length – количество вводимых символов; параметр available_symbols – это строка символов, доступных для ввода. Функция возвращает true, если пользователь подтвердил ввод, false, если отказался.
  • selectVal(title, list_of_values, count, [show_selected = true], [selected_index = 0]) – позволяет выбрать значение из списка list и возвращает индекс выбранного элемента.  title – отображаемый на дисплее заголовок; list – список значений для выбора, представляет собой массив значений типа char*, String или int; count – количество элементов в массиве; show_selected - флаг отображения метки на выбранном элементе; selected_index – индекс выбранного по умолчанию элемента.
  • showMenu(menu, menu_length, show_title) – отображает меню и возвращает ключ выбранного элемента. menu – массив элементов типа sMenuItem; menu_length – длина меню; show_title – признак необходимости отображения заголовка.
  • getSelectedMenuItem() – возвращает ключ выбранного пункта меню для использования внутри обработчиков.
  • attachIdleFunc(*IdleFunc) – позволяет привязать функцию, которая будет вызываться библиотекой при бездействии.
 Разберём несколько примеров их использования.

printAt, printf, printfAt

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

void setup() {
  char hello[] = "Hello, world!";
  String s = "String example";
  lcd.begin();
  lcd.printf("millis=%lu", millis());
  lcd.printAt(3, 1, hello);
  lcd.printAt(0, 2, s);
  lcd.printfAt(0, 3, "%s", s.c_str());
}

void loop() {

}


Результат работы функций printAt, printf, printfAt


Функция printAt поддерживает те же типы данных, что и print: вы можете выводить на дисплей целые и дробные числа, текстовые строки, будь то массив символов или переменная типа String. А при работе с функциями форматированного вывода printf и printfAt не забывайте, что они не поддерживают тип String и передавать им нужно указатель на строку в стиле Си. Для этого достаточно вызвать функцию c_str() класса String, в примере выше это показано.

attachEncoder, getEncoderState и attachButtons, getButtonsState

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

int x = 0;

void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
  lcd.printfAt(0, 0, "%d ", x);
}

void loop() {
  // Опрашиваем энкодер
  eEncoderState EncoderState = lcd.getEncoderState();
  switch (EncoderState) {
    case eLeft:   // При вращении влево уменьшаем значение переменной
      x--;
      break;
    case eRight:  // При вращении вправо увеличиваем значение переменной
      x++;
      break;
    case eButton: // При нажатии кнопки энкодера обнуляем значение переменной
      x = 0;
      break;
    case eNone:   // Энкодер не вращается, кнопка не нажата. Выходим из функции
      return;
  }
  lcd.printfAt(0, 0, "%d ", x); // Покажем новое значение x
}
В данном примере выполняется подключение энкодера функцией attachEncoder и опрос его состояния в цикле. При вращении энкодера изменяется значение переменной x, затем выводится на дисплей. Нажатие на кнопку приводит к обнулению переменной.

Аналогичный пример для библиотеки LiquidCrystal_I2C_Menu_Btns вы найдете в папке \examples\Buttons. Отличие состоит в использовании функций attachButtons и getButtonsState. Последняя возвращает значение типа eButtonsState, которое определено в библиотеке следующим образом:
enum eButtonsState {eNone, eLeft, eRight, eButton, eBack};
Это те же значения, что используются при работе с энкодером, плюс новое значение eBack для кнопки "Назад". Подключать эту кнопку необязательно, то есть функция attachButtons может быть вызвана с тремя параметрами:
lcd.attachButtons(pinLeft, pinRight, pinEnter);

printMultiline

#include <avr/pgmspace.h>
#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

// Объявим две строки в памяти программ. Так они не будут занимать оперативную память
const char text_1[] PROGMEM = "Using PROGMEM example";
const char text_2[] PROGMEM = "This text is stored in FLASH";

const char* const text[] PROGMEM = {text_1, text_2};

void setup() {
  lcd.begin();
    lcd.attachEncoder(pinDT, pinCLK, pinSW);
}

void loop() {
  char *buffer;
  // Вывод на экран длинной строки. Строка занимает оперативную память.
  lcd.printMultiline("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua");
  
  // Вывод строки из памяти программ.
  buffer = (char*) malloc(30); // Буфер для временного хранения строки
  strcpy_P(buffer, (char*)pgm_read_word(&(text[0]))); // Копируем строку в буфер
  lcd.printMultiline(buffer); // Выводим содержимое буфера на экран
  strcpy_P(buffer, (char*)pgm_read_word(&(text[1]))); // Аналогично со второй строкой
  lcd.printMultiline(buffer);
  free(buffer); // Освобождаем буфер
  
  // Другой пример хранения строк в памяти программ - использование макроса F().
  lcd.printMultiline(F("Using F() macro example. Press button to continue."));
}

Результат работы функции printMultiline



Функция printMultiline позволяет выводить на дисплей длинные строки с возможностью прокрутки. Но, объявив несколько таких строк в своем скетче, при компиляции вы можете заметить, как быстро они "съедают" память данных. В Ардуино Уно для хранения данных доступно всего 2кб, тогда как для кода программы отведено 32кб. Поэтому при наличии в программе большого объема текстовых данных целесообразно хранить их в памяти программ (FLASH). Для этого используется ключевое слово PROGMEM. 

PROGMEM – это модификатор переменных, он сообщает компилятору, что указанная переменная должна быть размещена не в памяти данных, а в памяти программ. Работает этот модификатор только с типами данных, объявленными в файле pgmspace.h, частью которой он является. Чтобы впоследствии процессор мог что-то сделать с этими данными, они должны быть скопированы из FLASH в SRAM. Для этого в примере выше выделяется буфер, в который происходит считывание строк text[0] и text[1]. После того как данные выведены на дисплей буфер может быть освобожден.

Еще один вариант размещения строки в памяти программ – это использование макроса F() непосредственно в функции printMultiline. В примере это также показано.

inputVal, inputValAt

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
}

void loop() {
  // Для примера запросим длину массива
  uint8_t len = lcd.inputVal("Input array len", 5, 10, 8);
  uint8_t A[len];
  uint8_t t;
  
  // Затем элементы массива
  for (uint8_t i = 0; i < len; i++) {
    lcd.printfAt(0, 0, "Input A[%d]: ", i); // Приглашение для ввода
    A[i] = lcd.inputValAt(12, 0, 0, 9, 5);  // Ввод значения
  }
  
  // Отсортируем массив
  for (uint8_t i = 0; i < len - 1; i++) {
    for (uint8_t j = i + 1; j < len; j++) {
      if(A[i] > A[j]){
        t = A[i];
        A[i] = A[j];
        A[j] = t;
      }
    }
  }
  
  // И выведем на дисплей
  lcd.clear();
  lcd.print("Sorted array:");
  lcd.setCursor(0, 1);
  for (uint8_t i = 0; i < len; i++)
    lcd.printf("%d ", A[i]);
  
  // Ожидаем нажатия кнопки для продолжения
  while (lcd.getEncoderState() == eNone);
}

Использование функций printfAt и inputValAt для ввода значений


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

В некоторых случаях необходимо знать значение переменной в процессе её редактирования, а не по завершении редактирования. Это позволило бы оперативно применять новое значение. Тогда, например, при изменении громкости мы сможем не только видеть её новое значение на дисплее, но и воспринимать изменение на слух. Для таких случаев в функциях inputVal и inputValAt предусмотрен параметр onChangeFunc. В нём передаётся указатель на функцию, которая должна вызываться при каждом изменении редактируемого параметра. Внутри такой функции можно применить новое значение параметра или даже визуализировать его ввод: отобразить шкалу как в примере ниже.


Шкала ввода для функций inputVal и inputValAt

В примерах к библиотеке есть скетч inputVal_onChange, поясняющий использование параметра onChangeFunc.

inputValBitwise

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

double val = 0;

void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
}

void loop() {
  // Ввод 5-значного числа со знаком, 2 цифры после запятой:
  if (lcd.inputValBitwise("Input value", val, 5, 2, true)) {
    lcd.print("You entered: ");
    lcd.print(val);
  }
  else
    lcd.print("Input canceled");
  while (lcd.getEncoderState() == eNone);
}


Пример использования функции inputValBitwise для поразрядного ввода чисел


Параметры, с которыми вызывается функция inputValBitwise в данном скетче, определяют ввод пятиразрядного числа со знаком, два младших разряда отведены для дробной части. При подтверждении ввода функция возвращает true и помещает введённое значение в переменную val.

Ещё несколько примеров ввода чисел функциями inputVal и inputValBitwise вы найдёте в скетче Input_double_and_long.


inputStrVal

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

char ip[] = "192.168.001.001"; // Массив символов с начальным значением/маской

void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
}

void loop() {
  if (lcd.inputStrVal("Input IP", ip, 15, "0123456789")){
    lcd.print("You entered:");
    lcd.printAt(0, 1, ip);
  }
  else
    lcd.print("Input canceled");
  while (lcd.getEncoderState() == eNone);
}

Пример использования функции inputStrVal для маскированного ввода


Здесь функция inputStrVal используется для ввода IP адреса. При вызове функции содержимое переданного буфера рассматривается как значение по умолчанию и выводится на дисплей. Последний параметр функции определяет разрешенные для ввода символы, в данном случае только цифры. Если буфер содержит символы, которые не могут быть введены, то их не удастся изменить. Таким способом можно реализовать маскированный ввод или, например, показать единицу измерения для вводимого значения.

selectVal

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

int index = 0;
  
void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
}

void loop() {
  String list[] = {"Europa+", "Record", "DFM", "Retro FM", "Energy"};
  index = lcd.selectVal("Select station", list, 5, true, index);
  lcd.printf("%s selected", list[index].c_str());
  while (lcd.getEncoderState() == eNone);
}

Пример работы функции selectVal


Функция selectVal используется для выбора значения из списка. Она работает с массивами значений типа char*, String или int и возвращает индекс выбранного элемента. Последние два параметра функции необязательны, это show_selected - определяет, должно ли визуально выделяться выбранное значение (по умолчанию true) и  preselected - задаёт индекс выбранного элемента (по умолчанию -1 - ничего не выбрано).

showMenu

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

// Объявим перечисление, используемое в качестве ключа пунктов меню
enum {mkBack, mkRoot, mkRun, mkOptions, mkMode, mkSpeed, mkLog, mkSelftest, mkHelp, mkFAQ, mkIndex, mkAbout};

// Описание меню
// структура пункта меню: {ParentKey, Key, Caption, [Handler]}
sMenuItem menu[] = {
  {mkBack, mkRoot, "Menu demo"},
    {mkRoot, mkRun, "Run"},
    {mkRoot, mkOptions, "Options"},
      {mkOptions, mkMode, "Mode"},
      {mkOptions, mkSpeed, "Speed"},
      {mkOptions, mkLog, "Print log"},
      {mkOptions, mkSelftest, "Selftest"},
      {mkOptions, mkBack, "Back"},
    {mkRoot, mkHelp, "Help"},
      {mkHelp, mkFAQ, "FAQ"},
      {mkHelp, mkIndex, "Index"},
      {mkHelp, mkAbout, "About"},
      {mkHelp, mkBack, "Back"},
    {mkRoot, mkBack, "Exit menu"}
};

uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem);

void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
}

void loop() {
  // Показываем меню
  uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1);
  // И выполняем действия в соответствии с выбранным пунктом
  if (selectedMenuItem == mkRun)
    lcd.print("Run selected");
  else if (selectedMenuItem == mkMode)
    lcd.print("Mode selected");
  else if (selectedMenuItem == mkSpeed)
    lcd.print("Speed selected");
  else if (selectedMenuItem == mkLog)
    lcd.print("Print log selected");
  else if (selectedMenuItem == mkSelftest)
    lcd.print("Selftest selected");
  else if (selectedMenuItem == mkFAQ)
    lcd.print("FAQ selected");
  else if (selectedMenuItem == mkIndex)
    lcd.print("Index selected");
  else if (selectedMenuItem == mkAbout)
    lcd.print("About selected");
  else if (selectedMenuItem == mkBack)
    lcd.print("Exit selected");
  delay(2000);
}


Пример меню, построенного функцией showMenu

Функция showMenu берёт на себя отрисовку меню и навигацию по нему. Входными параметрами функции являются массив элементов типа sMenuItem (это и есть наше меню), длина массива и признак отображения заголовка в меню. В качестве заголовка используется название родительского пункта меню. Отключение отображения заголовка полезно при использовании дисплея 1602.

Итак, меню - это массив элементов sMenuItem. Данная структура определёна в библиотеке следующим образом:

struct sMenuItem {
  uint8_t parent;
  uint8_t key;
  char    *caption;
  void    (*handler)();
};

Параметры parent и key служат для задания иерархии, caption – указатель на название элемента меню, handler - указатель на функцию, которая будет вызываться при выборе данного пункта меню. Будем называть такие функции обработчиками.

В качестве parent и key могут быть использованы целочисленные значения, начиная с 1. Но гораздо удобнее определить для них символьные имена, то есть работать с перечислением. Обратите внимание на то, что первым в перечислении должно быть определено значение mkBack, ему соответствует значение 0. Данное значение является служебным и используется для пунктов меню, отвечающих за возврат на уровень выше.

При выборе пункта меню, не имеющего подменю и обработчика, функция возвращает соответствующее ему значение key. После этого остаётся проанализировать вернувшееся значение при помощи if или case и выполнить соответствующее выбранному элементу действие.

Если же для выбранного пункта меню задан обработчик, то он будет вызван, после чего управление вернётся в меню.


getSelectedMenuItem и использование обработчиков меню

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

int brightness = 50;
int _delay = 10;

// Обработчики пунктов меню SetBrightness и SetDelay
// Используются для ввода значений brightness и _delay
void SetBrightness() {
  brightness = lcd.inputVal("Input brightness(%)", 0, 100, brightness, 5);
}

void SetDelay() {
  _delay = lcd.inputVal("Input delay(ms)", 0, 20, _delay);
}

// Объявим перечисление, используемое в качестве ключа пунктов меню
enum {mkBack, mkRoot, mkOptions, mkSetBrightness, mkSetDelay};

// Описание меню
// структура пункта меню: {ParentKey, Key, Caption, [Handler]}
sMenuItem menu[] = {
  {mkBack, mkRoot, "Main menu", NULL},
  {mkRoot, mkOptions, "Options", NULL},
  {mkOptions, mkSetBrightness, "SetBrightness", SetBrightness},
  {mkOptions, mkSetDelay, "SetDelay", SetDelay},
  {mkOptions, mkBack, "Back", NULL},
  {mkRoot, mkBack, "Back", NULL}
};

uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem);

void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
}

void loop() {
  uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1); // Вызываем меню
  /* Реакция на выбор пунктов меню SetBrightness и SetDelay реализована
   * в функциях-обработчиках.
   * При необходимости здесь может располагаться анализ значения selectedMenuItem 
   * для пунктов, не имеющих обработчиков:
     if (selectedMenuItem == ...) {...}
  */
}
В этом примере для двух пунктов меню заданы функции-обработчики SetBrightness и SetDelay. Они не имеют параметров и ничего не возвращают - это обязательное требование к обработчикам. Обработчики вызываются прямо из функции showMenu, то есть она не завершается и не возвращает ключ выбранного пункта меню. Внутри обработчиков можно как угодно работать с дисплеем, перерисовывать его и вызывать функции библиотеки, после завершения обработчика меню будет восстановлено.

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

attachIdleFunc

При вызове большинства функций библиотеки ожидается действие от пользователя: выбор пункта в меню, ввод значения и так далее. При этом мы по сути находимся в цикле и выполнение остальной части программы невозможно, пока мы не выйдем из этого цикла. Это логично. Но возможны случаи, когда такая логика работы не подходит. Например, если нам нужно опрашивать какие-либо датчики даже во время работы меню. В таких случаях поможет функция attachIdleFunc. Она позволяет указать функцию, которую будет вызывать библиотека снова и снова, пока мы находимся в меню, вводим или выбираем значения. А уже внутри этой самой функции можно сделать опрос датчиков или что угодно, что не будет долго выполняться, чтобы не мешать библиотеке опрашивать кнопки и энкодер.

Для демонстрации описанного функционала в приведённом ниже примере включается и выключается встроенный светодиод Ардуино, пока мы находимся внутри функций inputVal и printMultiline.

#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

// Пины, к которым подключен энкодер
#define pinCLK 2
#define pinDT  3
#define pinSW  4

unsigned long tm = 0;
bool ledState = false;

// Данная функция будет вызываться из библиотеки при бездействии
void myIdleFunc() {
  if (millis() - tm >= 500) {
    // Включаем и выключаем встроенный светодиод на Ардуино
    tm = millis();
    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState);
  }
}

void setup() {
  lcd.begin();
  lcd.attachEncoder(pinDT, pinCLK, pinSW);
  lcd.attachIdleFunc(myIdleFunc);
  pinMode(LED_BUILTIN, OUTPUT);
  lcd.print("Press the button");
}

int x = 0;

void loop() {
  myIdleFunc();
  if (lcd.getEncoderState() == eButton) {
    // Для проверки вызовем любую функцию библиотеки,
    // которая ожидает действий от пользователя:
    x = lcd.inputVal("Input some val", 0, 100, x);
    lcd.printMultiline("Some text here");
    lcd.clear();
    lcd.print("Press the button");
  }
}

Изначально myIdleFunc вызывается внутри функции loop, в результате светодиод мигает. При нажатии на кнопку энкодера происходит вызов функций библиотеки, выполнение loop приостанавливается. Тем не менее светодиод продолжает мигать, поскольку myIdleFunc вызывается библиотекой. И работе функций inputVal и printMultiline это не мешает.

Изменение названий пунктов меню

Ещё один приём, который я хочу показать, это изменение названий пунктов меню в ходе работы программы. Может пригодиться для отображения значений переменных в пунктах меню. В библиотеках есть примеры таких скетчей: Updating_menu_captions и Menu_for_setting_params.


Пример изменения названий пунктов меню


Поддержка дисплеев с кириллицей

Если ваш дисплей поддерживает кириллицу, то для корректного отображения русского текста необходимо включить её поддержку в самой библиотеке. Для этого в заголовочном файле (LiquidCrystal_I2C_Menu.h или LiquidCrystal_I2C_Menu_Btns.h) найдите и раскомментируйте строку:
#define CYRILLIC_DISPLAY
После этого сохраните внесённые изменения. Теперь русские буквы могут быть использованы в меню и других функциях библиотеки. Не забывайте, что для хранения одной русской буквы используется два байта, а не один. Поэтому для строки из N русских букв следует выделять N * 2 + 1 байт памяти.

Кроме того, при включении поддержки кириллицы вам станут доступны следующие функции:
uint8_t strlenUTF8(char *s)
void substrUTF8(char* source, char* dest, uint8_t fromPos, uint8_t count)
Функция strlenUTF8 возвращает длину строки s в символах (а не байтах, в отличие от функции strlen). Функция substrUTF8 копирует подстроку из строки source, начиная с символа fromPos, длиной count символов (опять же символов, а не байтов) в строку dest. Эти функции не являются методами классов LiquidCrystal_I2C_Menu / LiquidCrystal_I2C_Menu_Btns и вызываются "напрямую", без указания имени экземпляра класса, например:
#include <Wire.h>
#include <LiquidCrystal_I2C_Menu.h>
LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

void setup() {
  lcd.begin();
  char myText[] = "Привет, мир!";
  char mySubstr[21];
  lcd.print(strlenUTF8(myText)); // Напечатает "12"
  substrUTF8(myText, mySubstr, 8, 3);
  lcd.printAt(0, 1, mySubstr);   // Напечатает "мир"
}

void loop() {
  
}

Другие параметры

Кроме упомянутого выше CYRILLIC_DISPLAY заголовочные файлы библиотек содержат и другие параметры, которые могут быть изменены. Рассмотрим их.

#define SCROLL_LONG_CAPTIONS
- Данный параметр включает прокрутку длинных пунктов меню. Если такая возможность не нужна, просто закомментируйте данную строку. Это позволит немного уменьшить размер скомпилированного кода. Если же прокрутка нужна, то обратите внимание на временные интервалы данной функции:
#define SCROLL_DELAY        800  // Задержка прокрутки
#define DELAY_BEFORE_SCROLL 4000 // Задержка перед началом прокрутки
#define DELAY_AFTER_SCROLL  2000 // Задержка после вывода всей строки
- Они определяют скорость прокрутки и задержки перед её началом и в конце.

#define ENCODER_POOL_DELAY 5
- Интервал опроса энкодера (только для библиотеки LiquidCrystal_I2C_Menu). Я всегда использую 5мс. Может быть кому-то понадобится установить другое значение

#define BUTTONS_POOL_DELAY 50
#define BUTTONS_HOLD_BEFORE_REPEAT 1000
- Определяют интервал опроса кнопок и задержку при удержании кнопки нажатой (только для библиотеки LiquidCrystal_I2C_Menu_Btns). По истечении интервала BUTTONS_HOLD_BEFORE_REPEAT нажатая кнопка будет читаться как повторно нажатая каждые BUTTONS_POOL_DELAY мс до тех пор, пока она не будет отпущена.

Вместо заключения

При создании данных библиотек была проделана большая работа. Поэтому, надеюсь, вы оцените результат. Если возникнут вопросы по их использованию - пишите в комментариях, постараюсь на них ответить.

136 комментариев:

  1. Доброго времени суток !
    пытаюсь реализовать под свои цели ваши наработки.
    Сражаюсь вот с чем.
    При бездействии на главном экране получилось сделать авто откл подсветки,а вот если в меню или режим редактирования параметра...через заданную задержку времени ,если режим редактирования,сохраняет значение и выходит из меню и делает Back на главный экран.
    Знаний маловато,не получается реализовать

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Не понял. Вам нужен выход из меню и других функций по таймауту? Я думал добавить такую возможность. Но только не с сохранением параметра, его же не подтвердили.

      Удалить
  2. Не входя в меню ,получилось своять таймер автоотключения подсветки.
    НУ а если в меню или режим редактирования параметра - никак.
    Если просто в меню,спустя таймаут - автовыход из меню и откл подсветки дисплея.
    Если в режиме редактирования - выход из режима редактирования спустя таймаут без сохранения выставленного параметра,а следом без таймаута выход из меню,хотя можна и так (таймаут_меню + +таймаут_в режиме_редактирования).

    ОтветитьУдалить
    Ответы
    1. Добавил параметр INACTIVITY_TIMEOUT в LiquidCrystal_I2C_Menu.h. Он для выхода из меню и других функций. Скачайте новую версию из репозитория.
      Управление подсветкой внутри библиотеки не получится сделать логичным. Это надо делать глобально в основной программе. Не стОит оно того.

      Удалить
  3. LiquidCrystal_I2C_Menu/examples/selectVal_2/selectVal_2.ino
    Захожу в меню,галочка выбранного ранее варианта стоит,больше не чего не делаю.Через минуту на месте ,где установлен курсор,срабатывает один клик(без моего участия) по кнопке и всё,и больше нечего не происходит.А если сделал выбор пункта подменю,один раз нажал(галочка поставилась,второй раз нажал,подтвердил- то через минуту нечего не происходит
    То в своей поделке использую,вот пока так
    Какая переменная меняет состояние,в зависимости от того, в меню в данный момент или нет,чтоб реализовать таймер отключения подсветки на главном экране.
    Ещё раз прошу прощения,учусь

    ОтветитьУдалить
    Ответы
    1. Функция selectVal всегда возвращает целое число (индекс выбранного элемента). В том числе при завершении по таймауту. В примере selectVal_2.ino функция по таймауту вернет значение 0 (потому что preselected = 0), что равносильно клику на первом элементе.

      Еще раз: при выходе по таймауту selectVal возвращает значение preselected.

      Удалить
  4. Доброго вресени суток!
    В описании сказано что функцию GetSelectedMenuItem можно использовать в обработчиках, но обработчик, вызванный из меню и находящийся вне основного цикла (Loop) не имеет доступа к переменной SelectedMenuItem. Возможно ли определить выбранный пункт меню в таком случае?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Не понял вопроса. Можете пояснить, привести пример кода?

      Удалить
    2. Если речь о том, что обработчик не видит переменную SelectedMenuItem, объявленную в loop (или любой другой функции), то это само собой, ведь SelectedMenuItem - это локальная переменная. Внутри обработчика нет необходимости использовать именно ту переменную, можно объявить свою с любым именем.

      Удалить
  5. Hai, salam bahagia
    Nama saya muhamad Sayid akil dari Indonesia. Saya tertarik dalam source code yang Anda kembangkan dan sangat menakjubkan. Saya masih awam dalam arduino, saya ingin melakukan pengembangan lanjutan meski tidak tau berhasil atau tidak dan apabila berhasil apakah saya boleh menambahkan source anda yang Anda kerjakan ini, saya harap boleh. Saya bahkan sangat kesulitan yang source anda gunakan yang menggunakan encoder dan saya edit menggunakan tombol, 5 hari belum berhasil, dan akhirnya saya mencoba menggunakan versi yng menggunakan liblary tambahan dari anda 😂. Apabila boleh saya akan menempatkan nama Anda di dalam lcd sebagai Contributor dan mungkin saya akan melakukan donasi untuk anda ( apabila barang ini ternyata ada yang minat, saya aga kesulitan mencari email anda di dalam github sekalipun. Mohon balas di dalam email : muhamadsayidakil19@gmail.com

    Terima kasih salam bahagia, semoga hari anda menyenangkan..

    ОтветитьУдалить
  6. Здравствуйте! Возникла проблема при использовании функции inputVal. Необходимо ввести сначала десятые затем целые числа в переменную, в окне с различными параметрами. Так вот при вводе почему то невозможно установить курсор в определенном месте, и получается что сначала ввод происходит внизу потом вверху дисплея( Да и еще подскажите пожалуйста возможно ли отключить выход из меню при бездействии?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Чтобы отключить выход из меню закомментируйте в файле LiquidCrystal_I2C_Menu.h следующую строку:
      #define INACTIVITY_TIMEOUT 60000 // Таймаут бездействия до выхода из функций

      А про ввод чисел я что-то не понял. Если вы используете функцию inputVal, то она всегда очищает дисплей и ввод значения осуществляется на второй строке (если заголовок пустой, то на первой строке).
      Если используется inputValAt, то она ничего не очищает и ввод осуществляется с указанной позиции. В параметрах сначала указывается x, потом y:
      inputValAt(x, y, min, max, default, step = 1, [*onChangeFunc = NULL])

      Вы x и y перепутали?

      Удалить
    2. Да нет, оказывается была ошибка в параметрах, вроде бы разобрался. Еще такой вопрос: возможно ли вводить значения функции inputVal в реальном времени, без использования подтверждения, с помощью мощности библиотеки?

      Удалить
    3. "возможно ли вводить значения функции inputVal в реальном времени"
      - вот только на днях обновил библиотеку, добавил параметр для привязки функции, которая будет вызываться при изменении значения функциями inputVal и inputValAt. В публикации это описано. Скачайте последнюю версию библиотеки с гитхаба. Там и пример есть.

      Удалить
    4. Спасибо, совсем не успеваю за прогрессом)
      А вы не планировали ввести например обработку двойного нажатия кнопки энкоддера или обработку вращения с зажатой кнопкой?

      Удалить
    5. Пока нет такого в планах.
      Сейчас подумываю сделать подобную библиотеку для графических дисплеев, работающих с библиотеками Adafruit

      Удалить
    6. И снова у меня вопрос... Не могли бы вы подсказать, как завести в прерывание функцию inputVal? Иначе совсем не получается изменять привязанную к ней переменную или это осущесвляется некорректно.

      Удалить
    7. А зачем именно в прерывании?
      Если без прерывания никак, то в нём надо устанавливать флаг, который будет анализироваться основной программой. Если флаг установлен, то делаем что нам нужно и сбрасываем его. А помещать inputVal в обработчик неправильно.

      Удалить
    8. А разве есть другие варианты? И к тому же вроде как если использовать attachInterrupt 2 и 3 пины у нас заняты энкоддером.

      Удалить
    9. Энкодер можно подключить к любым другим пинам. И указать их функцией attachEncoder(pinA, pinB, pinBtn).
      И все-таки я не пойму, что вы хотите сделать. Поясните этот момент: "как завести в прерывание функцию inputVal? Иначе совсем не получается изменять привязанную к ней переменную".

      Удалить
    10. Постараюсь объяснить: имеется большое количество кода, различные вычисления и тд. Необходимо сделать первичный приоритет у энкоддера, что бы в любом куске кода с него могли считываться значения и вызывалась функция onChangeFunc, чтобы уже вносились изменения в параметры вычислений. По типу некоторого блока питания
      А насчет пинов, почему то мне всегда казалось, что энккодер использует 2 и 3 пины только из за доступных прерываний на них. Думал в библиотеке это как то реализовано, поэтому и спрашиваю у вас)

      Удалить
    11. Итак, вы хотите изменять значение некоторого параметра энкодером, в том числе не вызывая меню и функцию inputVal. Посмотрите цикл loop в этом скетче: https://drive.google.com/open?id=1nF2Kz0qNvdQfpgzyLa9GIOZ_j9P-prjb

      В нём проверяется состояние энкодера. При нажатии кнопки вызывается меню. А при вращении влево/вправо либо изменяется громкость, либо включается предыдущая/следующая радиостанция (в зависимости от состояния переменной EncoderPurpose). Эти действия можно выполнить и через меню, а так получается вроде быстрого доступа. Думаю, вам подойдет такая логика работы. После опроса энкодера можно вставить ваш код с вычислениями.

      Здесь нет приоритета у энкодера. Когда выполнение дойдёт до его опроса, тогда и будет зафиксировано его вращение, тогда и изменится ваш параметр. Но вряд ли это проблема, ведь это взаимодействие с пользователем, здесь не требуется мгновенная реакция. Или ваш код с вычислениями выполняется долго и его нужно прерывать энкодером?

      Энкодер в библиотеке опрашивается программно, без прерываний, поэтому пины могут использоваться любые. Но я всё равно часто использую 2 и 3 пины, чтобы можно было разрешить на них прерывания и выводить микроконтроллер из спящего режима энкодером.

      Удалить
  7. Здравствуйте! Будет ли Ваша библиотека работать с дисплеем без I2C ?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Если только перенести все мои функции в библиотеку LiquidCrystal.

      Мой код не привязан к типу дисплея. Он вызывает функции print, setCursor, createChar и т.п. - они одинаковы для обеих библиотек (LiquidCrystal и LiquidCrystal_I2C). Поэтому если Вы скопируете весь добавленный мною код в библиотеку LiquidCrystal, то сможете использовать его для дисплеев без I2C.

      В принципе тут нет ничего сложного: копируете/вставляете фрагменты кода, по тексту заменяете LiquidCrystal_I2C_Menu на LiquidCrystal_Menu. Попробуйте.

      Удалить
    2. Здравствуйте, портировали ли библиотеку на дисплей без I2C?

      Удалить
  8. Приветствую, Владимир!
    С глубоким уважением рассматриваю Вашу новую версию. С ранних версий использую её только из-за эксклюзивов "PrintAt" и "inputAt", да ещё плюс энкодер в одном флаконе! К сожалению, сам функционал меню я так и не смог подружить с многозадачностью, генератор на миллисах "виснет"(( Поэтому сейчас мудрю простой вариант "ручками". И вот, столкнулся с проблемой (возможно, не в библиотеке, а в моих корявых ручках).
    Итак, у меня есть 7 пунктов меню, которые мне необходимо "мотать" энкодером вперёд-назад. За это отвечает переменная mainMenuState. Дабы не мотать выше или ниже диапазона, я ограничил переменную constrain-ом (от 0 до 6). Для удобства вывел её "контрольку" в уголке 2004. И всё бы хорошо, но при инкременте всё замечательно упирается в цифру 6, но вот при декременте после достижения нуля "проскакиваем" на шестёрку и ниже... Код вроде правильный. Пытался ограничить if-ами - тоже безоезультатно. Кусок кода прилагаю

    void mainMenuHandler() { // ============== ОБРАБОТЧИК ГЛАВНОГО МЕНЮ
    // Опрашиваем энкодер

    eEncoderState EncoderState = lcd.getEncoderState();
    mainMenuPos = constrain(mainMenuPos, 0, 6);
    switch (EncoderState) {
    case eRight: // При вращении вправо увеличиваем значение переменной
    mainMenuPos++;
    // if(mainMenuPos >=6) mainMenuPos = 6;
    break;
    case eLeft: // При вращении влево уменьшаем значение переменной
    mainMenuPos--;
    // if(mainMenuPos <=0) mainMenuPos = 0;
    break;
    case eButton: // При нажатии кнопки энкодера переназначаем значение переменной
    mainMenuState = mainMenuPos;
    break;
    case eNone: // Энкодер не вращается, кнопка не нажата. Выходим из функции
    return;
    }
    }

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Попробуйте вот так:

      void mainMenuHandler() { // ============== ОБРАБОТЧИК ГЛАВНОГО МЕНЮ
      // Опрашиваем энкодер

      eEncoderState EncoderState = lcd.getEncoderState();
      switch (EncoderState) {
      case eRight: // При вращении вправо увеличиваем значение переменной
      if(mainMenuPos < 6) mainMenuPos++;
      break;
      case eLeft: // При вращении влево уменьшаем значение переменной
      if(mainMenuPos > 0) mainMenuPos--;
      break;
      case eButton: // При нажатии кнопки энкодера переназначаем значение переменной
      mainMenuState = mainMenuPos;
      break;
      case eNone:
      return;
      }
      }

      Удалить
    2. Спасибо, заработало. Я как-то даже и не подумал ТАК ограничивать... Однако всё же непонятен смысл функции CONSTRAIN...

      Удалить
  9. Огромное спасибо за библиотеку!
    Для работы на ESP32 пришлось немного пошаманить, по какой то причине вот такие конструкции вызывают ошибку при компиляции.
    min(_rows - _showMenuTitle, subMenuLen), выдает, что в первой части вместо uint8_t другой тип.
    Заменил везде на такую конструкцию
    uint8_t temp = _rows - _showMenuTitle;
    min(temp, subMenuLen)
    И все заработало.
    Теперь осталось с энкодером разобраться, у меня энкодер ky-040 (hw-040), и инкремент происходит только после второго щелчка, не очень удобно получается.

    ОтветитьУдалить
    Ответы
    1. Пожалуйста!
      Насчет энкодера. Функция getEncoderState определяет направление вращения в тот момент, когда пин A изменяется от 1 к 0. Это строка:
      if ((!encoderA) && (_pinAPrev)) {
      Попробуйте изменить её на:
      if (encoderA != _pinAPrev) {
      чтобы условие выполнялось при каждом изменении пина A.

      Удалить
    2. Нет, не сработало - на первый тик идет поворот в одну сторону, на второй тик поворот в противоположную сторону. В итоге стоим на месте. Попробую другой энкодер поискать.

      Удалить
    3. Здравствуйте! У меня такая же проблемка. У Вас же энкодер ky-040? Удалось его победить?

      Удалить
    4. Поменял в файле LiquidCrystal_I2C_Menu.cpp в процедуре getEncoderState() кусок кода
      if ((!encoderA) && (_pinAPrev)) {
      if (encoderB) Result = eRight;
      else Result = eLeft;
      }
      на:
      if (_pinAPrev) {
      if (encoderA && !encoderB) Result = eLeft;
      if (!encoderA && encoderB) Result = eRight;
      } else {
      if (encoderA && !encoderB) Result = eRight;
      if (!encoderA && encoderB) Result = eLeft;
      }

      и все заработало с одноимпульсным KY-40. Про два вида энкодеров KY-40 более-менее понятно написано тут https://alexgyver.ru/encoder/

      Удалить
    5. Не могли бы подробнее рассказать
      min(_rows - _showMenuTitle, subMenuLen), выдает, что в первой части вместо uint8_t другой тип.
      Заменил везде на такую конструкцию
      uint8_t temp = _rows - _showMenuTitle;
      min(temp, subMenuLen)


      В библиотеке там только в одном месте есть
      case eRight: {
      // Перемещение курсора вниз
      #ifdef SCROLL_LONG_CAPTIONS
      _scrollTime = millis() + DELAY_BEFORE_SCROLL;
      #endif
      if (cursorOffset < min(_rows - _showMenuTitle, subMenuLen) - 1) {// Moving cursor down if possible
      что конкретно изменить ?

      Удалить
  10. Этот комментарий был удален автором.

    ОтветитьУдалить
  11. Владимир, доброй ночи!
    Наконец-то руки дошли, решил всё-таки попытаться докопаться до истины, но без Вашей помощи, видимо, не судьба. Моя задача - высвободить loop() от зависания из-за меню.

    Чтобы не быть голословным:
    У меня есть регулируемый энкодером задающий генератор на миллисах. Мне необходимо, чтобы при любом состоянии меню он работал. Как это реализовать? Пробовал в примере Using_handlers вставить свой генератор в loop, но он не хочет работать, запускается только при закомментированной строке вызова меню в лупе:
    uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1);
    Причём переменную для регулировки частоты генератора необходимо как раз получать из меню (inputVal). В первом примере с блинком это то, что мне надо, но это не многоуровневое меню. Я в полном замешательстве

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Боюсь с генератором на миллис не получится использовать такое меню.
      Вам нужно было брать пример attachIdleFunc. В нём код, помещенный в функцию myIdleFunc, выполняется и при работе функций библиотеки, и внутри цикла loop. Тогда бы выполнение вашего кода не прерывалось при входе в меню. Но именно для генератора на миллис такой вариант не подходит: перерисовка дисплея может занимать длительное время - десятки миллисекунд. Это как если бы вы вставили delay внутри кода генератора. Тут нужно, чтобы генератор работал на таймере и прерываниях, тогда к нему можно будет прикрутить меню.
      В интернете немало скетчей генераторов на таймерах. Думаю, из них можно будет что-то сварганить. Будет время - посмотрю.

      Удалить
    2. Заранее премного благодарен!

      Удалить
  12. Владимир! Прекрасная работа! Спасибо Вам! Это то, что я искал

    ОтветитьУдалить
  13. Этот комментарий был удален автором.

    ОтветитьУдалить
  14. Добрый день. Подстраиваю данное меню под свои нужды, и не могу сообразить, как отключить заголовок меню для 1602. -- "В качестве заголовка используется название родительского пункта меню. Отключение отображения заголовка полезно при использовании дисплея 1602." -- где отключить этот заголовок?

    ОтветитьУдалить
  15. Ответы
    1. Добрый день.
      Не успел ответить. Ну хорошо, что разобрались. Обращайтесь, если возникнут вопросы.

      Удалить
  16. Добрый день, никак не получается вывести кириллицу на экран, в экране прошита кириллица, кодами выводится, а напрямую никак, какие-то кракозябры, подскажите новичку, помогите разобраться пожалуйста!

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Строку #define CYRILLIC_DISPLAY в .h файле раскомментировали? Скетч сохранили?

      Удалить
    2. Добрый день, подскажите что значит раскомментировать? Я просто добавил эту строчку в верхней строке скетча и установил вашу библиотеку

      Удалить
    3. В папке с библиотекой есть файл LiquidCrystal_I2C_Menu.h
      Откройте его в текстовом редакторе, поддерживающем UTF8. Я использую Notepad++
      Найдите в нём указанную строку. По умолчанию она закомментирована - перед ней стоят 2 слэша.
      Удалите эти 2 слэша и сохраните файл.

      Удалить
  17. Владимир, добрый день. А можно увеличить в библиотеке количество символов для написания текста?
    String list[] = {"Europa+", "Record", "DFM", "Retro FM", "Energy"};

    ОтветитьУдалить
  18. Владимир Здравствуйте. Отличная работа, большое спасибо. В одном из своих проектов я использую 40х4 LCD. Библиотека https://forum.arduino.cc/index.php?topic=492553.0 Пытался приспособить Вашу библиотеку, но не получается. Может быть поможете?

    ОтветитьУдалить
  19. Владимир Здравствуйте. Отличная работа, большое спасибо. В одном из своих проектов я использую ваш блок меню
    #include
    #include
    LiquidCrystal_I2C_Menu lcd(0x27, 16, 2);
    #include "FMTX.h"
    // Пины, к которым подключен энкодер
    #define pinCLK 2
    #define pinDT 3
    #define pinSW 4
    double val = 0;
    double x = 0;
    float freg;
    int brightness = 0;
    int RF = 0;

    int _delay = 10;

    // Обработчики пунктов меню SetBrightness и SetDelay
    // Используются для ввода значений brightness и _delay
    void SetBrightness() {
    brightness = (lcd.inputValBitwise ("chastota", val, 3, 1, true));
    lcd.print("You entered: ");
    lcd.print(val);
    }
    void SetRF() {
    RF = (lcd.inputValBitwise ("UF", x, 3, 1, true));
    lcd.print("You entered: ");
    lcd.print(x);
    }

    void SetDelay() {
    _delay = lcd.inputVal("Input delay(ms)", 0, 20, _delay);
    }

    // Объявим перечисление, используемое в качестве ключа пунктов меню
    enum {mkBack, mkRoot, mkOptions, mkSetBrightness, mkSetDelay, mkSetRF};

    // Описание меню
    // структура пункта меню: {ParentKey, Key, Caption, [Handler]}
    sMenuItem menu[] = {
    {mkBack, mkRoot, "Main menu", NULL},
    {mkRoot, mkOptions, "Options", NULL},
    {mkOptions, mkSetBrightness, "SetBrightness", SetBrightness},
    {mkOptions, mkSetRF, "SetRF", SetRF},
    {mkOptions, mkSetDelay, "SetDelay", SetDelay},
    {mkOptions, mkBack, "Back", NULL},
    {mkRoot, mkBack, "Back", NULL}

    };

    uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem);

    void setup() {
    lcd.begin();
    lcd.attachEncoder(pinDT, pinCLK, pinSW);
    }

    void loop() {
    uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1); // Вызываем меню
    /* Реакция на выбор пунктов меню SetBrightness и SetDelay реализована
    * в функциях-обработчиках.
    * При необходимости здесь может располагаться анализ значения selectedMenuItem
    * для пунктов, не имеющих обработчиков:
    if (selectedMenuItem == ...) {...}
    */
    {
    void freg();
    }
    fmtx_set_freq(val);
    {
    void fmtx_set_rfgain(u8 rfgain);
    }
    fmtx_set_rfgain(x);

    }

    У меня проблема в том что как сделать подтверждения в самом меню при изменения параметрах при нажатии знака подтверждения. У меня подтверждается только когда я выхожу в блок основного меню. Спасибо за помощь!

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Не мог ответить раньше.
      Что понимаете под подтверждением параметра? - Вызов функций fmtx_set_freq(val), void fmtx_set_rfgain(u8 rfgain)? Вызывайте их из обработчика, когда меняется сам параметр.

      Удалить
    2. Спасибо что ответили Владимир.
      Под подтверждением параметра я понимаю что в донам меню с права есть галочка на которую при нажатии должен применяться восстановленное значения.А у меня получается что параметр изменяется только тогда когда я выхожу в основной блок меню.Простите мою не грамотность но не могли бы объяснить что Вы имели в виду Вызывайте их из обработчика, когда меняется сам параметр.
      Владимир еще хотел Вас спросить как в даный скейч можно добавить регулированый аналоговый или ШИМ выход от 1.0 до 4.9 вольт
      Я пробовал добавить то что находил в сети но у меня не как не хочет работать с Вашим меню Пожалуйста помогите .

      Удалить
    3. Когда вы выбираете в меню один из пунктов SetBrightness, SetRF или SetDelay, вызывается соответствующая функция. Вызвалась, например, функция SetRF. Что вам в ней нужно сделать? Ввести значение переменной RF? Это в функции есть. А что собираетесь делать дальше с введённым значением? Как-то использовать его, применить, верно? Вот и примените его сразу после ввода, в функции SetRF.
      Для шим используйте стандартную функцию analogWrite

      Удалить
  20. Владимир, здравствуйте! Не пойму что не так, пишу кириллицей А Б В Г Д, а выдает на дисплее ч ш ъ ы ь

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Скетч перед загрузкой в Ардуино сохраняете? Нужно обязательно сохранять, т.к. в противном случае он сохраняется во временную папку в другой кодировке.

      Возможно, у вас в дисплее нестандартный шрифт, поэтому выводятся не те символы. Это можно проверить, загрузив в Ардуино следующий скетч. Он покажет прошитые в дисплей символы и соответствующие им коды. Их нужно сравнить с теми, которые прописаны в самом начале файла LiquidCrystal_I2C_Menu.cpp. Например, для вывода русской А мы берем англйскую, а для вывода Б берем символ с кодом 160 - это соответствие прописано в указанном файле. А какой у вас символ под кодом 160? Покрутите энкодером, чтобы добраться до него. Сравните так остальные русские символы. Если вы не подключаете энкодер, то можете просто в цикле вывести по очереди все коды с символами. С задержкой в несколько секунд, чтобы успеть посмотреть.

      В общем, если ваш шрифт отличается, то нужно изменить соответствие в файле LiquidCrystal_I2C_Menu.cpp, чтобы выводились правильные буквы.


      #include
      #include
      LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

      // Пины, к которым подключен энкодер
      #define pinCLK 2
      #define pinDT 3
      #define pinSW 4

      byte x = 1;

      void setup() {
      lcd.begin();
      lcd.attachEncoder(pinDT, pinCLK, pinSW);
      lcd.printfAt(0, 0, "%d - %c ", x, x);
      }

      void loop() {
      // Опрашиваем энкодер
      eEncoderState EncoderState = lcd.getEncoderState();
      switch (EncoderState) {
      case eLeft:
      x--;
      break;
      case eRight:
      x++;
      break;
      case eButton:
      x = 1;
      break;
      case eNone:
      return;
      }
      lcd.printfAt(0, 0, "%d - %c ", x, x);
      }

      Удалить
    2. Инклуды вырезались:

      #include "Wire.h"
      #include "LiquidCrystal_I2C_Menu.h"

      Удалить
  21. Владимир спасибо за ответ и за скетч чтения символов! Заехал на работу, на флешку скинул библиотеку ранее скаченную с которой не возникло проблем со шрифтами в кириллице. Поменял библиотеки на домашнем ПК все заработало нормально. После вашей рекомендации опять установил "нерабочую" библиотеку - ВСЕ РАБОТАЕТ! В чем была проблема так и не понял. А вообще спасибо за ваше творение - библиотека СУПЕР!

    ОтветитьУдалить
  22. Владимир, подскажите пожалуйста как заставить работать фрагмент кода:

    float CorrectLenght_Y = 0.0;

    else if (selectedMenuItem == mkSetCorrectLenght_Y) {
    CorrectLenght_Y = (lcd.inputValBitwise("Коррекция по Y", CorrectLenght_Y, 2, 1, true));
    EEPROM.update(4, CorrectLenght_Y);
    updateCaption(mkSetCorrectLenght_Y, "Коррекция по Y (%1.%1)", CorrectLenght_Y);
    }

    Проблема в том, что не выводится число в updateCaption и нет записи в EEPROM

    ОтветитьУдалить
    Ответы
    1. Не мог ответить раньше. Победили проблему? Если нет, то дело в следующем:

      lcd.inputValBitwise возвращает true или false, чтобы бы понятно, подтвердили мы ввод нового значения или нет. Само значение будет передано во втором параметре, т.е. в переменной CorrectLenght_Y. Поэтому и в EEPROM сохранялось не то.

      Кроме того реализация printf в avr gcc урезана для облегчения веса и по умолчанию не поддерживает числа с плавающей точкой. Поэтому для вывода значения CorrectLenght_Y в название пункта меню придётся преобразовать его в строку. Итого получается:

      if (lcd.inputValBitwise("Коррекция по Y", CorrectLenght_Y, 2, 1, true)) { // Если ввели новое значение CorrectLenght_Y
      char str[5];
      dtostrf(CorrectLenght_Y, 4, 1, str); // Преобразуем его в строку
      updateCaption(mkSetCorrectLenght_Y, "Коррекция по Y (%s)", str); // и выводим в меню
      }

      Удалить
  23. Владимир,большое спасибо за ответ! Отказался от float в пользу целых чисел, переведя все в миллиметры, но с числами с плавающей точкой думаю еще предстоит столкнуться, так что еще раз спасибо. Переведя значения в миллиметры, энкодером стало затруднительно вводить тысячные значения с шагом в 1 мм, у вас нет случаем варианта работы библиотеки с клавиатурой?

    ОтветитьУдалить
  24. Владимир, а добавление разрядов по двойному нажатию на кнопку энкодера сложно в библиотеку добавить? Например: по одному нажатию заходим в функцию lcd.inputVal, выставляем число, нажимаем 2 раза - появляется еще один разряд(десятки), опять двойной клик - еще разряд(сотни) и т.д. потом также по одному нажатию сохранение и выход из функции. Или может еще как то, не соображу как побыстрее можно выставить тысячные числа...

    ОтветитьУдалить
  25. Владимир, подскажите пожалуйста как можно выйти из меню находясь в функции?

    ОтветитьУдалить
    Ответы
    1. Если речь о том, чтобы при выходе из обработчика закрылось само меню, то никак. Потому что функции-обработчики здесь задумывались как раз для того, чтобы меню не закрывалось, и после выполнения обработчика мы оставались на том же пункте.

      Чтобы выйти из меню при выборе какого-либо пункта, он не должен иметь обработчика. Тогда выполнение функции showMenu завершится, и она вернёт ключ выбранного пункта меню. Дальше анализируете этот ключ и выполняете нужные действия. Это есть в примере showMenu.ino.

      Таким образом, есть 2 варианта обработки выбранного пункта меню:
      1. С использованием функций-обработчиков. После чего происходит возврат в меню.
      2. Путём анализа значение, возвращённого функцией showMenu. Для этого пункт не должен иметь обработчика. При выборе такого пункта будет происходить выход из меню.

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

      Удалить
  26. Эх, жаль. Смысл как раз в том, чтобы после поочередного ввода настроек в обработчиках, находящихся в функции, при выборе положительного ответа сразу выйти из меню в цикл программы для выполнения действий. Работать с разрядами получилось через inputValBitwise по такой схеме lcd.inputValBitwise("Расстояние до нуля:", NullPointY, 4); Так что еще раз спасибо)

    ОтветитьУдалить
  27. Здравствуйте Владимир, как делается структура меню, а точнее смысл его, я ваш пример еле как переделал под свой, но так и не понял ни капли как это работает

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Не поняли, как описать меню? А что тут непонятного, меню - это иерархическая структура, т.е. каждый пункт меню находится внутри другого, вышестоящего пункта. Такие структуры описываются в виде таблиц (в нашем случае массив), в которых содержится номер конкретного пункта, номер родительского пункта (эти два значения как раз позволяют описать иерархию элементов) и другая информация, названия и т.п.

      У нас есть массив элементов типа sMenuItem. Этот тип описан выше в статье, он содержит параметр parent для указания родительского пункта и key - номер данного пункта. И чтобы не путаться здесь с номерами/числами, я использую вместо них символьные константы - перечисления, например:
      enum {mkBack, mkRoot, mkRun, mkOptions, mkMode, mkSpeed, mkLog, mkSelftest, mkHelp, mkFAQ, mkIndex, mkAbout};

      При компиляции эти символьные константы по тексту скетча заменятся числовыми значениями: вместо mkBack подставится 0, вместо mkRoot 1 и т.д. по порядку их объявления.

      Продумываете своё меню, какие пункты в нём будет, и для каждого из них подбираете понятную символьную константу. Я в их именах использую префикс mk - это отсылка к MenuKey. Единственное требование к перечислению: первым в нём должно идти значение, используемое в меню для возврата на уровень выше (в примерах оно называется mkBack) чтобы ему соответствовало значение 0
      Далее описываете меню, используя своё перечисление. Для читабельности я добавляю табуляцию для вложенных пунктов меню - сразу видно, что за чем идёт.

      Удалить
  28. Здравствуйте, Владимир, очень хороший вклад. Мне нужна небольшая помощь. Я новичок в Arduino. И мне очень нравится электроника. Но в программировании я очень пессимистичен.

    Я хочу создать меню для своей SD-карты MP3-плеера
    Где я могу перечислить все файлы MP3, когда я даю ему первый вариант и при выборе файла Mp3 введите звуковую функцию, если я верну ее, что она возвращается в тот же список, и поэтому я могу продолжать смотреть файлы Mp3 Большое спасибо много Если вы можете мне помочь, я был бы признателен.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Не понял вопрос. Вы используете библиотеку для вывода списка MP3 файлов, верно? Вы делаете это функцией selectVal? Или вы еще ничего не написали, и не знаете как это сделать?

      Удалить
  29. Подскажите пожалуйста, можно добавить в библиотеку длительное нажатие кнопки энкодера, очень надо!

    ОтветитьУдалить
    Ответы
    1. Можно. И даже не очень сложно. Нужно будет внести изменения в функцию getEncoderState в файле LiquidCrystal_I2C_Menu.cpp. Сейчас в ней отслеживается момент нажатия кнопки. Нужно добавить фрагмент, который при нажатии запомнит текущее значение millis, а при следующих вызовах будет проверять: если кнопка нажата и при этом текущее значение millis минус запомненное значение >= 1000 (период после которого считаем кнопку удерживаемой), то возвращаем соответствующее значение - его нужно будет добавить в перечисление
      enum eEncoderState {eNone, eLeft, eRight, eButton};
      в файле LiquidCrystal_I2C_Menu.h
      - Несложно, но я сейчас не могу это сделать. Попробуйте.

      Удалить
  30. Здесь добавить?
    eEncoderState LiquidCrystal_I2C_Menu::getEncoderState() {
    bool encoderA, encoderB;
    eEncoderState Result = eNone;
    if (millis() - _prevPoolTime > ENCODER_POOL_DELAY) {
    _prevPoolTime = millis();
    if (digitalRead(_pinBtn) == LOW ) {
    if (_pinButtonPrev) {
    _pinButtonPrev = 0;
    Result = eButton;
    }
    }

    ОтветитьУдалить
  31. Воистину день знаний) Завтра попробую разобраться. Спасибо!

    ОтветитьУдалить
  32. Нет, не получилось. После многократных попыток сделал откат. Прошу помощи.

    ОтветитьУдалить
  33. Владимир, помогите пожалуйста, сам не разберусь. Короткое нажатие на кнопку энкодера уже задействовано, ввел еще одну кнопку для остановки/запуска процесса, не хватает входа в меню. Библиотека меню действительно получилась очень функциональная, сильно облегчает написание кода, особенно для начинающего, вроде меня. С удовольствием поддержал бы вашу авторскую работу если бы написали реквизиты. Заранее благодарен. Мой ящик m1tya52@yandex.ru

    ОтветитьУдалить
    Ответы
    1. Постараюсь, найти время на неделе.

      Удалить
    2. Вы кстати как понимаете длительное нажатие? Например, нажали кнопку, держим, по прошествии X мс библиотека воспринимает это как долгое нажатие, и функция getEncoderState возвращает соответствующий результат (скажем, eButtonLong) каждые Y мс до тех пор пока кнопку не отпустят. По аналогии как в компьютере: пока держим, буква печатается снова и снова.

      Или другой вариант: библиотека увидела, что кнопка удерживается, вернула 1 раз результат eButtonLong, и больше его не шлёт, пока кнопку не отпустят.

      Удалить
  34. Может я не совсем понимаю, но мне кажется второй вариант лучше в плане защиты от дребезга. Главное чтобы они не конфликтовали с однократным нажатием кнопки. У меня задумка в том, чтобы расширить функциональность работы с задачами программы. По однократному нажатию я работаю с функцией на главном экране(задаю параметр для перемещения каретки станка на заданные координаты), получается что для входа в меню идеальным вариантом было бы долгое нажатие кнопки энкодера, так как мы в данный момент работаем с дисплеем. Для запуска процессов работы станка вывел отдельную кнопку, так удобнее будет для оператора, сделал нужные настройки и стой на кнопку только жми)

    ОтветитьУдалить
    Ответы
    1. На неделе всё-таки не получилось.
      Пробуйте:
      https://drive.google.com/file/d/1KGgw8aIRIJz9DxkrB9WK9176X_-Y0mWb/view?usp=sharing
      Время долгого нажатия прописано в LiquidCrystal_I2C_Menu, параметр BUTTON_LONG_PRESS, в нём сейчас 1 секунда. Также в архиве есть пример.

      Удалить
    2. Спасибо, что выбрали время. В примерах все красиво работает, но у меня меню выстроено по другому примеру и в нем работает только одна из функций, либо с eButton либо eLongButton в зависимости в какой последовательности они находятся в цикле.

      if (lcd.getEncoderState() == eButton) {
      lcd.inputValBitwise("Переместиться по Y:", SetPos_Y, 4);
      if (SetPos_Y > NullPoint_Y) SetPos_Y = NullPoint_Y + Correct_Y;
      StepPos_Y = SetPos_Y * 10;
      lcd.printfAt(6, 0, ">%d", SetPos_Y);
      stepper1.setTarget(StepPos_Y);
      }
      if (lcd.getEncoderState() == eLongButton) {
      byte selectedMenuItem = lcd.showMenu(menu, menuLen, 1);
      }

      Так работает только eButton.

      if (lcd.getEncoderState() == eLongButton) {
      byte selectedMenuItem = lcd.showMenu(menu, menuLen, 1);
      }
      if (lcd.getEncoderState() == eButton) {
      lcd.inputValBitwise("Переместиться по Y:", SetPos_Y, 4);
      if (SetPos_Y > NullPoint_Y) SetPos_Y = NullPoint_Y + Correct_Y;
      StepPos_Y = SetPos_Y * 10;
      lcd.printfAt(6, 0, ">%d", SetPos_Y);
      stepper1.setTarget(StepPos_Y);
      }

      Так только eLongButton.

      Удалить
    3. Это потому, что вы вызываете функцию getEncoderState несколько раз подряд. Если она вернёт состояние кнопки при первом вызове, то второй и последующие вернут eNone: предполагается, что вы уже обработали все ожидаемые значения при первом вызове, а следующий анализ состояния энкодера будет только через 5 мс (для кнопки, нажимаемой человеком, даже это слишком часто). Поэтому:
      Объявляете переменную
      записываете в неё значение, возвращённое функцией lcd.getEncoderState()
      сравниваете значение этой переменной с eButton, eLongButton, eLeft и eRight
      - всё как в примерах

      Удалить
    4. Владимир, спасибо БОЛЬШОЕ, все работает! Давайте презентую на поддержку проекта?

      Удалить
    5. Ну это необязательно. Я ведь не ради денег помогаю. Но если прям хочется отблагодарить, можете, например, закинуть мне на телефон (89209004339), сколько посчитаете нужным.

      Удалить
    6. Здорово когда люди выкладывают в свободный доступ свои труды не ради денег, тем более когда они действительно полезны. Закинул немножко. Еще раз спасибо!

      Удалить
  35. Приветствую.
    Подскажите пожалуйста как можно прикрутить русские символы к вашей библиотеке для вывода на экран.
    Библиотека https://drive.google.com/file/d/1IyxTT7KAcx3IYHRsjigVHP9rQTwVighz/view из статьи https://роботехника18.рф/русский-шрифт-ардуино/ отлично русифицирует дисплеи без кириллицы. Можно как то её функционал добавить в вашу библиотеку? Заранее признателен за помощь.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Если не хотите покупать дисплей с русским шрифтом, то можно попробовать. Только нужно сразу учесть, что русификация будет использовать CGROM дисплея, а это всего 8 пользовательских символов. 2-3 из них уже используются в моей библиотеке для вывода символов прокрутки (стрелки вверх и вниз). Да и текст придётся подбирать так, чтобы минимизировать количество уникальных русских букв, отображаемых одновременно. В общем, реализуемо, но это будет большим компромиссом.

      У меня в .cpp файле есть функция write, вызываемая для каждого из печатаемых символов. Я бы сделал русификацию в ней: анализ символа, поиск его индекса в CGROM, если там нет, то создаём... Но это надо ковырять библиотеку LCD_1602_RUS, что из неё подойдёт, а что придётся переписывать.

      Удалить
  36. Владимир, добрый день!
    Пытаюсь приладить модуль для чтения флешки на чипе CH376S. Информацию и библиотеку для работы с этим модулем взял отсюда https://github.com/djuseeq/Ch376msc#firmware-difference. В глаза бросилась фраза из примеров которую гугл переводит как что то про Морковь лорен и суп который эволюционировал в европпу). Я это к чему... вы не знакомы с творчеством человека по ссылке, хотелось бы вопрос задать по теме, было бы здорово если бы кто нибудь кое что на русском объяснил?

    ОтветитьУдалить
  37. Владимир, доброго дня!
    Пытаюсь разобраться с printf и printfAt. Как я понял, эти функции из вашей библиотеки работают не со всеми типами данных. Пытаюсь вывести переменную типа float (с плавающей запятой) но ничего не получается.
    Вот строка программы:
    lcd.printfAt(0, 1, "%f", effective_value);
    А целые числа типа int выводятся нормально:
    lcd.printfAt(0, 2, "%10d", timerCount);
    Подскажите, может быть я что то не так делаю или все таки не смогу вывести переменную float?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Реализация всех возможностей и форматов vprintf была бы очень тяжелая. Поэтому используемая по умолчанию реализация форматированного вывода в avr-libc не работает с числами с плавающей точкой. Можно попробовать включить поддержку плавающей точки при помощи опций компилятора, но проще воспользоваться функцией dtostrf

      Удалить
  38. Этот комментарий был удален автором.

    ОтветитьУдалить
  39. Здравствуйте. Возникла проблема при реализации задумки. Подскажите как вывести значение температуры, считываемое с датчика(hdc1080, работает по I2C) возле одного из пунков меню key.
    Выглядеть должно примерно так: Temp: 32,43C

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Посмотрите пример Updating_menu_captions.ino. В нём к названиям пунктов меню приписываются значения изменяемых параметров. Вы можете делать так же при каждом измерении температуры.

      Удалить
    2. Спасибо за ответ) Реализовать прошлую задумку вышло, но посчитал это не самым лучшим вариантом. Сделал так, что все показания датчиков находятся на одом экране, а при нажатии кнопки осуществлялся вход в меню. Но настигла вторая проблема- нужен возврат на экран с данными датчиков по бездействию 10 секунд... Не могли бы вы подсказать как это сделать?

      Ниже прикрепляю код

      //библиотеки
      #include
      #include
      #include "ClosedCube_HDC1080.h"



      LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);
      ClosedCube_HDC1080 hdc1080;

      // пины энкодера
      #define pinCLK 2
      #define pinDT 3
      #define pinSW 4

      //символы
      byte t1[8]=
      {
      0b01001,
      0b01001,
      0b01001,
      0b01000,
      0b11100,
      0b01000,
      0b01010,
      0b00100
      };

      byte t2[8]=
      {
      0b01011,
      0b01011,
      0b01011,
      0b01000,
      0b11100,
      0b01000,
      0b01010,
      0b00100
      };


      byte h1[8]{
      0b00001,
      0b00001,
      0b00001,
      0b00100,
      0b01110,
      0b11111,
      0b11111,
      0b01110
      };

      byte h2[8]{
      0b00011,
      0b00011,
      0b00011,
      0b00100,
      0b01110,
      0b11111,
      0b11111,
      0b01110
      };


      unsigned long timing;

      // Ключи меню
      enum {mkBack, mkRoot, mkTemp1, mkDt1, mkNt1, mkTemp2, mkDt2, mkNt2, mkHum1, mkHum2, mkWater, mkWatering, mkTimes, mkLight, mkDay, mkNight, mkIndexes, mkLuminosity, };

      // Описание меню
      // структура пункта меню: {ParentKey, Key, Caption, [Handler]}
      sMenuItem menu[] =
      {
      {mkBack, mkRoot, "Menu"},

      {mkRoot, mkIndexes,"INDEXES"},
      {mkIndexes,mkLuminosity,"Luminosity"},
      {mkIndexes,mkBack,"BACK"},

      {mkRoot, mkTemp1, "TEMPERATURE_1"},
      {mkTemp1,mkDt1, "DAY_t1"},
      {mkTemp1,mkNt1, "NIGHT_t1"},
      {mkTemp1,mkBack, "BACK"},


      {mkRoot, mkTemp2, "TEMPERATURE_2"},
      {mkTemp2,mkDt2, "DAY_t2"},
      {mkTemp2,mkNt2, "NIGHT_t2"},
      {mkTemp2,mkBack, "BACK"},

      {mkRoot, mkHum1, "HUM_1"},
      {mkHum1,mkBack, "BACK"},

      {mkRoot, mkHum2, "HUM_2"},
      {mkHum2,mkBack, "BACK"},

      {mkRoot, mkWater, "WATERING"},
      {mkWater,mkWatering, "TIME"},
      {mkWater,mkTimes, "TIMES"},
      {mkWater,mkBack, "BACK"},

      {mkRoot, mkLight,"LIGHT"},
      {mkLight, mkDay,"DAY LENGTH"},
      {mkLight,mkNight,"NIGHT LENGTH"},
      {mkLight,mkBack,"BACK"}

      };

      uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem);





      void setup()
      {
      //запуск
      lcd.begin();
      lcd.attachEncoder(pinDT, pinCLK, pinSW);
      hdc1080.begin(0x40);

      //вывод символов на экран
      lcd.createChar(3,t1);
      lcd.createChar(4,t2);
      lcd.createChar(5,h1);
      lcd.createChar(6,h2);
      }




      void loop()
      {
      uint8_t selectedMenuItem;



      //считывание датчиков
      float temp1= hdc1080.readTemperature();
      float hum1= hdc1080.readHumidity();

      //экран ожидания нажатия на кнопку
      lcd.printAt(0,0,char(3));
      lcd.printAt(0,1,char(4));
      lcd.printAt(11,0,char(5));
      lcd.printAt(11,1,char(6));

      lcd.printAt(1,0,"=");
      lcd.printAt(1,1,"=");
      lcd.printAt(12,0,"=");
      lcd.printAt(12,1,"=");

      lcd.printAt(8,0,"C");
      lcd.printAt(18,0,"%");

      if(millis()-timing>=500){
      timing=millis();

      lcd.printAt(3,0,temp1);
      lcd.printAt(13,0,hum1);
      }
      //если кнопка нажата,перейти в меню
      if(lcd.getEncoderState()==eButton){
      selectedMenuItem = lcd.showMenu(menu, menuLen, 1);
      }

      //если никакие действия в меню не совершаются, выйти на экран ожидания
      ?????
      }

      Удалить
    3. инклуды не скопировались, но в коде они есть

      Удалить
    4. В файле LiquidCrystal_I2C_Menu.h есть строка:
      //#define INACTIVITY_TIMEOUT 60000 // Таймаут бездействия до выхода из функций
      Раскомментируйте её и укажите нужный таймаут бездействия в миллисекундах

      И я бы вынес отрисовку главного экрана в отдельную процедуру. В ней сделать опрос датчиков и вывод информации на дисплей. Вызывать ее в конце setup(), после выхода из меню и по условию if(millis()-timing>=500), хотя 500мс - это слишком часто. Так будет удобнее. Можно вызывать эту процедуру с параметром, который будет отвечать за полную перерисовку дисплея или только обновление показаний.

      При обновлении показаний без затирания старых значений возможна ситуация, когда было, например, 10C, при следующем замере стало 9 и отобразилось 90C. Я в таких случаях вывожу после значения дополнительный пробел:
      lcd.printfAt(0, 0, "%d ", x);

      Удалить
  40. можно ли применить как-то INACTIVITY TIMEOUT лишь для одной функции?

    ОтветитьУдалить
    Ответы
    1. Если только убрать её проверку из других функций библиотеки

      Удалить
  41. Теперь точно последний вопрос ;) и с гордость смогу выразить благодарность. При выборе некоторого температурного режима в меню у меня должна запускаться аппаратура которая собственно будет поддерживать температуру в некотором диапазоне. Если я буду использовать функцию-обработчик и INACTIVITY_TIMEOUT, то логично предположить, что рано или поздно функция поддержки климата схлопнется. Так вооот… можно ли как-то реализовать поддержку температуры в даном случае не прибегая к отказу от таймаута. Если да, то поделитесь информацией)

    ОтветитьУдалить
  42. Владир добрый вечер.
    В програирование я новичек подскажите пожалуйсто как реализовать выполнения оснойвной функции в моем случае это вычисление угловой скорости после ввода значения по варианту №5. Тоесть я потверждаю веденое значение и у меня програма переходит на фунцию вычисления скорости по входящими ипульсам. и при нпжатие на энкодер возвращается для изменения данных. Спасибо

    ОтветитьУдалить
  43. Good day Vladimir, a marvelous job on the library.
    I am a newbie on Arduino programming, your library work very well on Arduino board (Uno, Nano, mini Pro) but somehow its not work with ESP board (ESP8266, ESP32). do you have any hints to encounter this problem?

    ОтветитьУдалить
  44. Доброго времени суток.
    Владимир, есть такая необходимость:
    - в меню, из имеющихся пунктов, выбираем раздел "флешка", попадаем в соответствующую функцию, где происходит чтение имен файлов той самой флешки и выводится на дисплей, скажем в виде selectVal или пунктов подменю с последующим выбором одного из них. Подскажите пожалуйста, имеется ли возможность в данной библиотеке не имея заранее известных имен и их количества построить в список для дальнейшего выбора? Заранее благодарен.

    ОтветитьУдалить
  45. Добрый день Владимир. плата Arduino Mege 2560. При попытке компиляции примеров из вашей LiquidCrystal_I2C_Menu возникает Ошибка компиляции для платы... Подскажите пож. в чем может быть проблема

    ОтветитьУдалить
  46. В дополнение к предыдущему, примеры из LiquidCrystal_I2C_Ext компилируются нормально и работают.

    ОтветитьУдалить
  47. LiquidCrystal_I2C_Menu_Btns так же компилируется и работает, как с кнопками, так и с энкодером.

    ОтветитьУдалить
  48. Спасибо. Разобрался. Все работает. Отличная программа

    ОтветитьУдалить
  49. Этот комментарий был удален автором.

    ОтветитьУдалить
  50. Здравствуйте, Владимир!
    Вопрос! Как сделать многострочный вывод массива с возможностью прокрутки?

    void loop() {
    // Запрашиваем длину массива:
    uint8_t len = lcd.inputVal("Input array len", 5, 18, 8);
    uint8_t A[len];
    uint8_t t;

    // Запрашиваем элементы массива:
    for (uint8_t i = 0; i < len; i++) {
    lcd.printfAt(0, 0, "Input A[%d]: ", i);
    A[i] = lcd.inputValAt(12, 0, 0, 9, 5);
    }

    // Сортируем массив:
    for (uint8_t i = 0; i < len - 1; i++) {
    for (uint8_t j = i + 1; j < len; j++) {
    if(A[i] > A[j]){
    t = A[i];
    A[i] = A[j];
    A[j] = t;
    }
    }
    }

    // Выводим отсортированный массив на дисплей:
    lcd.clear();
    lcd.print("Sorted array:");
    lcd.setCursor(0, 1);
    for (uint8_t i = 0; i < len; i++)
    lcd.printf("%d ", A[i]);

    // Для продолжения ждем нажатия кнопки:
    lcd.printAt(0, 2, "Press button");
    lcd.printAt(0, 3, "to continue");
    while (lcd.getEncoderState() != eButton);
    }

    В строку влезает десять цифр, а если число 3-х значное. Или 18 чисел. Разбить попарно например с возможностью прокрутки.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Для вывода длинного текста с прокруткой в библиотеке есть функция printMultiline. Можно использовать её. Для этого нужно массив чисел поместить в строку String, дополнив каждое из них справа пробелами до ширины экрана -1. Например, для 16-символьного дисплея дополнить числа пробелами до 15 знаков. Шестнадцатым символом на дисплее будет знак прокрутки. Тогда такая строка при выводе на дисплей разобьётся по 15 символов и каждое число будет в аккурат на новой строке.
      Можно выводить по 2, по 3 числа в строку, дополнив их пробелами, чтобы суммарно получилось 15 символов. И склеить так весь массив в строку - смысл тот же.
      Дополнить пробелами можно, используя sprintf:
      #include "LiquidCrystal_I2C_Menu.h"
      LiquidCrystal_I2C_Menu lcd(0x27, 16, 2);

      const byte pinCLK = A1; // Энкодер пин A
      const byte pinDT = A2; // Энкодер пин B
      const byte pinBtn = A3; // Кнопка

      void setup() {
      char buffer[16];
      String s;
      lcd.begin();
      lcd.attachEncoder(pinDT, pinCLK, pinBtn);
      for (int i=0; i<10; i++){
      sprintf(buffer, "%-15d", i);
      s = s + buffer;
      }
      lcd.printMultiline(s);
      }

      void loop() {

      }

      Удалить
    2. Я так и сделал, но числа у меня меняются от 1 до 255 и перед ними я вставляю описание "Температура - 180" "Время - 30" Очень мудрёная программа получается. Я думал можно сделать проще. Ладушки, оставлю String + Sctring + String

      String (StringOne)= "Программа ";
      String (StringTemp)="Температура/C - ";
      String (StringTime)="Время/мин - ";
      StringProg = StringOne + p;
      int C;
      int T;
      int s;
      int address;
      address = p;
      lcd.printAt(0, 0, StringProg);
      for (s = 0; s < 18;s++){
      C = m[s];
      address = address + 4;
      s = s + 1;
      T = m[s];
      address = address + 4;
      Cr = StringProg + " " + StringTemp + C + " " + StringTime + T;
      lcd.printMultiline(Cr);

      Удалить
  51. Перевод строки /0D или каретки никак не вставить?

    ОтветитьУдалить
    Ответы
    1. StringProg + " " + StringTemp + C + " " + StringTime + T + /0D;

      Удалить
    2. Нет. По-моему, ничего такого дисплей не переваривает. Кстати, можно увеличить количество выводимых строк функцией printMultiline. Для этого нужно заменить используемые в ней 1-байтные переменные на 2-байтные. Позже поправлю это в библиотеке.

      Удалить
  52. Владимир, здравствуйте. Великолепная библиотека, огромное спасибо за ваш труд. Подскажите как вызвать inputValBitwise что бы она возвратила true, если пользователь подтвердил ввод, false, если отказался.

    ОтветитьУдалить
    Ответы
    1. Видимо, не надо так поздно сидеть)) Всё нашел:
      // Ввод 5-значного числа со знаком, 2 цифры после запятой:
      if (lcd.inputValBitwise("Input value", val, 5, 2, 1)) {
      lcd.print("You entered: ");
      lcd.print(val);
      }
      else
      lcd.print("Input canceled");

      Удалить
  53. Этот комментарий был удален автором.

    ОтветитьУдалить
  54. Владимир, здравствуйте!
    Подскажите пож, как с помощью вашей библиотеки можно реализовать ввод/корректировку типа данных дата и время?
    С возможностью контроля корректности дат, месяцев, часов и минут.
    Спасибо за вашу работу!

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      С часами и минутами всё понятно, это значение вида hh:mm - это можно сделать, например, при помощи функции inputStrVal (пример кода ниже). Это будет поразрядный ввод значения с проверкой корректности уже после ввода. Конечно, привычнее было бы по очереди ввести значения часов и минут, прокручивая их от 0 до 23 и от 0 до 59 соответственно, что-то вроде функции inputValAt. Но она не дополняет значение нулями, а без них получится не очень красиво. Вообще некрасиво). Поэтому в идеале или писать свою функцию, аналогичную inputValAt, или допилить её в библиотеке, чтобы она дополняла вводимое значение нулём слева. Тогда можно будет сначала вывести часы и минуты через двоеточие функцией printfAt, а затем запросить по очереди часы и минуты поверх этого текста.
      А с датами, месяцами - в зависимости от того, как представляете такой ввод на дисплее. Можно через ту же inputStrVal: запрашивать текст в формате "MM.DD HH:MM", после чего выделять из него нужные разряды и переводить их в числа.

      #include
      #include
      LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

      // Пины, к которым подключен энкодер
      #define pinCLK 2
      #define pinDT 3
      #define pinSW 4

      byte h = 9;
      byte m = 30;
      char timeAsText[6];

      void text2hm(char* str, byte& _h, byte& _m) {
      // Выделить из строки вида hh:mm значения часов и минут
      char buffer[3];
      memcpy(buffer, str, 2); // Берем первые 2 символа
      _h = atoi(buffer); // и переводим их в число. Это часы
      memcpy(buffer, str + 3, 2); // Берем 4 и 5 символ
      _m = atoi(buffer); // Это минуты
      }

      void hm2text(byte _h, byte _m, char* str) {
      // Соединить часы и минуты в строку
      sprintf(str, "%02d:%02d", _h, _m);
      }


      void setup() {
      lcd.begin();
      lcd.attachEncoder(pinDT, pinCLK, pinSW);
      hm2text(h, m, timeAsText);
      }

      void loop() {
      if (lcd.inputStrVal("Input time", timeAsText, 5, "0123456789")){
      text2hm(timeAsText, h, m);
      if ((h > 23) or (m > 59)) {
      // ругаемся на неверное значение
      }
      lcd.printfAt(0, 0, "hours: %d", h);
      lcd.printfAt(0, 1, "minutes: %d", m);
      }
      while (lcd.getEncoderState() == eNone);
      }

      Удалить
    2. Добрый вечер!
      Спасибо, воспользовался советом организации ввод даты и времени через inputStrVal.
      И вот это красиво :), без подключения string, а то тяжелая она...

      void text2hm(char* str, byte& _h, byte& _m) {
      // Выделить из строки вида hh:mm значения часов и минут
      char buffer[3];
      memcpy(buffer, str, 2); // Берем первые 2 символа
      _h = atoi(buffer); // и переводим их в число. Это часы
      memcpy(buffer, str + 3, 2); // Берем 4 и 5 символ
      _m = atoi(buffer); // Это минуты
      }

      Удалить
  55. Владимир, спасибо за проделанную работу! Отличная библиотека. С ее помощью сделал несколько небольших проектов. Замахнулся на большее, с длинным многоуровневым меню. Столкнулся с нехваткой динамической памяти(96%), много локальных переменных, стек порет кучу (atmega328)...
    Попытался перевести названия пунктов меню в PROGMEM с помощью макроса F():

    sMenuItem menu[] = {
    {mkBack, mkRoot, (F("Main menu")), NULL},
    .........
    }
    - ошибка компиляции "statement-expressions are not allowed outside functions nor in template-argument lists
    #define F(string_literal) (reinterpret_cast(PSTR(string_literal)))"
    Можно ли использовать программную память для хранения пунктов меню, как обойти данное ограничение?

    ОтветитьУдалить
    Ответы
    1. Наверное не правильно, но вариантов иных не нашел.
      Организовал вложенные меню созданием своих отдельных классов и вызов из головного меню подменю через эти отдельные классы. При этом перечисления кодов возвратов сделал сквозные.
      Тоже в связи с проблемами по памяти.

      Удалить
    2. Юрий, правильно ли я понял - классы подменю свои, без использования библиотеки?

      Удалить
    3. Вероятно я не так выразился.
      void showmenu() {
      uint8_t selectedMenuItem = mylcd.showMenu(menu, menuLen, 0);
      ...
      if (selectedMenuItem == mkPomps) {
      uint8_t selectedMenuItemExt = mylcd.showMenu(menuExt, menuLenExt, 0);
      }
      ...
      При выборе пункта меню mkPomps объекта "menu" стартует новое меню menuext


      // структура меню базовое : {ParentKey, Key, Caption, [Handler]}
      sMenuItem menu[] = {
      { mkBack, mkRoot, "Settings" },
      { mkRoot, mkDate, NULL, SetDT },
      { mkRoot, mkTime, NULL, SetDT },
      { mkRoot, mkUTC, NULL, SetDT },
      { mkRoot, mkGPS, "Set GPS" },
      { mkGPS, mkLat, NULL, SetGPS },
      { mkGPS, mkLon, NULL, SetGPS },
      { mkGPS, mkBack, "Back" },
      { mkRoot, mkAdj, "Adjust Sun" },
      { mkAdj, mkYan, NULL, SetMon },
      { mkAdj, mkFeb, NULL, SetMon },
      { mkAdj, mkMar, NULL, SetMon },
      { mkAdj, mkApr, NULL, SetMon },
      { mkAdj, mkMay, NULL, SetMon },
      { mkAdj, mkJun, NULL, SetMon },
      { mkAdj, mkJul, NULL, SetMon },
      { mkAdj, mkAug, NULL, SetMon },
      { mkAdj, mkSep, NULL, SetMon },
      { mkAdj, mkOkt, NULL, SetMon },
      { mkAdj, mkNov, NULL, SetMon },
      { mkAdj, mkDec, NULL, SetMon },
      { mkAdj, mkBack, "Back" },
      { mkRoot, mkPomps,"Set Pomps"},
      { mkRoot, mkBack, "Exit menu" }
      };

      // Расширенное меню настройки помп
      sMenuItem menuExt[] = {
      { mkBack, mkRoot, "Settings Pomps" },
      { mkRoot, mkLight, "Set Lighting" },
      { mkLight, mkLightDuration, NULL, SetDuration},
      { mkLight, mkLightWeek, NULL, SetWeek},
      { mkLight, mkBack, "Back" },
      { mkRoot, mkVac, "Set Vacuum" },
      { mkVac, mkVacStart, NULL, SetDuration},
      { mkVac, mkVacDuration, NULL, SetDuration},
      { mkVac, mkVacWeek, NULL, SetWeek},
      { mkVac, mkBack, "Back" },
      { mkRoot, mkChem, "Set Chemical" },
      { mkChem, mkChemStart, NULL, SetDuration},
      { mkChem, mkChemVolum, NULL, SetDuration},
      { mkChem, mkChemWeek, NULL, SetWeek},
      { mkChem, mkBack, "Back" },
      { mkRoot, mkBack, "Exit menu" }
      };

      Удалить
    4. Юрий здравствуйте!
      Хотел бы уточнить как вы заводили функцию void showmenu().
      А именно интересует - в каком месте кода (перед setup или где?). А так же как оэта функция привязывает к энкодеру? Или кнопкам.
      Спасибо!

      Удалить
  56. Юрий, мне кажется, что такой подход не позволит сэкономить RAM. Ведь любая строка в
    структуре меню отъедает память, байт на символ. А в случае использования экрана с русской кодировкой, как в моем случае, - 2 байта/символ.

    Можно, конечно, использовать костыль:

    sMenuItem menu[] = {
    {mkBack, mkRoot, NULL , Обработчик},
    .........
    }
    Где
    void Обработчик() {
    вывод пункта меню на экран из PROGMEM
    }

    но это нивелирует достоинство библиотеки - костыль он и есть костыль))

    ОтветитьУдалить
  57. Привет, у меня есть 2 вопроса.
    1) Как преобразовать миллисекунды в секунды? чтобы показать секунды, находящиеся внутри меню?
    2) как я могу напечатать больше строк, находясь внутри меню, в соответствии с выбором опции?

    с

    ОтветитьУдалить
  58. dear, since this is the best library for menu in arduino so far,i tried with esp32 but got lot of compile error ,worked on LiquidCrystal_I2C_Menu.cpp and fixed the issue ,please contact me to provide the updated file for future users,thank

    ОтветитьУдалить
    Ответы
    1. hi can you share me the library LiquidCrystal_I2C_Menu
      worked on the ESP32, I also had a lot of trouble compiling. Thank you very much
      My gmail is
      240683hn@gmail.com

      Удалить
  59. привет, я не могу сейчас говорить по-русски, поэтому я должен использовать гугл-переводчик
    Это программа, в которой я полагаюсь на вашу библиотеку в своих личных целях.
    пожалуйста, помогите мне с моими вопросами
    Я хочу использовать библиотеку #include для управления шаговым двигателем и использовать энкодер для поворота управляющего значения, затем нажать энкодер вниз, после чего двигатель заработает?
    Я хочу использовать Encoder для настройки значений времени, вентилятора, количества продуктов, скорости шагового двигателя, затем нажмите Encoder вниз, значение будет сохранено в памяти Mega2560, как мне подняться?
    Могу ли я использовать библиотеку #include для включения кнопок Start, Stop, Pause в программу?
    Это моя программа, вы можете мне помочь, у вас есть электронная почта, чтобы я мог связаться с вами?

    #include
    #include
    #include
    #include "pins.h"

    // Инициализировать ЖК-дисплей
    LCD LiquidCrystal_I2C_Menu (0x27, 20, 4);


    // Объявить Enum, включая Главные меню и подменю в Главном меню программы
    перечисление {mkBack, mkRoot, mkTime, mkFan, mkProduct, mkMove_X_Asix, mkMove_Y_Asix, mkSet_Time, mkSet_Fan, mkSet_SL, mkSet_Speed_X, mkSet_Speed_Y, mkXY_Speed,

    // Описание меню
    меню sMenuItem[] = {
    {mkBack, mkRoot, (char*) "Главное МЕНЮ"},
    {mkRoot, mk_Move_XY, (char*) "1. Переместить ось X + Y"},
    {mk_Move_XY, mkMove_X_Asix, (char*) "Переместить X"},
    {mk_Move_XY, mkMove_Y_Asix, (char*) "Переместить Y"},
    {mk_Move_XY, mkBack, (char*) "Назад"},
    {mkRoot, mkTime, (char*) "2. Контроль времени"},
    {mkTime, mkSet_Time, (char*) "Set_Time"},
    {mkTime, mkBack, (char*) "Назад"},
    {mkRoot, mkFan, (char*) "3. Скорость вентилятора"},
    {mkFan, mkSet_Fan, (char*) "Set_Fan Cooling"},
    {mkFan, mkBack, (char*) "Назад"},
    {mkRoot, mkProduct, (char*) "4. Контроль продукта"},
    {mkProduct, mkSet_SL, (char*) "Set_Product"},
    {mkProduct, mkBack, (char*) "Назад"},
    {mkRoot, mkSet_Speed_X, (char*) "5. Скорость оси X"},
    {mkSet_Speed_X, mkX_Speed, (char*) "Set_Speed_X"},
    {mkSet_Speed_X, mkBack, (char*) "Назад"},
    {mkRoot, mkSet_Speed_Y, (char*) "6. Скорость оси Y"},
    {mkSet_Speed_Y, mkY_Speed, (char*) "Set_Speed_Y"},
    {mkSet_Speed_Y, mkBack, (char*) "Назад"},
    {mkRoot, mkBack, (char*) "7. Меню выхода"},
    };

    недействительная установка () {
    ЖК.начало();
    ЖК-подсветка();
    ЖК.очистить();
    lcd.setCursor(4, 0);
    lcd.print ("МЭК");
    lcd.setCursor(4, 1);
    lcd.print("0996");
    lcd.setCursor(3, 2);
    LCD.print("125");
    lcd.setCursor(6, 3);
    lcd.print ("Версия 1.0");
    задержка(5000);
    lcd.attachEncoder (pinDT, pinCLK, pinSW);
    }

    недействительный цикл () {
    // Показать меню
    uint8_t menuLen = размер(меню) / размер(sMenuItem);
    uint8_t selectedMenuItem = lcd.showMenu (меню, menuLen, 1);
    // Распечатать выбранный операционный блок с выбранными функциями Total Menu
    если (selectedMenuItem == mkMove_X_Asix)
    lcd.print («мм»);
    иначе если (selectedMenuItem == mkMove_Y_Asix)
    lcd.print («мм»);
    иначе если (selectedMenuItem == mkSet_Time)
    lcd.print («с»);
    иначе если (selectedMenuItem == mkSet_Fan)
    lcd.print ("Об/мин");
    иначе если (selectedMenuItem == mkSet_SL)
    lcd.print("SL");
    иначе если (selectedMenuItem == mkX_Speed)
    lcd.print("мм/с");
    иначе если (selectedMenuItem == mkY_Speed)
    lcd.print("мм/с");
    задержка (2000 г.);
    }

    ОтветитьУдалить