пятница, 12 октября 2018 г.

LiquidCrystal_I2C_Ext - библиотека для создания меню на Ардуино

Я обратил внимание, что тема создания меню на Ардуино и ЖК дисплее весьма популярна. И ей уже посвящена одна из моих публикаций. Но я понимаю, что для новичков адаптация моего скетча может показаться нетривиальной задачей. Поэтому я решил написать библиотеку для создания меню на Ардуино и ЖК дисплее с I2C управлением, которую было бы легко использовать даже начинающему ардуинщику.

В моем распоряжении имеется ЖК дисплей 20x4 с I2C интерфейсом, к сожалению, без поддержки кириллицы. Для работы с ним нужна библиотека, я использую LiquidCrystal_I2C. И, чтобы моя библиотека не была отдельной надстройкой, завязанной на LiquidCrystal_I2C, я решил доработать последнюю, добавив в нее новые функции. И речь не только о создании меню: я добавил в библиотеку различные наработки, накопившиеся у меня за время работы с данным дисплеем.

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

Получившуюся библиотеку я назвал LiquidCrystal_I2C_Ext, скачать ее можно по ссылке http://clc.la/LiquidCrystal_I2C_Ext . По сравнению с предшественницей в ней появились следующие функции:

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

Я приведу несколько примеров с данными функциями, чтобы не осталось вопросов по их использованию.

printAt, printf, printfAt

#include <Wire.h>
#include <LiquidCrystal_I2C_Ext.h>
LiquidCrystal_I2C_Ext 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 поддерживает те же типы данных, что и print: вы можете выводить на дисплей целые и дробные числа, текстовые строки (будь то массив символов или переменная типа String). А при работе с функциями форматированного вывода не забывайте, что они не поддерживают тип String и передавать им нужно указатель на строку в стиле Си. Для этого достаточно вызвать функцию c_str() класса String, в примере выше это показано.

Говоря о форматированном выводе, хочу отметить еще один момент. Реализация функций семейства printf на Ардуино поддерживает не все команды форматирования. Это было сделано с целью уменьшить размер библиотеки. Лично я столкнулся с невозможностью указать в формате printf значения width и precision через переменную (т.е. через символ *).

attachEncoder, getEncoderState

#include <Wire.h>
#include <LiquidCrystal_I2C_Ext.h>
LiquidCrystal_I2C_Ext 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 и выводится на дисплей. Нажатие на кнопку приводит к обнулению переменной.

printMultiline

#include <avr/pgmspace.h>
#include <Wire.h>
#include <LiquidCrystal_I2C_Ext.h>
LiquidCrystal_I2C_Ext 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); // И освобождаем буфер
  lcd.printMultiline(F("Using F() macro example. Press button to continue."));
}

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

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

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

inputVal, inputValAt

#include <Wire.h>
#include <LiquidCrystal_I2C_Ext.h>
LiquidCrystal_I2C_Ext 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]);
  
  // Для продолжения ждем нажатия кнопки:
  lcd.printAt(0, 2, "Press button");
  lcd.printAt(0, 3, "to continue");
  while (lcd.getEncoderState() != eButton);
}

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

inputValBitwise

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

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

long v = 0;

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

void loop() {
  if (lcd.inputValBitwise("Input value", v, 5))
    lcd.printf("You entered %ld", v);
  else
    lcd.print("Input canceled");
  lcd.printAt(0, 2, "Press button");
  lcd.printAt(0, 3, "to continue");
  while (lcd.getEncoderState() != eButton); // Для продолжения ждем нажатия кнопки
}

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

inputStrVal

#include <Wire.h>
#include <LiquidCrystal_I2C_Ext.h>
LiquidCrystal_I2C_Ext 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");
  press_button_to_continue();
}

void press_button_to_continue(){
  lcd.printAt(0, 2, "Press button");
  lcd.printAt(0, 3, "to continue");
  while (lcd.getEncoderState() != eButton);
}

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

selectVal

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

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

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

void loop() {
  int index;
  String list[] = {"Off", "On"};
  index = lcd.selectVal("Turn backlight", list, 2, lcd.getBacklight());
  lcd.setBacklight(index);
  lcd.printf("Backlight turned %s", list[index].c_str());
  delay(2000);
}

Функция selectVal очень полезна при выборе значения из списка. Эта задача может быть решена и с использованием меню, но преимущество функции selectVal состоит в том, что она не только позволяет выбрать значение, но еще и показывает текущее выбранное значение. Функция работает с массивами значений типа char*, String или int и возвращает индекс выбранного элемента.

showMenu

#include <Wire.h>
#include <LiquidCrystal_I2C_Ext.h>
LiquidCrystal_I2C_Ext 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};

// Опишем меню
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, которая берет на себя отрисовку меню и навигацию по нему. Работу с ней можно разделить на 3 этапа. Сначала нужно описать меню. Для этого в библиотеке определена структура sMenuItem:
struct sMenuItem {
  uint8_t parent;
  uint8_t key;
  char    *caption;
};

Параметры parent и key служат для задания иерархии, caption – указатель на название элемента меню. В примере показано, как описывается меню в виде массива элементов sMenuItem.

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

Следующий этап – вызов функции showMenu. Ее параметрами являются описанное ранее меню, его длина и признак отвечающий за вывод заголовка.

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

По-моему, получилась интересная библиотека.  Есть некоторые вещи, которые хотелось бы доработать (например, в меню нет прокрутки длинных названий),  но я решил их пока отложить, т.к. сейчас занимаюсь другими проектами. В общем скачивайте, пробуйте, оставляйте комментарии.

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

  1. Хорошая работа. Спасибо.
    Сегодня наткнулся.
    Использую в проекте Ваше Меню на библиотеке LiquidCrystal_I2C.
    Буду переносить на эту. Некоторые моменты должны получиться красивее :))

    ОтветитьУдалить
  2. Большое спасибо за такую крутую крутую библиотеку, то что надо!Потыркалсясо своим проектиком и возник вопрос: как при обработке конечного элемента меню узнать его родителя(в каком предыдущем элементе меню он находится)?
    у меня меню такого вида:
    заголовок 1
    вариант 1
    вариант 2
    заголовок 2
    вариант 1
    вариант 2
    при обработке элемента "вариант 1" нужно знать к какому заголовку он принадлежит. Спасибо. Если не сложно, можете ответить на почту ag.balakin@gmail.com

    ОтветитьУдалить
  3. Приветствую! Скачал, поставил библиотеку, скопировал текст с примером подключения энкодера. Первый раз все залилось хорошо, второй раз решил залить (поправил чтоб выводил в сериал, экрана нет под рукой), поймал ошибку:
    LiquidCrystal_I2C_Ext.cpp:4:18: fatal error: Wire.h: No such file or directory
    #include
    ^
    compilation terminated.
    Ошибка компиляции.

    Подскажите в чем может быть загвоздка?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Приложите скетч, при котором выходит эта ошибка. Версия IDE какая?

      Удалить
    2. #include
      LiquidCrystal_I2C_Ext 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); // Выводим значение счетчика на дисплей
      }

      Вот такую ошибку выдает:
      C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Ext\LiquidCrystal_I2C_Ext.cpp:4:18: fatal error: Wire.h: No such file or directory
      #include
      ^
      compilation terminated.
      Ошибка компиляции.

      Версия Arduino 1.6.5

      Удалить
    3. У меня все библиотеки находятся в папке ARDUINO_DIR\libraries\ и подобных ошибок не возникало.
      Переместив библиотеку в C:\Users\User\Documents\Arduino\libraries\ смог воспроизвести вашу ошибку. Не могу сказать почему в этом случае она возникает, но лечится она добавлением wire.h в секцию include:
      #include "Wire.h"

      Удалить
    4. Большое спасибо, удалось побороть проблему!

      Удалить
  4. Добрый день! Спасибо за библиотеку. Очень удобно. Только функции inputLongVal и inputStrVal работают некорректно. Это только у меня так? inputLongVal печатает всю строку нулями независимо от количества разрядов для ввода и соотв сообщение не появляется.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Проблему увидел, но сейчас нет возможности разобраться. Завтра отвечу.

      Удалить
  5. Еще вопрос: Как с помощью этой библиотеки напечатать строку формата HH:MM:SS с возможностью редактирования ?

    ОтветитьУдалить
    Ответы
    1. Объявите строку, инициализируйте значением вида 00:00:00, оно по сути будет маской ввода: при вызове inputStrVal для ввода цифр (последний параметр функции - INPUT_09) вы не сможете изменить в строке нецифровые символы.

      char s[] = "10:00:00";
      if (lcd.inputStrVal("Input time", s, 8, INPUT_09))
      lcd.printf("%s", s);
      else
      lcd.print("Input canceled");

      Удалить
  6. А, еще заметил что русские комментарии в файлах .h и .cpp в неверной кодировке.

    ОтветитьУдалить
  7. Спасибо за ответ! Да, в общем, сам додумался до этого. Еще раз спасибо за библиотеку!

    ОтветитьУдалить
  8. Еще вопрос. А никак из функции, например из inputIntVal, выйти не в корень меню, а в нужное подменю? Просто меню может быть глубоким, и каждый раз после выхода из функции возвращаться обратно долго. Спасибо.

    ОтветитьУдалить
    Ответы
    1. В этой библиотеке никак. Может быть в будущем добавлю такую возможность

      Удалить
    2. Присоединяюсь, отличная библиотека, но выброс из многоуровнего меню в главное - как то не логично

      Удалить
  9. Если вдруг займетесь доработкой и добавите возможность попадать из обработчика на уровень выше, или в любое место, а также отдельную функцию печати и вывода в переменные типа int времени формата ЧЧ:ММ:СС и ЧЧ:ММ, то я уверен, ваша библиотека будет лучшей из того что на сегодня есть. В порядке фантазии, вероятно, нужна еще будет возможность редактирования нескольких переменных в одном окне с прокруткой окна. Удачи! Еще раз спасибо.

    ОтветитьУдалить
  10. Здравствуйте!
    А можно адаптировать библиотеку под экран 16х2?
    При создании меню используя функцию inputLongVal каша получается.
    Допустим число разрядов =4, вводимое число 1024 тогда картинка на экране получается такого вида, 2 строка:
    1ОК4 cancel
    то есть ОК поверх вводимых чисел.
    Спасибо!

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Попробую. У меня нет возможности протестировать на 1602, поэтому попробуйте все функции, может что-то еще нужно поправить?

      Удалить
  11. сделал так, вроде работает:
    bool LiquidCrystal_I2C_Ext::inputStrVal(const char title[], char buffer[], uint8_t len, uint8_t availSymbols) {
    eEncoderState encoderState = eNone;
    uint8_t l = len;
    if (l > _cols) l = _cols;
    char tmpBuffer[l];
    memcpy(tmpBuffer, buffer, l);
    uint8_t pos = 0;
    clear();
    print(title); // Выводим заголовок
    printAt(0, 1, buffer);
    //printAt(1, _rows - 1, "OK");
    printAt(6, _rows - 1, "OK");
    printAt(_cols - 6, _rows - 1, "Cancel");
    setCursor(0, 1);
    cursor();

    //Основной цикл - выбор разряда для изменения либо OK/Cancel
    while (1) {
    encoderState = getEncoderState();
    switch (encoderState) {
    case eNone: {
    encoderIdle();
    continue;
    }
    case eLeft: { // Двигаем курсор влево
    if (pos == 0) continue; // Левее перемещаться некуда
    if (pos == 255) { // Выбран Cancel, перемещаемся к OK
    //printAt(0, _rows - 1, ">");
    printAt(5, _rows - 1, ">");
    printAt(_cols - 7, _rows - 1, " ");
    pos--;
    continue;
    }
    if (pos == 254) { // Выбран OK, перемещаемся к строке
    //printAt(0, _rows - 1, " ");
    printAt(5, _rows - 1, " ");
    pos = l - 1;
    setCursor(pos, 1);
    cursor();
    continue;
    }
    // Выбрана строка, перемещаемся к символу слева
    setCursor(--pos, 1);
    continue;
    }
    case eRight: { // Двигаем курсор вправо
    if (pos == 255) continue; // Правее перемещаться некуда
    if (pos == 254) { // Выбран Ok, перемещаемся к Cancel
    //printAt(0, _rows - 1, " ");
    printAt(5, _rows - 1, " ");
    printAt(_cols - 7, _rows - 1, ">");
    pos++;
    continue;
    }
    if (pos == l - 1) { // Выбран крайний правый символ, перемещаемся к OK
    noCursor();
    //printAt(0, _rows - 1, ">");
    printAt(5, _rows - 1, ">");
    pos = 254;
    continue;
    }
    setCursor(++pos, 1);
    continue;
    }
    case eButton: { //Нажата кнопка
    if (pos == 255) {
    noCursor();
    clear();
    return 0; // Cancel.
    }
    if (pos == 254) { // OK.
    noCursor();
    clear();
    memcpy(buffer, tmpBuffer, l);
    return 1;
    }
    // Редактирование выбранного символа
    encoderState = eNone;
    setCursor(pos, 1);
    blink();
    while (encoderState != eButton)
    {
    encoderState = getEncoderState();
    switch (encoderState) {
    case eNone: {
    encoderIdle();
    continue;
    }
    case eLeft: {
    if (!getNextSymbol(tmpBuffer[pos], 0, availSymbols)) continue;
    printAt(pos, 1, tmpBuffer[pos]);
    setCursor(pos, 1);
    continue;
    }
    case eRight: {
    if (!getNextSymbol(tmpBuffer[pos], 1, availSymbols)) continue;
    printAt(pos, 1, tmpBuffer[pos]);
    setCursor(pos, 1);
    continue;
    }
    }
    }
    noBlink();
    continue;
    }
    }
    }
    }

    ОтветитьУдалить
    Ответы
    1. так и не получилось запустить меню без задержки по циклу...

      Удалить
    2. Библиотеку с гитхаба скачали? Разобрались с функцией attachIdleFunc?

      Удалить
  12. ограничиваем число разрядов до п и пишем всё в одну строку и вводные данные и ок и cancel.
    По поводу проверить все возможности - проверю, собираюсь вашу библиотеку в своем проэкте использовать.

    ОтветитьУдалить
  13. ограничиваем число разрядов до пяти...
    не пропечаталось что-то

    ОтветитьУдалить
  14. Проверил частично. Как с Вами можно связаться? Вопросы назрели, здесь будет не удобно.

    ОтветитьУдалить
  15. Спасибо!
    Классная библиотека. Для своих проектов, если используется экран и энкодер, считай 90 процентов проекта уже готово - всё есть в библиотеке: меню, энкодер, ввод, вывод данных на экран!

    ОтветитьУдалить
  16. Очень перспективная библиотека, но, прошу у автора прощение, не смотря на титанический труд, библиотека недоработана. Я уже 30 лет не написал ни строчки кода, думал что все быстро восстановится, но пока получается медленно - мозги уже не так быстро шевелятся как в молодости. Да в принципе и задачи такой нет восстановить прошлые навыки, есть необходимость решить насущные проблемы. Думал ваша библиотека решит мои проблемы. Неделю уже ее кручу и так и так, но похоже придетстя все самому писать, что сильно усложнит мои проблемы. Для моих целей нужно что бы на экране постоянно выводился статус системы (показания датчиков и статусы исполнительных механизмов). При нажатии клавиши - переход в меню. Это часть реализовывается с этой библиотекой легко. А вот дальше проблемы. У меня трех уровневое меню и обработка любого пункта функциями iputVal, inputStrVal приводит в вылету в головное меню. Если в третьем уровне 10 пунктов меню и часть из них нужно отредактировать, это превращается в пытку. Была проблема с вводом форматированных значений времени (контроль по макс часы и минуты), но я это благополучно исправил. Владимир, повторюсь, очень нравится ваш стиль програмирования - все легко читается. Нахожусь в дурацкой ситуации, понимаю что не могу вас просить ни о чем - вы мне ничего не должны, но если вы решили подарить людям классную библиотеку, то сделайте это в виде законченного продукта, в таком виде она идеальна для одноуровневых меню.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Библиотека задумывалась как облегченный способ организации меню и альтернатива приведенному ранее скетчу (https://tsibrov.blogspot.com/2018/01/arduino-text-menu.html). В том скетче были предусмотрены и прокрутка текста, и автоматическое отключение подсветки дисплея, и возможность обрабатывать выбранный пункт как после выхода из меню (как сделано в данной библиотеке), так и не выходя из него путем вызова соответствующего обработчика. В итоге скетч получился довольно сложный, от я этого я и хотел уйти в библиотеке.
      А по поводу того что она не доработана - кому-то не хватает одного функционала, другому - другого. Всё ведь в нее не засунешь, согласны?
      Конкретно про возврат к определенному уровню и пункту меню при повторном вызове showMenu - это будут те еще костыли. Я не вижу здесь приемлемой реализации. Могу попробовать добавить обработчики - пользовательские функции, которые будут отрабатывать при выборе пункта меню, при этом выход из меню происходить не будет. Сейчас дисплея нет под рукой, проверить смогу в понедельник.

      Удалить
    2. Владимир, спасибо за ответ. Мне кажетя что с обработчиками будет совсем не плохо.

      Удалить
    3. Попробуйте. В архиве библиотека и пример использования обработчиков
      https://drive.google.com/open?id=1ySkWabqiRiSADExXiSEt35W_JjTVB0xn

      Удалить
    4. Сегодня вечером попробую, завтра отпишусь. Спасибо

      Удалить
    5. Владимир, вставил в свой код - все работает как надо. Огромное спасибо!!!

      Удалить
    6. Не иогу понять, попробовал покправлять подсветкой - вешается микропроцессор

      Удалить
    7. Покажите скетч. И посмотрите пример в \examples\selectVal\ он как раз управляет подсветкой. Только что проверил - работает.

      Удалить
    8. Проблема вот в чем, я пытался из функции обработчика прерывания управлять подсветкой. Процессор вешается сразу. В цикле loop все нормально. Вроде правил я никаких не нарушал, но вот так. Проверил по оригинальной библиотеке - тоже самое

      Удалить
    9. Э не, прерывания - это совсем другое дело. Я не ковырял код библиотеки wire (посредством которой библиотека и общается с дисплеем), но, наверняка, она не обходится без прерываний. Вот только при входе в обработчик прерывания обработка последующих прерываний запрещается до выхода из него. Уверен, что дело в этом, тогда вам должна помочь команда sei(); в самом начале вашего обработчика. Попробуйте.

      Удалить
    10. Спасибо, заработало!!! Только не понятно, стоит ли в конце обработчика ставить cli, не будет ли конфликта, ведь после выхода из обработчика прерывания опять будут разрешены

      Удалить
    11. Конфликта не будет. Команда возврата из обработчика (reti) в любом случае разрешит прерывания.

      Удалить
  17. Новая версия с обработчиками это очень круто! Только я не понял как передавать аргументы в обработчик функций.

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

      Удалить
  18. void _set_lengthDay() {
    byte hs, ms, he, me;
    c[selectedMenuItem].getStart(hs, ms);
    c[selectedMenuItem].getEnd(he, me);
    }
    selectedMenuItem - глобальная переменная. Если использовать функцию _set_lengthDay() в обработчике selectedMenuItem всегда равна 0, хотя она очевидно меняется во время работы меню.

    ОтветитьУдалить
    Ответы
    1. Вы хотели сделать универсальный обработчик для нескольких пунктов меню? Не получится, нужны отдельные обработчики.
      selectedMenuItem - это ваша переменная, библиотека с ней никак не взаимодействует.

      Удалить
  19. Добрый день.
    Спасибо за интересную библиотеку. Я так понял, что пока выполняется Меню, все остальное затормаживается, пока не выйдешь из меню. Или я не прав.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Да. Чтобы выполнился код, идущий после вызова showMenu, нужно выйти из меню.

      Удалить
    2. Добрый вечер.
      Наверное логичней было бы сделать выход из меню через таймаут, если не было команд энкодера. Мало ли - пользователь забыл выйти из меню и все подвисло.

      Удалить
  20. Добрый день Владимир.
    У меня вопрос, а как из вашей библиотеки использовать функции другой библиотеки. Конкретнее я хочу использовать в вашей библиотеке кроме энкодера еще и кейпад. Для кейпада есть библиотека i2ckeypad в ней всего 2 метода init и get_key. Из вашей библиотеки нужно использовать только get_key. Как это можно реализовать. Или подскажите куда копать. Заранее спасибо.

    ОтветитьУдалить
    Ответы
    1. Добрый!
      Ну так добавьте в LiquidCrystal_I2C_Ext.cpp директиву #include с той библиотекой и вызывайте get_key. Или я что-то не понял?

      Удалить
    2. Добрый вечер.
      Мне нужно вызывать метод get_key из программы и из вашей библиотеки. Я решил это изменив вашу библиотеку, добавив в нее методы, переменные и т.п., чтобы использовать кейпад аналогично энкодеру. Вроде все работает, пока проверяю и причесываю измененную библиотеку. Еще раз спасибо.

      Удалить
  21. Добрый день, Владимир.
    Еще раз спасибо за такую библиотеку.
    Один вопрос, если можно:
    Функция showMenu использует цикл do..while и применение showMenu в теле loop останавливает его.
    Это специально сделано или случайно перешло из Вашей программы меню на LCD?
    Там я заменил 1 в while на флаг и заходил/выходил из меню.
    А библиотеку править не хотелось бы. Может я что-то не так делаю у себя в программе и функцию showMenu надо использовать не так, как в примере, тогда основной цикл будет крутиться?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      "Функция showMenu использует цикл do..while и применение showMenu в теле loop останавливает его" - совершенно верно. Функция showMenu - это цикл, выход из которого возможен только после выбора элемента меню. После этого функция возвращает ключ выбранного элемента. А как иначе?

      Удалить
    2. Наверное, не правильно задал вопрос.
      На ардуинке (у меня LCD с меню висит на NANO) кроме Меню и LCD есть еще другие задачи
      Функция по нажатию вернула ключ, цикл loop прошел один раз, отработал все, что надо и для других задач один раз и остановился в ожидании следующего нажатия в Меню. А как быть другим подпрограмм (например, опрос датчиков, обмен с другими ардуинками по ModBus и т.д.)
      Т.е. мой вопрос:
      Если применять функцию showMenu, то подразумевается, что ардуинка работает ТОЛЬКО на обработку Меню и все остальные задачи решать через прерывания? Или вызов этой функции надо куда-то загнать, чтобы не тормозила loop, но отслеживала свою задачу?

      Удалить
    3. Т.е. у вас вызов showMenu прямо внутри loop? В таком случае, конечно, меню будет вызываться постоянно, мешая работе основной программы. Добавьте условие для вызова showMemu, например, при нажатии кнопки энкодера.

      Удалить
    4. Спасибо. Так и сделаю.
      Только учусь и поэтому не вижу сразу простейших решений.

      Удалить
  22. Добрый день, Владимир.
    Еще вопрос. Почему при компиляции выскакивают сообщения:
    E:\...\showMenu_handlers\showMenu_handlers.ino:41:1: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
    Строка 41 - это конец описания меню.
    Скетч все равно компилируется и загружается, но интересно

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Компилятору не нравятся команды вида
      char *s = "Text";
      При инициализации массива меню именно именно это и происходит, поэтому в сообщении указана строка 41.
      Руки дойдут, я постараюсь избавиться от всех предупреждений. Это именно предупреждение, не ошибка. Работе программы они не мешают.

      Удалить
  23. Владимир еще один вопрос.
    В некоторых конструкциях Switch...Case... используется оператор continue. Это принципиально, почему не использовать break. Пока я меняю его на goto, т.е. переход на после switch. У меня после switch добавлено немного своего.

    ОтветитьУдалить
    Ответы
    1. Если нашелся нужный case, выполнились соответствующие действия и до следующий итерации ничего делать не требуется, то логичнее использовать continue. Но можно и break. При условии, что код, идущий после switch, актуален для всех веток case. Поэтому принципиально или нет надо смотреть по ситуации.

      Удалить
    2. Спасибо.
      У меня после switch (обработка энкодера) идет код для обработки кейпада, то же switch. Думаю что будет нормально. Попробую, если не получится оставлю goto, с ним пока ошибок не выявлено.

      Удалить
  24. Добрый день, Владимир.
    А можно с помощью inputValBitwise() вводить отрицательные числа.

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Можно. Описание функций и их параметров приведено в начале публикации

      Удалить
    2. Но у меня в примере не получается ввести отрицательное значение. Вводятся только цифры 0-9. Или я что-то не так делаю.

      Удалить
    3. Посмотрите все-таки описание функций вверху. Я же не просто к слову о нем упомянул.

      Удалить
    4. Я дико извиняюсь, но в описании описано 3 параметра:
      В параметрах функции присутствует заголовок, переменная, в которую будет помещено введенное значение, и количество разрядов для ввода.
      А судя по библиотеке их 5:
      inputValBitwise(const String &title, T &value, uint8_t precision, uint8_t scale, bool _signed)
      Вот у меня и не получалось ввести отрицательные. Задал все 5 параметров и все сработало.

      Удалить
    5. Вы смотрите пример вызова.
      А я говорю про описание, которое приведено в начале публикации.

      Удалить
    6. Извините, как-то проглядел это описание. Привык что в примерах обычно показывают максимум возможностей (параметров) функции.

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

    ОтветитьУдалить
  26. Ранее Вы опубликовали проект генератора на AD9833 с предыдущей реализацией меню. Возможно переделать тот скетч на новой библиотеке для сравнения? Думаю увидев рядом две реализации одного проекта наглядно поможет оценить эти разные подходы и сделать выбор - какой развивать у себя.
    Я сейчас реализовал "тот" проект, для моих целей в структуре меню не все устраивает, хочу переделать под себя, но сложно....

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

      Удалить
    2. В том то и дело, что мне, как сильно начинающему, сложно понять без сравнения с конкретным примером как это сделать. Абстрактные примеры уверенности не добавляют, а на конкретном работающем образце (огромное Вам спасибо за https://tsibrov.blogspot.com/2018/06/ad9833.html) я бы, пожалуй, разобрался. Мне нужно будет переделать меню и скетч (под 1602, таймер выключения, регулировку амплитуды и т.д.), самому это полезнее и интереснее.

      Удалить
  27. Ответы
    1. Добрый день!
      Напишите свой email, я пришлю вариант реализации генератора на этой библиотеке

      Удалить
    2. mr.alpen@yandex.ru
      заранее благодарю, если еще для 1602 то ....

      Удалить
    3. а можно и мне увидеть сие чудо? johnnsoft@gmail.com

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

      Удалить
  28. если есть возможность выложите пожалуйста пример использования этого меню хотябы на примере морганием светодиодом в 1 пцнкте задаешь например интервал во 2 выполняется программа

    ОтветитьУдалить
    Ответы
    1. Я переношу библиотеку на github, скачайте ее оттуда:
      https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu
      и посмотрите пример Using_handlers. В нем 2 обработчика, в одном задавайте интервал, в другом сделайте цикл для моргания светодиодом.

      Удалить
    2. спасибо большое!помогло!!!!вроде разобрался

      Удалить
    3. Блин, мне не помогло... Как только в основном лупе вызываю библиотеку - сразу блинк перестаёт блинкать... Уж что я только не делал - даже на Arduino Due подключал шедулер и создавал второй луп - всё равно всё вешается наглухо. Хотя блинк у меня через миллисы работает, даже не через делеи...
      ХЕЛП плиз!

      Удалить
    4. Библиотека отличнейшая и простая в управлении, но... Делаю всё как в примере Using_handlers

      #include // ПОДКЛЮЧАЕМ САМУ БИБЛИОТЕКУ
      const int ledPin = 13; // номер выхода, подключенного к светодиоду
      int ledState = LOW; // этой переменной устанавливаем состояние светодиода
      long previousMillis = 0; // храним время последнего переключения светодиода
      // ====== ЗАДАЮ ПИНЫ ЭНКОДЕРУ ======
      #define pinSW 11
      #define pinDT 10
      #define pinCLK 9
      int speedDelay; // КОНТРОЛЬ ЗАДЕРЖКИ (В МИЛЛИСЕКУНДАХ, дефолтный = 4Гц, 125 мс)
      int powerState = 90; // ДЕФОЛТНОЕ ЗНАЧЕНИЕ ЯКРОСТИ
      int speedNumber = 25; // ДЕФОЛТНОЕ ЗНАЧЕНИЕ СКОРОСТИ
      // ФУНКЦИЯ РЕГУЛЯТОРА ЯРКОСТИ
      void SetBrightness() {
      powerState = lcd1.inputVal("Input brightness(%)", 0, 100, powerState, 5);
      }
      // ФУНКЦИЯ РЕГУЛЯТОРА СКОРОСТИ
      void SetSpeed() {
      speedNumber = lcd1.inputVal("Input speed", 0, 32, speedNumber, 1);
      }
      // СОБСТВЕННО, САМ ГЕНЕРАТОР (ТОЧНЕЕ, ЕГО ФУНКЦИЯ)
      void generator()
      {
      // МАПИМ ЗНАЧЕНИЕ ЭНКОДЕРА (ОТ 0 ДО 32) В ЗНАЧЕНИЕ ДЛЯ ГЕНЕРАТОРА В МС
      speedDelay = map(speedNumber, 0, 32, 400, 10);
      unsigned long currentMillis = millis();
      // ПРОВЕРЯЕМ НЕ ПРОШЕЛ ЛИ НУЖНЫЙ ИНТЕРВАЛ, И ЕСЛИ ПРОШЕЛ ТО
      if (currentMillis - previousMillis > speedDelay) {
      // СОХРАНЯЕМ ВРЕМЯ ПОСЛЕДНЕГО ПЕРЕКЛЮЧЕНИЯ
      previousMillis = currentMillis;
      // ЕСЛИ СВЕТОДИОД НЕ ГОРИТ, ТО ЗАЖИГАЕМ, И НАОБОРОТ
      if (ledState == LOW)
      ledState = HIGH;
      else
      ledState = LOW;
      // УСТАНАВЛИВАЕМ СОСТОЯНИЯ ВЫХОДА, ЧТОБЫ ВКЛЮЧИТЬ ИЛИ ВЫКЛЮЧИТЬ СВЕТОДИОД
      digitalWrite(ledPin, ledState);
      }
      }
      // ОБЪЯВЛЯЕМ ВСЕ ПУНКТЫ МЕНЮ
      enum {mkBack, mkRoot, mkSetBrightness, mkSetSpeed};
      // ЗАДАЁМ СТРУКТУРУ (ДЛЯ ПРОСТОТЫ 1 УРОВЕНЬ)
      sMenuItem menu[] = {
      {mkBack, mkRoot, "Main menu", NULL},
      {mkRoot, mkSetBrightness, "SetBrightness", SetBrightness},
      {mkRoot, mkSetSpeed, "SetDelay", SetSpeed},
      {mkRoot, mkBack, "Back", NULL},
      };
      // Determine the number of items in the menu
      uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem);
      void setup() {
      // ИНИЦИАЛИЗИРУЕМ ЖКИ
      lcd1.begin();
      lcd1.backlight();
      // ПОДТЯГИВАЕМ ПУЛ-АПОМ ЭНКОДЕР
      pinMode(pinSW, INPUT_PULLUP);
      pinMode(pinDT, INPUT_PULLUP);
      pinMode(pinCLK, INPUT_PULLUP);
      // ЗАДАЕМ РЕЖИМ ВЫХОДА ДЛЯ ЛЕД-ПИНА
      pinMode(ledPin, OUTPUT);
      // ЦЕПЛЯЕМ ЭНКОДЕР
      lcd1.attachEncoder(pinDT, pinCLK, pinSW);
      }
      // ОСНОВНОЙ ЛУП
      void loop() {
      generator (); // ВЫЗОВ ФУНКЦИИ ГЕНЕРАТОРА
      // КАК ТОЛЬКО ПОДКЛЮЧАЕТСЯ ВЫЗОВ МЕНЮ - СВЕТОДИОД ПЕРЕСТАЁТ МИГАТЬ
      uint8_t selectedMenuItem = lcd1.showMenu(menu, menuLen, 1); // Вызываем меню
      }

      Удалить
  29. Большой труд!
    Владимир, здоровья, успехов, благословений!

    ОтветитьУдалить
  30. Спасибо Вам!
    Огромная работа, заслуживает благодарности! "Полностью" в ней разобрался )) Осталось 2 вопроса:
    1. Как гасить подсветку, при бездействии.
    2. Если мы находимся в меню, то при бездействии автоматически гасим подсветку и автоматически делаем выход из меню.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Библиотеку качали из блога или с гитхаба? Актуальная версия на гитхабе: https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu

      Недавно делал похожее меню для OLED дисплея, в нем как раз реализовал возврат при бездействии. Для этого нужно:

      1. Объявить переменную: unsigned long lastActionTime;
      2. В файле LiquidCrystal_I2C_Menu.cpp в функции опроса энкодера (getEncoderState) перед строкой return Result;
      добавить:
      if (Result != eNone) lastActionTime = millis();
      3. В функции showSubMenu в самом начале цикла do добавить выход, если последние действие было давно:
      do {
      if (millis() - lastActionTime > IDLE_TIMEOUT) {
      free(subMenu);
      return 0;
      }
      if (needRepaint) {
      ...

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

      Чтобы погасить подсветку нужно аналогичное условие if с меньшим интервалом и проверкой, что подсветка включена. Но лучше оформить это в виде процедуры, в которой будет и включение, и выключение подсветки, типа:

      void BacklightControl() { // Управление подсветкой
      if ((millis() - lastActionTime > BACKLIGHT_TIMEOUT) and (_backlightval == LCD_BACKLIGHT)){
      noBacklight(); // Выключаем, если еще не выключили
      }
      if ((millis() - lastActionTime < BACKLIGHT_TIMEOUT) and (_backlightval == LCD_NOBACKLIGHT)){
      backlight(); // Включаем, если еще не включили
      }
      }

      И добавить вызов этой функции во все циклы: в меню, в функции ввода и выбора значений...

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

      Удалить
    3. Спасибо. Выход из меню, по истечении времени, осуществить получилось, всё хорошо.
      Но с выключением подсветки проблема. Максимум чего добился, при бездействии - выход из меню с последующим выключением подсветки.
      if (millis() - lastActionTime > 15000) {
      free(subMenu);
      if (_backlightval == LCD_BACKLIGHT){
      noBacklight();
      }
      return 0;
      }
      При попытке использовать как функцию (как вы рекомендовали), появляются ошибки(то "'BacklightControl' was not declared in this scope", то "'lastActionTime' was not declared in this scope" - в разных вариациях). В общем просьба точнее пните меня пожалуйста, запутался что-то...

      Удалить
    4. Функция BacklightControl вызывает функцию класса LiquidCrystal_I2C_Menu, поэтому и сама должна принадлежать этому классу. Объявите ее в LiquidCrystal_I2C_Menu.cpp так:

      void LiquidCrystal_I2C_Menu::BacklightControl(){
      }

      а также добавьте ее в описание класса в файле LiquidCrystal_I2C_Menu.h, например, после строки void encoderIdle(); :
      void BacklightControl();

      Должно получиться

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

    ОтветитьУдалить
  32. Добрый день.
    Активно пользуюсь вашей библиотекой для UNO/Nano, сейчас есть проект на ESP32. Но запустить не получается. При попытке компиляции множество ошибок. Подскажите, это как то возможно поправить или лучше поискать другую библиотеку? По факту, для ЕСП32 не так то просто оказалось найти работающую.
    C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu-master\LiquidCrystal_I2C_Menu.cpp: In member function 'void LiquidCrystal_I2C_Menu::printMultiline(const char*)':

    C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu-master\LiquidCrystal_I2C_Menu.cpp:345:85: error: no matching function for call to 'min(uint8_t&, size_t)'

    for (uint8_t i = 0; i < min(_rows, (strlen(str) + lineLength - 1) / lineLength); i++) {

    ^

    In file included from c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\algorithm:62:0,

    from C:\Users\User\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\cores\esp32/Arduino.h:142,

    from C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu-master\LiquidCrystal_I2C_Menu.cpp:3:

    c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3451:5: note: candidate: template _Tp std::min(std::initializer_list<_Tp>, _Compare)

    min(initializer_list<_Tp> __l, _Compare __comp)

    ^

    c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3451:5: note: template argument deduction/substitution failed:

    C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu-master\LiquidCrystal_I2C_Menu.cpp:345:85: note: mismatched types 'std::initializer_list<_Tp>' and 'unsigned char'

    for (uint8_t i = 0; i < min(_rows, (strlen(str) + lineLength - 1) / lineLength); i++) {

    ^

    In file included from c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\algorithm:62:0,

    from C:\Users\User\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\cores\esp32/Arduino.h:142,

    from C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu-master\LiquidCrystal_I2C_Menu.cpp:3:

    c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3445:5: note: candidate: template _Tp std::min(std::initializer_list<_Tp>)

    min(initializer_list<_Tp> __l)

    ^

    c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3445:5: note: template argument deduction/substitution failed:

    C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu-master\LiquidCrystal_I2C_Menu.cpp:345:85: note: mismatched types 'std::initializer_list<_Tp>' and 'unsigned char'

    for (uint8_t i = 0; i < min(_rows, (strlen(str) + lineLength - 1) / lineLength); i++) {

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      А для ESP32 есть рабочая версия библиотеки liquidcrystal_i2c? Если такая найдется, то на нее можно будет перенести и функции для меню.

      Удалить
    2. Вот эта у меня нормально работает, правда без серьезного тестирования, при компиляции предупреждения идут о возможной несовместимости, но тестовые примеры отрабатывают.
      https://github.com/johnrickman/LiquidCrystal_I2C
      Есть еще несколько библиотек, авторы которых заявляют о том, что нормально будет работать с ЕСП32, но по факту - ошибки при компиляции. Например вот эта понравилась по функционалу, но ошибки выдает при компиляции.
      https://github.com/enjoyneering/LiquidCrystal_I2C

      Удалить
    3. Автор последней библиотеки есть на хабре и в ЖЖ, можно попробовать с ним пообщаться на предмет ошибок.
      https://elchupanibrei.livejournal.com/27443.html
      https://habr.com/ru/users/enjoyneering/comments/

      Удалить
    4. Если я чем то могу помочь - с удовольствием приму участие - железо есть, в коде могу разобраться, тесты - поиски багов без проблем.

      Удалить
    5. Посмотрю в выходные, получится ли перенести

      Удалить
    6. Попробуйте:
      https://drive.google.com/open?id=1iGXsAPYkyAecNJIt_kdYuJTw9N2LC1Sw

      Удалить
    7. Доброй ночи.
      Ошибки выдает, как я понимаю, там что то с типами переменных не нравится. Для более оперативной связи мои контакты:
      mable@mail.ru
      viber-whatsup 7 9i7 34 32595

      Compiling library "LiquidCrystal_I2C_Menu"

      C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu\LiquidCrystal_I2C_Menu.cpp: In member function 'void LiquidCrystal_I2C_Menu::printMultiline(const char*)':

      C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu\LiquidCrystal_I2C_Menu.cpp:387:85: error: no matching function for call to 'min(uint8_t&, size_t)'

      for (uint8_t i = 0; i < min(_rows, (strlen(str) + lineLength - 1) / lineLength); i++) {

      ^

      In file included from c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\algorithm:62:0,

      from C:\Users\User\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\cores\esp32/Arduino.h:142,

      from C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu\LiquidCrystal_I2C_Menu.cpp:7:

      c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3451:5: note: candidate: template _Tp std::min(std::initializer_list<_Tp>, _Compare)

      min(initializer_list<_Tp> __l, _Compare __comp)

      ^

      c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3451:5: note: template argument deduction/substitution failed:

      C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu\LiquidCrystal_I2C_Menu.cpp:387:85: note: mismatched types 'std::initializer_list<_Tp>' and 'unsigned char'

      for (uint8_t i = 0; i < min(_rows, (strlen(str) + lineLength - 1) / lineLength); i++) {

      ^

      In file included from c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\algorithm:62:0,

      from C:\Users\User\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\cores\esp32/Arduino.h:142,

      from C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu\LiquidCrystal_I2C_Menu.cpp:7:

      c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3445:5: note: candidate: template _Tp std::min(std::initializer_list<_Tp>)

      min(initializer_list<_Tp> __l)

      ^

      c:\users\user\appdata\local\arduino15\packages\esp32\tools\xtensa-esp32-elf-gcc\1.22.0-80-g6c4433a-5.2.0\xtensa-esp32-elf\include\c++\5.2.0\bits\stl_algo.h:3445:5: note: template argument deduction/substitution failed:

      C:\Users\User\Documents\Arduino\libraries\LiquidCrystal_I2C_Menu\LiquidCrystal_I2C_Menu.cpp:387:85: note: mismatched types 'std::initializer_list<_Tp>' and 'unsigned char'

      for (uint8_t i = 0; i < min(_rows, (strlen(str) + lineLength - 1) / lineLength); i++) {

      ^

      Удалить
  33. Нашел еще одну библиотеку которая нормально работает. Но в отличие от предыдущей - нет предупреждений при компиляции о возможной несовместимости с ESP32. Не знаю насколько это важно.
    https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

    ОтветитьУдалить
  34. Добрый день. Еще раз спасибо за библиотеку. Использую ее, немного доделал для себя. Я еще пользуюсь клавиатурой. Встал вопрос мне нужно ее использовать в одном проекте, но там Пин 2 занят.
    Можно Пины Энкодера использовать не такие как в примерах или Пин 2 обязателен. Он ведь связан с прерыванием.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Пины можно использовать любые. Там опрос энкодера без прерываний.

      Удалить
  35. Добрый день!
    Библиотека хорошая. Ещё раз Спасибо!
    Подскажите как сделать управление в меню 4 кнопками, или 3.

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

      Удалить
    2. Прочел все коменты, про кнопки ничего нет...

      Удалить
    3. Добрый день!
      Если еще актуально, то вот статья:
      https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html
      В ней есть ссылка на библиотеку, использующую кнопки вместо энкодера.

      Удалить
  36. Приветствую, Владимир!
    Первое знакомство с библиотекой меня немного разочаровало, т.к. на 32-битной Arduino Due она не работает (но я с удовольствием пользовался lcd.printAt).

    Сейчас вторая попытка освоиться, уже на Arduino Pro Micro (мини-версия Leonatdo). Но опять траблы (наверное, руки кривые))).

    Планируется слепить эдакий мульти-таймер, с помощью которого диспетчер на конечной остановке будет отправлять 5 маршрутов с определёнными (различными) интервалами.

    Структура меню создана, всё "ныряет" как положено, но как только дело доходит до функций, отличных от NULL, меня просто вываливает в Рут. Кроме того, часы реального времени, которые должны тикать на главном экране, также не выводятся.

    Должно быть:
    Строка 0 - Заголовок (Главное меню) // выполняется
    Строка 1 - Пункт меню "Маршруты" // выполняется
    Строка 2 - Пункт меню "Настройки" // выполняется
    Строка 3 - дата и время // НЕ ВЫВОДИТСЯ

    Такое впечатление, что внутри Loop вообще ничего не выполняется. Объясните, что я делаю неправильно? (код с комментами в следующих постах, в один пост не влезает)

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Я сейчас сам как раз допиливаю эту библиотеку. А именно делаю поддержку дисплеев с кириллицей. Вы кстати как выводите русский текст в меню?

      По поводу скетча. Только начал смотреть, еще не разобрался, но сразу обратил внимание вот на что:
      if (selectedMenuItem == mkRoot)
      - функция showMenu может вернуть ключ только конечного пункта меню, т.е. не имеющего дочерних элементов. mkRoot - "ГЛАВНОЕ МЕНЮ" таковым не является. При его выборе функция продолжает работать, отрисовывая следующий уровень меню. Указанное условие не будет выполнено.

      Удалить
    2. на примере track8:
      {mkTrack8, mkInterval8, "Интервал", _Track8}
      _Track8 - это не имя функции-обработчика, это переменная int _Track8 = 5;
      Поэтому нужно исправить меню на
      {mkTrack8, mkInterval8, "Интервал", Track8}

      Функция Track8 описана ниже меню, компилятор её "не видит", поэтому заругается при попытке компиляции. Перенесите все эти функции TrackX выше, чтобы они были между используемыми в них переменными _TrackX и описанием меню. Другой вариант - оставить всё как есть, но где-нибудь вверху скетча добавить прототипы функций-обработчиков.

      И еще совет по реализации. Если подменюшки всех маршрутов одинаковые то я бы описал их в отдельном меню. А при выборе конкретного маршрута показывал бы это подменю. Типа:

      uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1); // Вызываем меню
      if ((selectedMenuItem == mkTrack8) or (selectedMenuItem == mkTrack21)) { // or (selectedMenuItem == ...
      uint8_t X = lcd.showMenu(menu2, menu2Len, 1); // Вызываем подменю для маршрутов
      // На выходе имеем выбранный маршрут (selectedMenuItem) и выбранное для него действие (X)
      // Анализируем их и выполняем соответствующий код
      }

      Удалить
  37. #include "Wire.h"
    #include "LiquidCrystal_I2C_Menu.h"
    LiquidCrystal_I2C_Menu lcd(0x27, 20, 4);

    #include "EEPROM.h"

    // Подключаем модуль точного времени
    #include "DS3231.h"
    RTClib RTC;

    #define pinCLK 0
    #define pinDT 1
    #define pinSW 4

    // задаём начальные значения интервалов
    int _Track8 = 5;
    int _Track21 = 8;
    int _Track50 = 25;
    int _Track51 = 30;
    int _Track163 = 45;

    enum {mkBack, mkRoot, mkTracks, mkTrack8, mkStart8, mkPause8, mkReset8, mkInterval8,
    mkTrack21, mkStart21, mkReset21, mkPause21, mkInterval21,
    mkTrack50, mkStart50, mkReset50, mkPause50, mkInterval50,
    mkTrack51, mkStart51, mkReset51, mkPause51, mkInterval51,
    mkTrack163, mkStart163, mkReset163, mkPause163, mkInterval163,
    mkSettings};

    // Menu definition
    sMenuItem menu[] = {
    {mkBack, mkRoot, "ГЛАВНОЕ МЕНЮ", NULL},
    {mkRoot, mkTracks, "Маршруты", NULL},
    {mkRoot, mkSettings, "Настройки", NULL},


    // Список меню маршрутов
    {mkTracks, mkTrack8, "Маршрут 8", NULL},
    {mkTracks, mkTrack21, "Маршрут 21", NULL},
    {mkTracks, mkTrack50, "Маршрут 50", NULL},
    {mkTracks, mkTrack51, "Маршрут 51", NULL},
    {mkTracks, mkTrack163, "Маршрут 163", NULL},
    {mkTracks, mkBack, "Назад", NULL}, //

    // Обработка маршрута 8
    {mkTrack8, mkStart8, "Поехали", NULL},
    {mkTrack8, mkReset8, "Сброс", NULL},
    {mkTrack8, mkPause8, "Пауза", NULL},
    {mkTrack8, mkInterval8, "Интервал", _Track8}, // вызываем ввод интервала маршрута 8
    {mkTrack8, mkBack, "Назад", NULL},

    // Обработка маршрута 21
    {mkTrack21, mkStart21, "Поехали", NULL},
    {mkTrack21, mkReset21, "Сброс", NULL},
    {mkTrack21, mkPause21, "Пауза", NULL},
    {mkTrack21, mkInterval21, "Интервал", _Track21},
    {mkTrack21, mkBack, "Назад", NULL},

    // Обработка маршрута 50
    {mkTrack50, mkStart50, "Поехали", NULL},
    {mkTrack50, mkReset50, "Сброс", NULL},
    {mkTrack50, mkPause50, "Пауза", NULL},
    {mkTrack50, mkInterval50, "Интервал", _Track50},
    {mkTrack50, mkBack, "Назад", NULL},

    // Обработка маршрута 51
    {mkTrack51, mkStart51, "Поехали", NULL},
    {mkTrack51, mkReset51, "Сброс", NULL},
    {mkTrack51, mkPause51, "Пауза", NULL},
    {mkTrack51, mkInterval51, "Интервал", _Track51},
    {mkTrack51, mkBack, "Назад", NULL},

    // Обработка маршрута 163
    {mkTrack163, mkStart163, "Поехали", NULL},
    {mkTrack163, mkReset163, "Сброс", NULL},
    {mkTrack163, mkPause163, "Пауза", NULL},
    {mkTrack163, mkInterval163, "Интервал", _Track163},
    {mkTrack163, mkBack, "Назад", NULL}
    };

    // Determine the number of items in the menu
    uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem);

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

    ОтветитьУдалить
  38. void loop() {
    uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1); // Вызываем меню

    if (selectedMenuItem == mkRoot) { // Если мы находимся в руте менюхи, то
    DateTime now = RTC.now(); // инициализируем часики
    curTime(); // вызываем функцию вывода часов на дисплей и выводим время и дату в нижней строке (только для Рута)
    // однако время ни хрена не выводится, вообще пустая строка
    }

    if (selectedMenuItem == mkInterval8) { // Если выбран пункт настройки интевала маршрута 8, то
    Track8(); // вызываем функцию ввода значения интервала 8-го маршрута
    }
    // и ни хрена не происходит, идёт какая-то пауза в секунду и вываливает в Рут
    // для экономии места аналогичные вызовы не описываю

    } // конец лупа

    // Функция ввода интервала для маршрута 8
    void Track8() {
    _Track8 = lcd.inputVal("Track8", 1, 99, _Track8, 1);
    }
    // Функция ввода интервала для маршрута 21
    void Track21() {
    _Track21 = lcd.inputVal("Track21", 1, 99, _Track21, 1);
    }
    // Функция ввода интервала для маршрута 50
    void Track50() {
    _Track50 = lcd.inputVal("Track50", 1, 99, _Track50, 1);
    }
    // Функция ввода интервала для маршрута 51
    void Track51() {
    _Track51 = lcd.inputVal("Track51", 1, 99, _Track51, 1);
    }
    // Функция ввода интервала для маршрута 163
    void Track163() {
    _Track163 = lcd.inputVal("Track163", 1, 99, _Track163, 1);
    }



    void curTime () {
    DateTime now = RTC.now();
    if(now.hour() <=9) {
    lcd.printAt(0, 3, 0);
    lcd.printAt(1, 3, now.hour(), DEC);
    }
    else lcd.printAt(0, 0, now.hour(), DEC);
    lcd.print(':');
    if(now.minute() <=9) {
    lcd.printAt(3, 3, 0);
    lcd.print(now.minute(), DEC);
    }
    else lcd.printAt(3, 3, now.minute(), DEC);
    lcd.printAt(5, 3, ':');
    if(now.second() <=9) {
    lcd.printAt(6, 3, 0);
    lcd.print(now.second(), DEC);
    }
    else lcd.printAt(6, 3, now.second(), DEC);
    if(now.day() <= 9) {
    lcd.printAt(12, 3, "0");
    lcd.print(now.day(), DEC);
    }
    else lcd.printAt(12, 3, now.day(), DEC);
    lcd.print('/');
    if (now.month() <= 9) {
    lcd.printAt(15, 3, "0");
    lcd.print(now.month(), DEC);
    }
    else lcd.printAt(15, 3,now.month(), DEC);
    lcd.print('/');
    lcd.print(now.year() -2000, DEC);
    }

    ОтветитьУдалить
  39. Пытаюсь вывести часы реального времени в файле Menu_for_setting_params.ino

    Их необходимо вывести в главном экране. Вывод сформирован в отдельной функции. И мне необходимо вставить часы вместо строки
    lcd.printAt(0, 3, "Press btn for menu");

    И всё бы нормально, но выводится время в текущий момент и... висит. То есть при загрузке они однократно срабатывают, и не обновляются. Где копать?

    ОтветитьУдалить
    Ответы
    1. Висит потому, что, находясь в меню, никакой другой код из скетча не выполняется.

      А зачем выводить время именно в меню? Почему бы не изменить интерфейс следующим образом:
      1. Главный экран. На нём текущее время и предложение нажать кнопку для входа в меню.
      2. Меню. Переходим по пунктам, выполняем какие-либо действия. В итоге выходим из него и оказываемся снова в функции loop, т.е. на главном экране.
      Такой вариант не походит?

      Вы русский как выводите? Что-то добавляли в библиотеку?

      Удалить
    2. void loop() {
      uint8_t selectedMenuItem;
      // 1. Вывод времени на дисплей
      // 2. Опрос энкодера:
      if (lcd.getEncoderState() == eButton) {
      // 3. Если нажата кнопка энкодера, то показываем меню
      selectedMenuItem = lcd.showMenu(menu, menuLen, 1);
      // 4. Анализируем selectedMenuItem и выполняем соответствующий код
      }
      }

      Удалить