├── wiring └── LiquidCrystal_I2C_Menu wiring.png ├── examples ├── printMultiline │ └── printMultiline.ino ├── printf │ └── printf.ino ├── Dialog_example │ └── Dialog_example.ino ├── inputStrVal_2 │ └── inputStrVal_2.ino ├── selectVal_2 │ └── selectVal_2.ino ├── selectVal │ └── selectVal.ino ├── inputStrVal │ └── inputStrVal.ino ├── inputValBitwise │ └── inputValBitwise.ino ├── Encoder │ └── Encoder.ino ├── Input_double_and_long │ └── Input_double_and_long.ino ├── attachIdleFunc │ └── attachIdleFunc.ino ├── printMultiline_2 │ └── printMultiline_2.ino ├── Using_handlers │ └── Using_handlers.ino ├── inputVal │ └── inputVal.ino ├── inputVal_onChange │ └── inputVal_onChange.ino ├── showMenu │ └── showMenu.ino ├── Updating_menu_captions │ └── Updating_menu_captions.ino └── Menu_for_setting_params │ └── Menu_for_setting_params.ino ├── Changes.log ├── keywords.txt ├── README.md ├── LiquidCrystal_I2C_Menu.h └── LiquidCrystal_I2C_Menu.cpp /wiring/LiquidCrystal_I2C_Menu wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirTsibrov/LiquidCrystal_I2C_Menu/HEAD/wiring/LiquidCrystal_I2C_Menu wiring.png -------------------------------------------------------------------------------- /examples/printMultiline/printMultiline.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции printMultiline 2 | */ 3 | #include 4 | #include 5 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 6 | 7 | // Пины, к которым подключен энкодер 8 | #define pinCLK 2 9 | #define pinDT 3 10 | #define pinSW 4 11 | 12 | 13 | void setup() { 14 | lcd.begin(); 15 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 16 | } 17 | 18 | void loop() { 19 | lcd.printMultiline("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"); 20 | } 21 | -------------------------------------------------------------------------------- /examples/printf/printf.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функций вывода на дисплей: 2 | * printf(format, ...) - форматированный вывод 3 | * printAt(x, y, value, [param]) - вывод с указанной позиции 4 | * printfAt(x, y, format, ...) - форматированный вывод с указанной позиции 5 | * Описание спецификаторов форматированного вывода можно найти по ссылке: 6 | * https://www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html#gaa3b98c0d17b35642c0f3e4649092b9f1 7 | */ 8 | #include 9 | #include 10 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 11 | 12 | void setup() { 13 | char hello[] = "Hello, world!"; 14 | String s = "String example"; 15 | lcd.begin(); 16 | lcd.printf("millis=%lu", millis()); 17 | lcd.printAt(3, 1, hello); 18 | lcd.printAt(0, 2, s); 19 | lcd.printfAt(0, 3, "%s", s.c_str()); 20 | } 21 | 22 | void loop() { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /examples/Dialog_example/Dialog_example.ino: -------------------------------------------------------------------------------- 1 | // Пример использования меню для построения диалога 2 | #include 3 | #include 4 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 5 | 6 | // Пины, к которым подключен энкодер 7 | #define pinCLK 2 8 | #define pinDT 3 9 | #define pinSW 4 10 | 11 | void setup() { 12 | lcd.begin(); 13 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 14 | } 15 | 16 | void loop() { 17 | sMenuItem formatConfirmation[] = { // Опишем наше меню: 18 | {0, 1, "Format drive C?", NULL}, // Это будет заголовок 19 | {1, 2, "No", NULL}, // и два пункта для выбора 20 | {1, 3, "Yes", NULL} 21 | }; 22 | // Показываем меню и анализируем результат 23 | if (lcd.showMenu(formatConfirmation, 3, 1) == 3) 24 | lcd.print("Yes selected"); 25 | else 26 | lcd.print("No selected"); 27 | while (lcd.getEncoderState() == eNone); 28 | } 29 | -------------------------------------------------------------------------------- /examples/inputStrVal_2/inputStrVal_2.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции inputStrVal для маскированного ввода. Синтаксис: 2 | * inputStrVal(title, buffer, len, availSymbols), где 3 | * title - заголовок 4 | * buffer - указатель на массив char для вводимых символов 5 | * len - длина вводимого значения 6 | * availSymbols - массив символов, доступных для ввода и редактирования 7 | */ 8 | 9 | #include 10 | #include 11 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 12 | 13 | // Пины, к которым подключен энкодер 14 | #define pinCLK 2 15 | #define pinDT 3 16 | #define pinSW 4 17 | 18 | char ip[] = "192.168.001.001"; // Массив символов с начальным значением/маской 19 | 20 | void setup() { 21 | lcd.begin(); 22 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 23 | } 24 | 25 | void loop() { 26 | if (lcd.inputStrVal("Input IP", ip, 15, "0123456789")){ 27 | lcd.print("You entered:"); 28 | lcd.printAt(0, 1, ip); 29 | } 30 | else 31 | lcd.print("Input canceled"); 32 | while (lcd.getEncoderState() == eNone); 33 | } 34 | -------------------------------------------------------------------------------- /examples/selectVal_2/selectVal_2.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции selectVal для выбора значения. Синтаксис: 2 | * selectVal(title, list, count, show_selected, preselected), где 3 | * title - заголовок 4 | * list - массив значений для выбора 5 | * count - количество элементов в массиве 6 | * show_selected - флаг отображения указателя на выбранном элементе 7 | * preselected - индекс выбранного по умолчанию элемента 8 | * Функция возвращает индекс выбранного значения 9 | */ 10 | #include 11 | #include 12 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 13 | 14 | // Пины, к которым подключен энкодер 15 | #define pinCLK 2 16 | #define pinDT 3 17 | #define pinSW 4 18 | 19 | void setup() { 20 | lcd.begin(); 21 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 22 | } 23 | 24 | void loop() { 25 | int index; 26 | String list[] = {"Off", "On"}; 27 | index = lcd.selectVal("Turn backlight", list, 2, false); 28 | lcd.setBacklight(index); 29 | lcd.printf("Backlight turned %s", list[index].c_str()); 30 | while (lcd.getEncoderState() == eNone); 31 | } 32 | -------------------------------------------------------------------------------- /examples/selectVal/selectVal.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции selectVal для выбора значения. Синтаксис: 2 | * selectVal(title, list, count, show_selected, preselected), где 3 | * title - заголовок 4 | * list - массив значений для выбора 5 | * count - количество элементов в массиве 6 | * show_selected - флаг отображения указателя на выбранном элементе 7 | * preselected - индекс выбранного по умолчанию элемента 8 | * Функция возвращает индекс выбранного значения 9 | */ 10 | #include 11 | #include 12 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 13 | 14 | // Пины, к которым подключен энкодер 15 | #define pinCLK 2 16 | #define pinDT 3 17 | #define pinSW 4 18 | 19 | int index = 0; 20 | 21 | void setup() { 22 | lcd.begin(); 23 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 24 | } 25 | 26 | void loop() { 27 | String list[] = {"Europa+", "Record", "DFM", "Retro FM", "Energy"}; 28 | index = lcd.selectVal("Select station", list, 5, true, index); 29 | lcd.printf("%s selected", list[index].c_str()); 30 | while (lcd.getEncoderState() == eNone); 31 | } 32 | -------------------------------------------------------------------------------- /examples/inputStrVal/inputStrVal.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции inputStrVal. Синтаксис: 2 | * inputStrVal(title, buffer, len, availSymbols), где 3 | * title - заголовок 4 | * buffer - указатель на массив char для вводимых символов 5 | * len - длина вводимого значения 6 | * availSymbols - массив символов, доступных для ввода и редактирования 7 | */ 8 | 9 | #include 10 | #include 11 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 12 | 13 | // Пины, к которым подключен энкодер 14 | #define pinCLK 2 15 | #define pinDT 3 16 | #define pinSW 4 17 | 18 | char *buffer; // Буфер для ввода 19 | 20 | void setup() { 21 | lcd.begin(); 22 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 23 | buffer = (char*) malloc (16); // Выделим 16 байт (15 символов + конец строки) 24 | memset(buffer, '\0', 16); // Заполним их все символом конца строки 25 | } 26 | 27 | void loop() { 28 | if (lcd.inputStrVal("Input your name", buffer, 15, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")){ 29 | lcd.print("Hello,"); 30 | lcd.printAt(0, 1, buffer); 31 | } 32 | else 33 | lcd.print("Input canceled"); 34 | while (lcd.getEncoderState() == eNone); 35 | } 36 | -------------------------------------------------------------------------------- /examples/inputValBitwise/inputValBitwise.ino: -------------------------------------------------------------------------------- 1 | /* Использование функции inputValBitwise для поразрядного ввода чисел. Синтаксис: 2 | * inputValBitwise(title, value, precision, [scale = 0], [signed = 0]), где 3 | * title - заголовок 4 | * value - ссылка на переменную, в которой будет сохранено введенное значение 5 | * precision - общее количество цифр (до и после запятой) 6 | * scale - количество цифр после запятой, по умолчанию 0 7 | * signed - признак ввода со знаком. По умолчанию 0 - беззнаковый ввод 8 | */ 9 | #include 10 | #include 11 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 12 | 13 | // Пины, к которым подключен энкодер 14 | #define pinCLK 2 15 | #define pinDT 3 16 | #define pinSW 4 17 | 18 | double val = 0; 19 | 20 | void setup() { 21 | lcd.begin(); 22 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 23 | } 24 | 25 | void loop() { 26 | // Ввод 5-значного числа со знаком, 2 цифры после запятой: 27 | if (lcd.inputValBitwise("Input value", val, 5, 2, 1)) { 28 | lcd.print("You entered: "); 29 | lcd.print(val); 30 | } 31 | else 32 | lcd.print("Input canceled"); 33 | while (lcd.getEncoderState() == eNone); 34 | } 35 | -------------------------------------------------------------------------------- /Changes.log: -------------------------------------------------------------------------------- 1 | 22.11.2020 - добавлен файл Changes.log для документирования изменений. 2 | - добавлен необязательный параметр onChangeFunc в функции inputVal, inputValAt. В данном параметре передаётся функция, определенная в основной программе, которая будет вызываться при изменении значения внутри inputVal или inputValAt. 3 | - добавлен пример inputVal_onChange.ino для демонстации использования нового параметра onChangeFunc. 4 | - обновлен синтаксис вызова функций inputVal и inputValAt в примере inputVal.ino 5 | 6 | 29.09.2020 - добавлена возможность выхода из функций библиотеки при бездействии пользователя. Чтобы задействовать новую возможность, необходимо раскомментировать параметр INACTIVITY_TIMEOUT в файле LiquidCrystal_I2C_Menu.h и при необходимости изменить его значение (время бездействия в миллисекундах). 7 | 8 | 09.07.2020 - актуализация библиотеки и примеров. Множество изменений, среди которых создание версии библиотеки для управления кнопками - LiquidCrystal_I2C_Menu_Btns, поддержка кириллицы, выполнение функций основной программы "при бездействии". Полное описание библиотеки в статье https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html 9 | 10 | 08.04.2019 - создан репозиторий на github. Новое имя библиотеки - LiquidCrystal_I2C_Menu (взамен старого LiquidCrystal_I2C_Ext) -------------------------------------------------------------------------------- /examples/Encoder/Encoder.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции getEncoderState для работы с энкодером. 2 | * Функция вовращает одно из следующих значений: 3 | * eNone, eLeft, eRight, eButton. 4 | * Для привязки используемых энкодером пинов вызывается функция attachEncoder. 5 | */ 6 | #include 7 | #include 8 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 9 | 10 | // Пины, к которым подключен энкодер 11 | #define pinCLK 2 12 | #define pinDT 3 13 | #define pinSW 4 14 | 15 | int x = 0; 16 | 17 | void setup() { 18 | lcd.begin(); 19 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 20 | lcd.printfAt(0, 0, "%d ", x); 21 | } 22 | 23 | void loop() { 24 | // Опрашиваем энкодер 25 | eEncoderState EncoderState = lcd.getEncoderState(); 26 | switch (EncoderState) { 27 | case eLeft: // При вращении влево уменьшаем значение переменной 28 | x--; 29 | break; 30 | case eRight: // При вращении вправо увеличиваем значение переменной 31 | x++; 32 | break; 33 | case eButton: // При нажатии кнопки энкодера обнуляем значение переменной 34 | x = 0; 35 | break; 36 | case eNone: // Энкодер не вращается, кнопка не нажата. Выходим из функции 37 | return; 38 | } 39 | lcd.printfAt(0, 0, "%d ", x); // Покажем новое значение x 40 | } 41 | -------------------------------------------------------------------------------- /examples/Input_double_and_long/Input_double_and_long.ino: -------------------------------------------------------------------------------- 1 | /* Несколько примеров использования функций ввода inputVal и 2 | * inputValBitwise для ввода чисел 3 | */ 4 | 5 | #include 6 | #include 7 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 8 | 9 | // Пины, к которым подключен энкодер 10 | #define pinCLK 2 11 | #define pinDT 3 12 | #define pinSW 4 13 | 14 | void setup() { 15 | lcd.begin(); 16 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 17 | } 18 | 19 | void loop() { 20 | double x = 0; 21 | long y = 0; 22 | 23 | x = lcd.inputVal("Input value", 0.0, 10.0, x, 0.25); // Ввод double от 0 до 10 с шагом 0.25 24 | x = lcd.inputVal("Input value", 0, 10, x, 0.25); // Аналогично предыдущему вызову, но тип переменных задан явно - 25 | lcd.inputValBitwise("Input value", x, 4, 2, 0); // Беззнаковый ввод в формате XX.XX 26 | lcd.inputValBitwise("Input value", x, 4, 2, 1); // Ввод числа со знаком в формате [-]XX.XX 27 | 28 | y = lcd.inputVal ("Input value", 0, 10000, y, 100); // Ввод значения long. Тип переменной указан явно, чотбы избежать ошибок при компиляции 29 | lcd.inputValBitwise("Input value", y, 4); // Беззнаковый ввод целого числа длиной до 4 цифр 30 | lcd.inputValBitwise("Input value", y, 4, 0, 1); // Ввод целого числа длиной до 4 цифр 31 | } 32 | -------------------------------------------------------------------------------- /examples/attachIdleFunc/attachIdleFunc.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции attachIdleFunc для привязки функции, 2 | * выполняемой при бездействии 3 | */ 4 | 5 | #include 6 | #include 7 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 8 | 9 | // Пины, к которым подключен энкодер 10 | #define pinCLK 2 11 | #define pinDT 3 12 | #define pinSW 4 13 | 14 | unsigned long tm = 0; 15 | bool ledState = false; 16 | 17 | // Данная функции будет вызываться из библиотеки при бездействии 18 | void myIdleFunc() { 19 | if (millis() - tm >= 500) { 20 | // Включаем и выключаем встроенный светодиод на Ардуино 21 | tm = millis(); 22 | ledState = !ledState; 23 | digitalWrite(LED_BUILTIN, ledState); 24 | } 25 | } 26 | 27 | void setup() { 28 | lcd.begin(); 29 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 30 | lcd.attachIdleFunc(myIdleFunc); 31 | pinMode(LED_BUILTIN, OUTPUT); 32 | lcd.print("Press the button"); 33 | } 34 | 35 | int x = 0; 36 | 37 | void loop() { 38 | myIdleFunc(); 39 | if (lcd.getEncoderState() == eButton) { 40 | // Для проверки вызовем любую функцию библиотеки, 41 | // которая ожидает действий от пользователя: 42 | x = lcd.inputVal("Input some val", 0, 100, x); 43 | lcd.printMultiline("Some text here"); 44 | lcd.clear(); 45 | lcd.print("Press the button"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/printMultiline_2/printMultiline_2.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции printMultiline, а также 2 | * способы размещения длинных строк в памяти программ 3 | */ 4 | #include 5 | #include 6 | #include 7 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 8 | 9 | // Пины, к которым подключен энкодер 10 | #define pinCLK 2 11 | #define pinDT 3 12 | #define pinSW 4 13 | 14 | // Объявим две строки в памяти программ. Так они не будут занимать оперативную память 15 | const char text_1[] PROGMEM = "Using PROGMEM example"; 16 | const char text_2[] PROGMEM = "This text is stored in FLASH"; 17 | 18 | const char* const text[] PROGMEM = {text_1, text_2}; 19 | 20 | void setup() { 21 | lcd.begin(); 22 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 23 | } 24 | 25 | void loop() { 26 | char *buffer; 27 | buffer = (char*) malloc(30); // Буфер для временного хранения строки 28 | strcpy_P(buffer, (char*)pgm_read_word(&(text[0]))); // Копируем строку в буфер 29 | lcd.printMultiline(buffer); // Выводим содержимое буфера на экран 30 | strcpy_P(buffer, (char*)pgm_read_word(&(text[1]))); // Аналогично со второй строкой 31 | lcd.printMultiline(buffer); 32 | free(buffer); // Освобождаем буфер 33 | 34 | // Другой пример хранения строк в памяти программ - использование макроса F(). 35 | lcd.printMultiline(F("Using F() macro example. Press button to continue.")); 36 | } 37 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ################################################ 2 | # Syntax Coloring Map For LiquidCrystal_I2C_Menu 3 | ################################################ 4 | 5 | ################################################ 6 | # Datatypes (KEYWORD1) 7 | ################################################ 8 | LiquidCrystal_I2C_Menu KEYWORD1 9 | sMenuItem KEYWORD1 10 | 11 | ################################################ 12 | # Methods and Functions (KEYWORD2) 13 | ################################################ 14 | init KEYWORD2 15 | begin KEYWORD2 16 | clear KEYWORD2 17 | home KEYWORD2 18 | noDisplay KEYWORD2 19 | display KEYWORD2 20 | noBlink KEYWORD2 21 | blink KEYWORD2 22 | noCursor KEYWORD2 23 | cursor KEYWORD2 24 | scrollDisplayLeft KEYWORD2 25 | scrollDisplayRight KEYWORD2 26 | leftToRight KEYWORD2 27 | rightToLeft KEYWORD2 28 | shiftIncrement KEYWORD2 29 | shiftDecrement KEYWORD2 30 | noBacklight KEYWORD2 31 | backlight KEYWORD2 32 | autoscroll KEYWORD2 33 | noAutoscroll KEYWORD2 34 | createChar KEYWORD2 35 | setCursor KEYWORD2 36 | print KEYWORD2 37 | blink_on KEYWORD2 38 | blink_off KEYWORD2 39 | cursor_on KEYWORD2 40 | cursor_off KEYWORD2 41 | setBacklight KEYWORD2 42 | load_custom_character KEYWORD2 43 | printstr KEYWORD2 44 | printAt KEYWORD2 45 | printf KEYWORD2 46 | printfAt KEYWORD2 47 | attachEncoder KEYWORD2 48 | getEncoderState KEYWORD2 49 | printMultiline KEYWORD2 50 | inputVal KEYWORD2 51 | inputValAt KEYWORD2 52 | inputValBitwise KEYWORD2 53 | inputStrVal KEYWORD2 54 | selectVal KEYWORD2 55 | showMenu KEYWORD2 56 | attachIdleFunc KEYWORD2 57 | getSelectedMenuItem KEYWORD2 58 | 59 | ################################################ 60 | # Constants (LITERAL1) 61 | ################################################ 62 | -------------------------------------------------------------------------------- /examples/Using_handlers/Using_handlers.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования обработчиков меню. 2 | * Использование функций-обаботчиков позволяет выполнять действия 3 | * при выборе пунктов меню, не покидая само меню 4 | */ 5 | #include 6 | #include 7 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 8 | 9 | // Пины, к которым подключен энкодер 10 | #define pinCLK 2 11 | #define pinDT 3 12 | #define pinSW 4 13 | 14 | int brightness = 50; 15 | int _delay = 10; 16 | 17 | // Обработчики пунктов меню SetBrightness и SetDelay 18 | // Используются для ввода значений brightness и _delay 19 | void SetBrightness() { 20 | brightness = lcd.inputVal("Input brightness(%)", 0, 100, brightness, 5); 21 | } 22 | 23 | void SetDelay() { 24 | _delay = lcd.inputVal("Input delay(ms)", 0, 20, _delay); 25 | } 26 | 27 | // Объявим перечисление, используемое в качестве ключа пунктов меню 28 | enum {mkBack, mkRoot, mkOptions, mkSetBrightness, mkSetDelay}; 29 | 30 | // Описание меню 31 | // структура пункта меню: {ParentKey, Key, Caption, [Handler]} 32 | sMenuItem menu[] = { 33 | {mkBack, mkRoot, "Main menu", NULL}, 34 | {mkRoot, mkOptions, "Options", NULL}, 35 | {mkOptions, mkSetBrightness, "SetBrightness", SetBrightness}, 36 | {mkOptions, mkSetDelay, "SetDelay", SetDelay}, 37 | {mkOptions, mkBack, "Back", NULL}, 38 | {mkRoot, mkBack, "Back", NULL} 39 | }; 40 | 41 | uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem); 42 | 43 | void setup() { 44 | lcd.begin(); 45 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 46 | } 47 | 48 | void loop() { 49 | uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1); // Вызываем меню 50 | /* Реакция на выбор пунктов меню SetBrightness и SetDelay реализована 51 | * в функциях-обработчиках. 52 | * При необходимости здесь может располагаться анализ значения selectedMenuItem 53 | * для пунктов, не имеющих обработчиков: 54 | if (selectedMenuItem == ...) {...} 55 | */ 56 | } 57 | -------------------------------------------------------------------------------- /examples/inputVal/inputVal.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функций inputVal и inputValAt для ввода значений. Синтаксис: 2 | * inputVal(title, min, max, default, [step = 1], [*onChangeFunc = NULL]), где 3 | * title - заголовок 4 | * min и max - минимальное и максимальное значения для задания диапазона ввода 5 | * default - начальное значение 6 | * step - шаг приращения, по умолчанию = 1 7 | * *onChangeFunc - указатель на void функцию, которая будет вызываться при изменении значения 8 | * 9 | * inputValAt(x, y, min, max, default, step = 1, [*onChangeFunc = NULL]), где 10 | * x и y - позиция на дисплее для ввода значения 11 | * остальные параметры аналогичны описанным ранее для функции inputVal 12 | * 13 | * Функция inputVal очищает дисплей перед вводом. inputValAt не очищает дисплей 14 | */ 15 | 16 | #include 17 | #include 18 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 19 | 20 | // Пины, к которым подключен энкодер 21 | #define pinCLK 2 22 | #define pinDT 3 23 | #define pinSW 4 24 | 25 | void setup() { 26 | lcd.begin(); 27 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 28 | } 29 | 30 | void loop() { 31 | // Для примера запросим длину массива 32 | uint8_t len = lcd.inputVal("Input array len", 5, 10, 8); 33 | uint8_t A[len]; 34 | uint8_t t; 35 | 36 | // Затем элементы массива 37 | for (uint8_t i = 0; i < len; i++) { 38 | lcd.printfAt(0, 0, "Input A[%d]: ", i); // Приглашение для ввода 39 | A[i] = lcd.inputValAt(12, 0, 0, 9, 5); // Ввод значения 40 | } 41 | 42 | // Отсортируем массив 43 | for (uint8_t i = 0; i < len - 1; i++) { 44 | for (uint8_t j = i + 1; j < len; j++) { 45 | if(A[i] > A[j]){ 46 | t = A[i]; 47 | A[i] = A[j]; 48 | A[j] = t; 49 | } 50 | } 51 | } 52 | 53 | // И выведем на дисплей 54 | lcd.clear(); 55 | lcd.print("Sorted array:"); 56 | lcd.setCursor(0, 1); 57 | for (uint8_t i = 0; i < len; i++) 58 | lcd.printf("%d ", A[i]); 59 | 60 | while (lcd.getEncoderState() == eNone); 61 | } 62 | -------------------------------------------------------------------------------- /examples/inputVal_onChange/inputVal_onChange.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции onChange при вызове inputVal. Синтаксис: 2 | * inputVal(title, min, max, default, [step = 1], [*onChangeFunc = NULL]), где 3 | * title - заголовок 4 | * min и max - минимальное и максимальное значения для задания диапазона ввода 5 | * default - начальное значение 6 | * step - шаг приращения, по умолчанию = 1 7 | * *onChangeFunc - указатель на void функцию, которая будет вызываться при изменении значения 8 | */ 9 | 10 | #include 11 | #include 12 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 13 | 14 | // Пины, к которым подключен энкодер 15 | #define pinCLK 2 16 | #define pinDT 3 17 | #define pinSW 4 18 | 19 | int v = 0; 20 | long l = 0; 21 | 22 | void setup() { 23 | lcd.begin(); 24 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 25 | } 26 | 27 | // Функция, которая будет вызываться при изменении переменной v 28 | void volumeOnChange(int newValue) { 29 | // Тип переменной newValue должен совпадать с типом переменной v - int 30 | // Функция может использоваться для применения нового значения v непосредственно при редактировании, 31 | // а не при завершении редактирования. 32 | // Кроме того можно добавить свои эффекты на экран редактирования. Например, шкалу: 33 | for (uint8_t i = 0; i < 15; i++) { 34 | if (i < newValue) 35 | lcd.printAt(3 + i, 1, "\xFF"); 36 | else 37 | lcd.printAt(3 + i, 1, "\xDB"); 38 | } 39 | } 40 | 41 | // Функция, которая будет вызываться при изменении переменной l 42 | void longOnChange(long newValue) { 43 | // Тип переменной newValue должен совпадать с типом переменной l - long 44 | // Пример вывода шкалы для переменной, диапазон которой не совпадает с размером шкалы. 45 | // Решение - привести newValue к нужному диапазону функцией map 46 | long pos = map(newValue, 0, 1000, 0, 15); // в шкале 15 делений 47 | for (uint8_t i = 0; i < 15; i++) { 48 | if (i < pos) 49 | lcd.printAt(4 + i, 1, "\xFF"); 50 | else 51 | lcd.printAt(4 + i, 1, "\xDB"); 52 | } 53 | } 54 | 55 | void loop() { 56 | v = lcd.inputVal("Input volume", 0, 15, v, 1, volumeOnChange); 57 | l = lcd.inputVal ("Input value", 0, 1000, l, 10, longOnChange); 58 | } 59 | -------------------------------------------------------------------------------- /examples/showMenu/showMenu.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования функции showMenu для отображения меню. Синтаксис: 2 | * showMenu(menu, menuLen, showTitle), где 3 | * menu - массив пунктов меню. Тип элементов - sMenuItem 4 | * menuLen - количество элементов в меню 5 | * showTitle - флаг отображения заголовка в меню (название родительского пункта) 6 | * Функция возвращает ключ выбранного пункта меню 7 | */ 8 | #include 9 | #include 10 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 11 | 12 | // Пины, к которым подключен энкодер 13 | #define pinCLK 2 14 | #define pinDT 3 15 | #define pinSW 4 16 | 17 | // Объявим перечисление, используемое в качестве ключа пунктов меню 18 | enum {mkBack, mkRoot, mkRun, mkOptions, mkMode, mkSpeed, mkLog, mkSelftest, mkHelp, mkFAQ, mkIndex, mkAbout, mkLongCaption}; 19 | 20 | // Описание меню 21 | // структура пункта меню: {ParentKey, Key, Caption, [Handler]} 22 | sMenuItem menu[] = { 23 | {mkBack, mkRoot, "Menu demo"}, 24 | {mkRoot, mkRun, "Run"}, 25 | {mkRoot, mkOptions, "Options"}, 26 | {mkOptions, mkMode, "Mode"}, 27 | {mkOptions, mkSpeed, "Speed"}, 28 | {mkOptions, mkLog, "Print log"}, 29 | {mkOptions, mkSelftest, "Selftest"}, 30 | {mkOptions, mkBack, "Back"}, 31 | {mkRoot, mkHelp, "Help"}, 32 | {mkHelp, mkFAQ, "FAQ"}, 33 | {mkHelp, mkIndex, "Index"}, 34 | {mkHelp, mkAbout, "About"}, 35 | {mkHelp, mkBack, "Back"}, 36 | {mkRoot, mkLongCaption, "Long caption scrolling example"}, 37 | {mkRoot, mkBack, "Exit menu"} 38 | }; 39 | 40 | uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem); 41 | 42 | void setup() { 43 | lcd.begin(); 44 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 45 | } 46 | 47 | void loop() { 48 | // Показываем меню 49 | uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1); 50 | // И выполняем действия в соответствии с выбранным пунктом 51 | if (selectedMenuItem == mkRun) 52 | lcd.print("Run selected"); 53 | else if (selectedMenuItem == mkMode) 54 | lcd.print("Mode selected"); 55 | else if (selectedMenuItem == mkSpeed) 56 | lcd.print("Speed selected"); 57 | else if (selectedMenuItem == mkLog) 58 | lcd.print("Print log selected"); 59 | else if (selectedMenuItem == mkSelftest) 60 | lcd.print("Selftest selected"); 61 | else if (selectedMenuItem == mkFAQ) 62 | lcd.print("FAQ selected"); 63 | else if (selectedMenuItem == mkIndex) 64 | lcd.print("Index selected"); 65 | else if (selectedMenuItem == mkAbout) 66 | lcd.print("About selected"); 67 | else if (selectedMenuItem == mkLongCaption) 68 | lcd.print("Scrolling selected"); 69 | else if (selectedMenuItem == mkBack) 70 | lcd.print("Exit selected"); 71 | while (lcd.getEncoderState() == eNone); 72 | } 73 | -------------------------------------------------------------------------------- /examples/Updating_menu_captions/Updating_menu_captions.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования меню для ввода параметров 2 | * и отображения их текущих значений в пунктах меню. 3 | * Обработка выбранных пунктов осуществляется при возврате из меню. 4 | */ 5 | #include 6 | #include 7 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 8 | 9 | // Пины, к которым подключен энкодер 10 | #define pinCLK 2 11 | #define pinDT 3 12 | #define pinSW 4 13 | 14 | int brightness = 50; 15 | int _delay = 10; 16 | 17 | // Объявим перечисление, используемое в качестве ключа пунктов меню 18 | enum {mkBack, mkRoot, mkSetBrightness, mkSetDelay}; 19 | 20 | // Описание меню 21 | // структура пункта меню: {ParentKey, Key, Caption, [Handler]} 22 | sMenuItem menu[] = { 23 | {mkBack, mkRoot, "Options"}, 24 | {mkRoot, mkSetBrightness, NULL}, // Названия этих пунктов 25 | {mkRoot, mkSetDelay, NULL} // сгенерируем позже 26 | }; 27 | 28 | uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem); 29 | 30 | // Функция поиска индекса пункта по его ключу 31 | int getItemIndexByKey(uint8_t key) { 32 | for (uint8_t i = 0; i < menuLen; i++) 33 | if (menu[i].key == key) 34 | return i; 35 | return -1; 36 | } 37 | 38 | // Функция формирования названия пункта меню, содержащего значение параметра 39 | void updateCaption(uint8_t key, char format[], int value){ 40 | // key - ключ пункта меню, для которого обновляется навание 41 | // format - шаблон названия со значением 42 | // value - значение, добавляемое в название 43 | uint8_t index = getItemIndexByKey(key); 44 | char* buf = (char*) malloc(40); 45 | sprintf(buf, format, value); 46 | menu[index].caption = (char*) realloc(menu[index].caption, strlen(buf)+1); 47 | strcpy(menu[index].caption, buf); 48 | free(buf); 49 | } 50 | 51 | void setup() { 52 | lcd.begin(); 53 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 54 | // Формируем названия пунктов меню 55 | updateCaption(mkSetBrightness, "Brightness (%d%%)", brightness); 56 | updateCaption(mkSetDelay, "Delay (%dms)", _delay); 57 | } 58 | 59 | void loop() { 60 | // Показываем меню 61 | uint8_t selectedMenuItem = lcd.showMenu(menu, menuLen, 1); 62 | // и анализируем выбранный элемент 63 | if (selectedMenuItem == mkSetBrightness) { 64 | // Выбран пункт Brightness. Запрашиваем новое значение 65 | brightness = lcd.inputVal("Input brightness(%)", 0, 100, brightness, 5); 66 | // и обновляем название пункта 67 | updateCaption(mkSetBrightness, "Brightness (%d%%)", brightness); 68 | } 69 | else if (selectedMenuItem == mkSetDelay) { 70 | // Выбран пункт Delay. Запрашиваем новое значение 71 | _delay = lcd.inputVal("Input delay(ms)", 0, 20, _delay); 72 | // и обновляем название пункта 73 | updateCaption(mkSetDelay, "Delay (%dms)", _delay); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/Menu_for_setting_params/Menu_for_setting_params.ino: -------------------------------------------------------------------------------- 1 | /* Пример использования меню для ввода параметров 2 | * с отображением их текущих значений в пунктах меню 3 | * и сохранением в EEPROM. 4 | * Обработка выбранных пунктов осуществляется в обработчиках. 5 | */ 6 | 7 | #include 8 | #include 9 | LiquidCrystal_I2C_Menu lcd(0x27, 20, 4); 10 | #include 11 | 12 | // Пины, к которым подключен энкодер 13 | #define pinCLK 2 14 | #define pinDT 3 15 | #define pinSW 4 16 | 17 | uint8_t brightness; // Параметры, которые мы будем именять 18 | uint8_t _delay; 19 | 20 | // Объявим перечисление, используемое в качестве ключа пунктов меню 21 | enum {mkBack, mkRoot, mkSetBrightness, mkSetDelay, mkDefaults}; 22 | 23 | // Прототипы обработчиков пунктов меню: 24 | void SetBrightness(); 25 | void SetDelay(); 26 | void SetDefaults(); 27 | 28 | // Описание меню 29 | // структура пункта меню: {ParentKey, Key, Caption, [Handler]} 30 | sMenuItem menu[] = { 31 | // {ParentKey, Key, Caption, [Handler]} - структура пункта меню 32 | {mkBack, mkRoot, "Options", NULL}, 33 | {mkRoot, mkSetBrightness, NULL, SetBrightness}, // Названия этих пунктов 34 | {mkRoot, mkSetDelay, NULL, SetDelay}, // будут сгенерированы позже 35 | {mkRoot, mkDefaults, "Defaults", SetDefaults}, 36 | {mkRoot, mkBack, "Back", NULL} 37 | }; 38 | 39 | uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem); 40 | 41 | // Функция поиска индекса пункта по его ключу 42 | int getItemIndexByKey(uint8_t key){ 43 | for (uint8_t i = 0; i < menuLen; i++) 44 | if (menu[i].key == key) 45 | return i; 46 | return -1; 47 | } 48 | 49 | // Функция формирования названия пункта меню, содержащего значение параметра 50 | void updateCaption(uint8_t key, char format[], int value){ 51 | // key - ключ пункта меню, для которого обновляется навание 52 | // format - шаблон названия со значением 53 | // value - значение, добавляемое в название 54 | uint8_t index = getItemIndexByKey(key); 55 | char* buf = (char*) malloc(40); 56 | sprintf(buf, format, value); 57 | menu[index].caption = (char*) realloc(menu[index].caption, strlen(buf)+1); 58 | strcpy(menu[index].caption, buf); 59 | free(buf); 60 | } 61 | 62 | // Обработчик для пункта меню Brightness 63 | void SetBrightness(){ 64 | // Запрашиваем новое значение 65 | brightness = lcd.inputVal("Input brightness(%)", 0, 100, brightness, 5); 66 | // Сохраняем его в EEPROM 67 | EEPROM.update(0, brightness); 68 | // Обновляем название пункта меню 69 | updateCaption(mkSetBrightness, "Brightness (%d%%)", brightness); 70 | // Далее может распологаться код - реакция на изменение значения Brightness 71 | } 72 | 73 | // Обработчик для пункта меню Delay 74 | void SetDelay(){ 75 | // Запрашиваем новое значение 76 | _delay = lcd.inputVal("Input delay(ms)", 0, 20, _delay); 77 | // Сохраняем его в EEPROM 78 | EEPROM.update(1, _delay); 79 | // Обновляем название пункта меню 80 | updateCaption(mkSetDelay, "Delay (%dms)", _delay); 81 | // Далее может распологаться код - реакция на изменение значения Delay 82 | } 83 | 84 | // Обработчик для пункта меню Defaults 85 | void SetDefaults(){ 86 | brightness = 50; 87 | _delay = 10; 88 | EEPROM.update(0, brightness); 89 | EEPROM.update(1, _delay); 90 | updateCaption(mkSetBrightness, "Brightness (%d%%)", brightness); 91 | updateCaption(mkSetDelay, "Delay (%dms)", _delay); 92 | // Далее может распологаться код - реакция на изменение значений Delay и Brightness 93 | } 94 | 95 | // Перерисовка информации на экране 96 | void LCDRepaint(){ 97 | lcd.clear(); 98 | lcd.printfAt(0, 0, "Brightness (%d%%)", brightness); 99 | lcd.printfAt(0, 1, "Delay (%dms)", _delay); 100 | lcd.printAt(0, 3, "Press enter for menu"); 101 | } 102 | 103 | void setup() { 104 | lcd.begin(); 105 | lcd.attachEncoder(pinDT, pinCLK, pinSW); 106 | 107 | // Считываем значения brightness и _delay из EEPROM 108 | brightness = EEPROM.read(0); 109 | if (brightness > 100) { 110 | brightness = 50; 111 | EEPROM.write(0, brightness); 112 | } 113 | _delay = EEPROM.read(1); 114 | if(_delay > 20) { 115 | _delay = 10; 116 | EEPROM.write(1, _delay); 117 | } 118 | 119 | // Формируем названия пунктов меню 120 | updateCaption(mkSetBrightness, "Brightness (%d%%)", brightness); 121 | updateCaption(mkSetDelay, "Delay (%dms)", _delay); 122 | 123 | LCDRepaint(); 124 | } 125 | 126 | void loop() { 127 | // Для изменения параметров необходимо нажать кнопку 128 | if (lcd.getEncoderState() == eButton) { // При нажатии 129 | lcd.showMenu(menu, menuLen, 1); // показываем меню 130 | LCDRepaint(); // после чего обновляем информацию на дисплее 131 | } 132 | // Далее может располагаться основной функционал программы 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## LiquidCrystal_I2C_Menu 2 | **LiquidCrystal_I2C_Menu** - это библиотека для создания пользовательского интерфейса на Ардуино и текстовом ЖК дисплее с I2C интерфейсом. Библиотека предоставляет функционал для форматированного вывода, ввода и выбора значений, организации меню. Управление осуществляется с помощью энкодера вращения с кнопкой. 3 | 4 | ## Подключение 5 | [![LiquidCrystal_I2C_Menu Подключение](https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu/raw/master/wiring/LiquidCrystal_I2C_Menu%20wiring.png "LiquidCrystal_I2C_Menu Подключение")](https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu/raw/master/wiring/LiquidCrystal_I2C_Menu%20wiring.png "LiquidCrystal_I2C_Menu Подключение") 6 | 7 | ## Управление кнопками 8 | Вместо энкодера вращения могут быть использованы кнопки. Для этого существует другая версия данной библиотеки - [LiquidCrystal_I2C_Menu_Btns](https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu_Btns "LiquidCrystal_I2C_Menu_Btns") 9 | 10 | ## Функции 11 | Наряду со стандартными функциями, унаследованными из библиотеки LiquidCrystal_I2C, в данной библиотеке реализованы следующие: 12 | - **printAt(x, y, text)** – вывод текста на дисплей с указанной позиции. 13 | - **printf(format, …)** – форматированный вывод текста. Действуют те же правила, что и в других функциях форматированного вывода, например, sprintf. 14 | - **printfAt(x, y, format, …)** – форматированный вывод с указанной позиции. 15 | - **attachEncoder(pinA, pinB, pinBtn)** – сообщает библиотеке, к каким выводам Ардуино подключен энкодер. 16 | - **getEncoderState()** – опрос состояния энкодера. Возвращает значение типа eEncoderState (перечисляемый тип, описан в библиотеке как {eNone, eLeft, eRight, eButton}). 17 | - **printMultiline(text)** – вывод длинного текста с возможностью вертикальной прокрутки. Возврат из функции осуществляется при нажатии кнопки энкодера. 18 | - **inputVal(title, min, max, default, [step], [onChangeFunc])** – ввод числового значения. title – заголовок; параметры min и max задают диапазон, в котором может изменяться значение; default – начальное значение; step – величина приращения, по умолчанию равна 1; необязательный параметр onChangeFunc - указатель на функцию, которая должна вызываться при изменении значения. 19 | - **inputValAt(x, y, min, max, default, [step], [onChangeFunc])** – аналогична функции inputVal, но в отличие от нее не очищает дисплей при вызове и ввод значения осуществляется с указанной позиции. 20 | - **inputValBitwise(title, value, precision, [scale], [signed])** – позволяет вводить значения путем редактирования отдельных разрядов числа. Параметр title определяет заголовок; value – ссылка на переменную, в которую будет помещен результат ввода; precision – общее количество разрядов в числе; scale – количество разрядов после запятой, значение по умолчанию 0; signed – разрешает (при значении true) или запрещает (при значении false – по умолчанию) ввод отрицательных чисел. Функция возвращает true, если пользователь подтвердил ввод, false, если отказался. 21 | - **inputStrVal(title, buffer, length, available_symbols)** – аналогично функции inputValBitwise предоставляет возможность поразрядного ввода, но кроме цифр могут быть введены и другие символы. Параметр title определяет заголовок; buffer – ссылка на символьный буфер, в который будет помещен результат ввода; length – количество вводимых символов; параметр available_symbols – это строка символов, доступных для ввода. Функция возвращает true, если пользователь подтвердил ввод, false, если отказался. 22 | - **selectVal(title, list_of_values, count, [show_selected], [selected_index])** – позволяет выбрать значение из списка list и возвращает индекс выбранного элемента. title – отображаемый на дисплее заголовок; list – список значений для выбора, представляет собой массив значений типа char*, String или int; count – количество элементов в массиве; show_selected - флаг отображения метки на выбранном элементе; selected_index – индекс выбранного по умолчанию элемента. 23 | - **showMenu(menu, menu_length, show_title)** – отображает меню и возвращает ключ выбранного элемента. menu – массив элементов типа sMenuItem; menu_length – длина меню; show_title – признак необходимости отображения заголовка. 24 | - **getSelectedMenuItem()** – возвращает ключ выбранного пункта меню для использования внутри обработчиков. 25 | - **attachIdleFunc(IdleFunc)** – позволяет привязать функцию, которая будет вызываться библиотекой при бездействии. 26 | 27 | ## Поддержка дисплеев с кириллицей 28 | Для включения поддержки дисплеев с кириллицей необходимо в файле LiquidCrystal_I2C_Menu.h раскомментировать строку #define CYRILLIC_DISPLAY. После этого станет возможным использование русского текста в меню и других функциях библиотеки. 29 | 30 | 31 | ## Более подробно 32 | Более подробную информацию о библиотеке и её функциях вы найдете здесь: [https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html](https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html "https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html") -------------------------------------------------------------------------------- /LiquidCrystal_I2C_Menu.h: -------------------------------------------------------------------------------- 1 | #ifndef TSBR_LIQUID_CRYSTAL_I2C_MENU_H 2 | #define TSBR_LIQUID_CRYSTAL_I2C_MENU_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // commands 10 | #define LCD_CLEARDISPLAY 0x01 11 | #define LCD_RETURNHOME 0x02 12 | #define LCD_ENTRYMODESET 0x04 13 | #define LCD_DISPLAYCONTROL 0x08 14 | #define LCD_CURSORSHIFT 0x10 15 | #define LCD_FUNCTIONSET 0x20 16 | #define LCD_SETCGRAMADDR 0x40 17 | #define LCD_SETDDRAMADDR 0x80 18 | 19 | // flags for display entry mode 20 | #define LCD_ENTRYRIGHT 0x00 21 | #define LCD_ENTRYLEFT 0x02 22 | #define LCD_ENTRYSHIFTINCREMENT 0x01 23 | #define LCD_ENTRYSHIFTDECREMENT 0x00 24 | 25 | // flags for display on/off control 26 | #define LCD_DISPLAYON 0x04 27 | #define LCD_DISPLAYOFF 0x00 28 | #define LCD_CURSORON 0x02 29 | #define LCD_CURSOROFF 0x00 30 | #define LCD_BLINKON 0x01 31 | #define LCD_BLINKOFF 0x00 32 | 33 | // flags for display/cursor shift 34 | #define LCD_DISPLAYMOVE 0x08 35 | #define LCD_CURSORMOVE 0x00 36 | #define LCD_MOVERIGHT 0x04 37 | #define LCD_MOVELEFT 0x00 38 | 39 | // flags for function set 40 | #define LCD_8BITMODE 0x10 41 | #define LCD_4BITMODE 0x00 42 | #define LCD_2LINE 0x08 43 | #define LCD_1LINE 0x00 44 | #define LCD_5x10DOTS 0x04 45 | #define LCD_5x8DOTS 0x00 46 | 47 | // flags for backlight control 48 | #define LCD_BACKLIGHT 0x08 49 | #define LCD_NOBACKLIGHT 0x00 50 | 51 | #define En B00000100 // Enable bit 52 | #define Rw B00000010 // Read/Write bit 53 | #define Rs B00000001 // Register select bit 54 | 55 | //#define CYRILLIC_DISPLAY // Раскомментировать для поддержки дисплеев с кириллицей 56 | #define SCROLL_LONG_CAPTIONS // Раскомментировать для прокрутки длинных названий в меню и списках 57 | #define ENCODER_POOL_DELAY 5 58 | #define NUMERIC_SIGNS "- " 59 | 60 | #define SCROLL_DELAY 800 61 | #define DELAY_BEFORE_SCROLL 4000 62 | #define DELAY_AFTER_SCROLL 2000 63 | 64 | //#define INACTIVITY_TIMEOUT 60000 // Таймаут бездействия до выхода из функций 65 | 66 | enum eEncoderState {eNone, eLeft, eRight, eButton}; 67 | 68 | struct sMenuItem { 69 | uint8_t parent; 70 | uint8_t key; 71 | char *caption; 72 | void (*handler)(); 73 | }; 74 | 75 | #ifdef CYRILLIC_DISPLAY 76 | uint8_t strlenUTF8(const char *); 77 | void substrUTF8(const char*, char*, uint8_t, uint8_t); 78 | #endif 79 | 80 | /** 81 | This is the driver for the Liquid Crystal LCD displays that use the I2C bus. 82 | 83 | After creating an instance of this class, first call begin() before anything else. 84 | The backlight is on by default, since that is the most likely operating mode in 85 | most cases. 86 | */ 87 | class LiquidCrystal_I2C_Menu : public Print { 88 | public: 89 | /** 90 | Constructor 91 | 92 | @param lcd_addr I2C slave address of the LCD display. Most likely printed on the 93 | LCD circuit board, or look in the supplied LCD documentation. 94 | @param lcd_cols Number of columns your LCD display has. 95 | @param lcd_rows Number of rows your LCD display has. 96 | @param charsize The size in dots that the display has, use LCD_5x10DOTS or LCD_5x8DOTS. 97 | */ 98 | LiquidCrystal_I2C_Menu(uint8_t lcd_addr, uint8_t lcd_cols, uint8_t lcd_rows, uint8_t charsize = LCD_5x8DOTS); 99 | 100 | /** 101 | Set the LCD display in the correct begin state, must be called before anything else is done. 102 | */ 103 | void begin(); 104 | 105 | /** 106 | Remove all the characters currently shown. Next print/write operation will start 107 | from the first position on LCD display. 108 | */ 109 | void clear(); 110 | 111 | /** 112 | Next print/write operation will will start from the first position on the LCD display. 113 | */ 114 | void home(); 115 | 116 | /** 117 | Do not show any characters on the LCD display. Backlight state will remain unchanged. 118 | Also all characters written on the display will return, when the display in enabled again. 119 | */ 120 | void noDisplay(); 121 | 122 | /** 123 | Show the characters on the LCD display, this is the normal behaviour. This method should 124 | only be used after noDisplay() has been used. 125 | */ 126 | void display(); 127 | 128 | /** 129 | Do not blink the cursor indicator. 130 | */ 131 | void noBlink(); 132 | 133 | /** 134 | Start blinking the cursor indicator. 135 | */ 136 | void blink(); 137 | 138 | /** 139 | Do not show a cursor indicator. 140 | */ 141 | void noCursor(); 142 | 143 | /** 144 | Show a cursor indicator, cursor can blink on not blink. Use the 145 | methods blink() and noBlink() for changing cursor blink. 146 | */ 147 | void cursor(); 148 | 149 | void scrollDisplayLeft(); 150 | void scrollDisplayRight(); 151 | void printLeft(); 152 | void printRight(); 153 | void leftToRight(); 154 | void rightToLeft(); 155 | void shiftIncrement(); 156 | void shiftDecrement(); 157 | void noBacklight(); 158 | void backlight(); 159 | bool getBacklight(); 160 | void autoscroll(); 161 | void noAutoscroll(); 162 | void createChar(uint8_t, uint8_t[]); 163 | void setCursor(uint8_t, uint8_t); 164 | virtual size_t write(uint8_t); 165 | void command(uint8_t); 166 | 167 | inline void blink_on() { 168 | blink(); 169 | } 170 | inline void blink_off() { 171 | noBlink(); 172 | } 173 | inline void cursor_on() { 174 | cursor(); 175 | } 176 | inline void cursor_off() { 177 | noCursor(); 178 | } 179 | 180 | // Compatibility API function aliases 181 | void setBacklight(uint8_t new_val); // alias for backlight() and nobacklight() 182 | void load_custom_character(uint8_t char_num, uint8_t *rows); // alias for createChar() 183 | void printstr(const char[]); 184 | 185 | // Extended functions 186 | /** 187 | printAt = setCursor + print. 188 | */ 189 | void printAt(uint8_t, uint8_t, const String &); 190 | void printAt(uint8_t, uint8_t, const char[]); 191 | void printAt(uint8_t, uint8_t, char); 192 | void printAt(uint8_t, uint8_t, unsigned char, int = DEC); 193 | void printAt(uint8_t, uint8_t, int, int = DEC); 194 | void printAt(uint8_t, uint8_t, unsigned int, int = DEC); 195 | void printAt(uint8_t, uint8_t, long, int = DEC); 196 | void printAt(uint8_t, uint8_t, unsigned long, int = DEC); 197 | void printAt(uint8_t, uint8_t, double, int = 2); 198 | void printAt(uint8_t, uint8_t, const Printable&); 199 | 200 | /** 201 | printf - печать форматированных строк. 202 | */ 203 | void printf(const char *, ...); 204 | 205 | /** 206 | printfAt - печать форматированных строк с указанной позиции. 207 | */ 208 | void printfAt(uint8_t, uint8_t, const char *, ...); 209 | 210 | /** 211 | Установка пинов для энкодера 212 | */ 213 | void attachEncoder(uint8_t, uint8_t, uint8_t); 214 | 215 | /** 216 | 217 | */ 218 | eEncoderState getEncoderState(); 219 | 220 | /** 221 | printMultiline - печать текста с возможностью вертикальной прокрутки. 222 | */ 223 | void printMultiline(const String &); 224 | void printMultiline(const char[]); 225 | 226 | /** 227 | Ввод значений путем инкремента/декремента начального значения. 228 | inputValAt не очищает экран, позволяя использовать printf для вывод заголовка. 229 | */ 230 | template T inputVal(const String &, T, T, T, T = 1, void (*)(T) = NULL); // title, min, max, default, step = 1, onChangeFunc 231 | template T inputVal(const char[], T, T, T, T = 1, void (*)(T) = NULL); // title, min, max, default, step = 1, onChangeFunc 232 | template T inputValAt(uint8_t, uint8_t, T, T, T, T = 1, void (*)(T) = NULL); // x, y, min, max, default, step = 1, onChangeFunc 233 | 234 | /** 235 | Ввод числовых и строковых значений путем редактирования отдельных разрядов. 236 | Возвращает TRUE если пользователь подтвердил ввод. 237 | */ 238 | template bool inputValBitwise(const String &, T &, uint8_t, uint8_t = 0, bool _signed = 0); // title, value, precision, scale 239 | template bool inputValBitwise(const char[], T &, uint8_t, uint8_t = 0, bool _signed = 0); // title, value, precision, scale 240 | bool inputStrVal(const String &, char[], uint8_t, const char[]); // title, buffer, length, available symbols 241 | bool inputStrVal(const char[], char[], uint8_t, const char[]); // title, buffer, length, available symbols 242 | 243 | 244 | /** 245 | Выбор значения из списка. Возвращает индекс выбранного элемента. 246 | */ 247 | uint8_t selectVal(const String &, const char**, uint8_t, bool = true, uint8_t = 0); //title, list of values, count, show selected, selected index 248 | uint8_t selectVal(const char[], const char**, uint8_t, bool = true, uint8_t = 0); //title, list of values, count, show selected, selected index 249 | uint8_t selectVal(const String &, String[], uint8_t, bool = true, uint8_t = 0); //title, list of values, count, show selected, selected index 250 | uint8_t selectVal(const char[], String[], uint8_t, bool = true, uint8_t = 0); //title, list of values, count, show selected, selected index 251 | uint8_t selectVal(const String &, int[], uint8_t, bool = true, uint8_t = 0); //title, list of values, count, show selected, selected index 252 | uint8_t selectVal(const char[], int[], uint8_t, bool = true, uint8_t = 0); //title, list of values, count, show selected, selected index 253 | 254 | /** 255 | Функция отображения меню. Возвращает ключ выбранного пункта меню. 256 | */ 257 | uint8_t showMenu(sMenuItem[], uint8_t, bool); 258 | uint8_t getSelectedMenuItem(){return _selectedMenuItem;}; 259 | void attachIdleFunc(void (*IdleFunc)(void)); 260 | private: 261 | void send(uint8_t, uint8_t); 262 | void write4bits(uint8_t); 263 | void expanderWrite(uint8_t); 264 | void pulseEnable(uint8_t); 265 | uint8_t _addr; 266 | uint8_t _displayfunction; 267 | uint8_t _displaycontrol; 268 | uint8_t _displaymode; 269 | uint8_t _cols; 270 | uint8_t _rows; 271 | uint8_t _charsize; 272 | uint8_t _backlightval; 273 | 274 | uint8_t _pinA; 275 | uint8_t _pinB; 276 | uint8_t _pinBtn; 277 | unsigned long _prevPoolTime; 278 | bool _pinButtonPrev; 279 | bool _pinAPrev; 280 | bool _showMenuTitle; 281 | uint8_t _menuLen; 282 | sMenuItem *_menu; 283 | #ifdef SCROLL_LONG_CAPTIONS 284 | uint8_t _scrollPos; 285 | unsigned long _scrollTime; 286 | #endif 287 | #if defined(INACTIVITY_TIMEOUT) 288 | unsigned long lastActivityTime; 289 | #endif 290 | uint8_t _selectedMenuItem; 291 | void (*_IdleFunc)() = NULL; 292 | bool _inputLongVal(const char[], long &, long, long); 293 | bool getNextEditable(char S[], uint8_t lenS, const char availSymbols[], uint8_t ¤tPos, bool direction); 294 | bool isEditable(const char* ch, const char availSymbols[]); 295 | bool getNextSymbol(char *ch, bool direction, const char availSymbols[], bool looped = 0); 296 | bool _inputStrVal(const char title[], char buffer[], uint8_t len, const char availSymbols[], bool _signed); 297 | template uint8_t _selectVal(const char[], T[], uint8_t, bool, uint8_t); 298 | bool printTitle(const char title[]); 299 | void _prepareForPrint(char [], char*, uint8_t); 300 | void _prepareForPrint(char [], int, uint8_t); 301 | void _prepareForPrint(char [], String, uint8_t); 302 | uint8_t showSubMenu(uint8_t); 303 | void encoderIdle(); 304 | }; 305 | 306 | template T LiquidCrystal_I2C_Menu::inputVal(const String &title, T minValue, T maxValue, T defaultValue, T step, void (*onChangeFunc)(T)) { 307 | return inputVal(title.c_str(), minValue, maxValue, defaultValue, step, onChangeFunc); 308 | } 309 | 310 | template T LiquidCrystal_I2C_Menu::inputVal(const char title[], T minValue, T maxValue, T defaultValue, T step, void (*onChangeFunc)(T)) { 311 | T v = defaultValue; 312 | if ((minValue > maxValue) or (v < minValue) or (v > maxValue)) 313 | return v; 314 | uint8_t hasTitle = printTitle(title); 315 | v = inputValAt(0, hasTitle, minValue, maxValue, defaultValue, step, onChangeFunc); 316 | clear(); 317 | return v; 318 | } 319 | 320 | template T LiquidCrystal_I2C_Menu::inputValAt(uint8_t x, uint8_t y, T minValue, T maxValue, T defaultValue, T step, void (*onChangeFunc)(T)) { 321 | T v = defaultValue; 322 | if ((minValue > maxValue) or (v < minValue) or (v > maxValue)) 323 | return v; 324 | eEncoderState encoderState = eNone; 325 | printAt(x, y, v); 326 | if (onChangeFunc != NULL) (*onChangeFunc)(v); 327 | String S; 328 | #if defined(INACTIVITY_TIMEOUT) 329 | lastActivityTime = millis(); 330 | #endif 331 | while (1) { 332 | encoderState = getEncoderState(); 333 | #ifdef INACTIVITY_TIMEOUT 334 | // Выход по таймауту 335 | if (millis() - lastActivityTime > INACTIVITY_TIMEOUT) return defaultValue; 336 | #endif 337 | switch (encoderState) { 338 | case eNone: 339 | encoderIdle(); 340 | continue; 341 | case eButton: 342 | return v; 343 | case eLeft: 344 | S = String(v); 345 | if (v >= minValue + step) v -= step; 346 | else v = minValue; 347 | break; 348 | case eRight: 349 | S = String(v); 350 | if (v + step <= maxValue) v += step; 351 | else v = maxValue; 352 | break; 353 | } 354 | for (uint8_t i = 0; i < S.length(); i++) 355 | S.setCharAt(i, ' '); 356 | printAt(x, y, S.c_str()); 357 | printAt(x, y, v); 358 | if (onChangeFunc != NULL) (*onChangeFunc)(v); 359 | } 360 | } 361 | 362 | template bool LiquidCrystal_I2C_Menu::inputValBitwise(const String &title, T &value, uint8_t precision, uint8_t scale, bool _signed) { 363 | return inputValBitwise(title.c_str(), value, precision, scale); 364 | } 365 | 366 | template bool LiquidCrystal_I2C_Menu::inputValBitwise(const char title[], T &value, uint8_t precision, uint8_t scale, bool _signed) { 367 | char buffer[_cols]; 368 | String S; 369 | int dotPos; 370 | if (value) 371 | dtostrf(abs(value), precision, scale, buffer); 372 | else 373 | dtostrf(1, precision, scale, buffer); 374 | S = buffer; 375 | if (value == 0) S.replace("1", "0"); 376 | 377 | dotPos = S.lastIndexOf("."); 378 | if (S.length() > (unsigned)(precision + (dotPos >= 0))) return 0; // Значение не соответствует заданной точности 379 | S.replace(" ", "0"); 380 | if ((dotPos > 0) & (dotPos < precision - scale)) S = "0" + S; 381 | if (_signed) { 382 | if (value < 0) S = "-" + S; 383 | else S = " " + S; 384 | } 385 | S.getBytes((unsigned char*)buffer, S.length() + 1); 386 | if (_inputStrVal(title, buffer, S.length(), "0123456789", _signed)) { 387 | S = buffer; 388 | S.trim(); 389 | if (dotPos >= 0) value = S.toFloat(); 390 | else value = S.toInt(); 391 | return 1; 392 | } 393 | 394 | return 0; 395 | } 396 | 397 | #endif // TSBR_LIQUID_CRYSTAL_I2C_MENU_H 398 | -------------------------------------------------------------------------------- /LiquidCrystal_I2C_Menu.cpp: -------------------------------------------------------------------------------- 1 | #include "LiquidCrystal_I2C_Menu.h" 2 | #include 3 | #include 4 | #include 5 | 6 | uint8_t scrollUp[8] = {0x4, 0xa, 0x11, 0x1f}; 7 | uint8_t scrollDown[8] = {0x0, 0x0, 0x0, 0x0, 0x1f, 0x11, 0xa, 0x4}; 8 | uint8_t scrollBoth[8] = {0x4, 0xa, 0x11, 0x1f, 0x1f, 0x11, 0xa, 0x4}; 9 | 10 | uint8_t iconOk[8] = {0x0, 0x1, 0x1, 0x2, 0x12, 0xc, 0x4}; 11 | uint8_t iconCancel[8] = {0x0, 0x0, 0x11, 0xa, 0x4, 0xa, 0x11}; 12 | 13 | #ifdef CYRILLIC_DISPLAY 14 | #include 15 | // Таблица перекодировки для дисплеев с поддержкой кириллицы 16 | const uint8_t rusRecodeTable[] PROGMEM = { 17 | 'A', // А 18 | 160, // Б 19 | 'B', // В 20 | 161, // Г 21 | 224, // Д 22 | 'E', // Е 23 | 163, // Ж 24 | 164, // З 25 | 165, // И 26 | 166, // Й 27 | 'K', // К 28 | 167, // Л 29 | 'M', // М 30 | 'H', // Н 31 | 'O', // О 32 | 168, // П 33 | 'P', // Р 34 | 'C', // С 35 | 'T', // Т 36 | 169, // У 37 | 170, // Ф 38 | 'X', // Х 39 | 225, // Ц 40 | 171, // Ч 41 | 172, // Ш 42 | 226, // Щ 43 | 173, // Ъ 44 | 174, // Ы 45 | 'b', // Ь 46 | 175, // Э 47 | 176, // Ю 48 | 177, // Я 49 | 50 | 'a', // а 51 | 178, // б 52 | 179, // в 53 | 180, // г 54 | 227, // д 55 | 'e', // е 56 | 182, // ж 57 | 183, // з 58 | 184, // и 59 | 185, // й 60 | 186, // к 61 | 187, // л 62 | 188, // м 63 | 189, // н 64 | 'o', // о 65 | 190, // п 66 | 'p', // р 67 | 'c', // с 68 | 191, // т 69 | 'y', // у 70 | 228, // ф 71 | 'x', // х 72 | 229, // ц 73 | 192, // ч 74 | 193, // ш 75 | 230, // щ 76 | 194, // ъ 77 | 195, // ы 78 | 196, // ь 79 | 197, // э 80 | 198, // ю 81 | 199 // я 82 | }; 83 | uint8_t prevChar = 0; 84 | #define charCount strlenUTF8 85 | 86 | uint8_t strlenUTF8(const char *s) { 87 | uint8_t count = 0; 88 | unsigned char c; 89 | while (*s) { 90 | count++; 91 | c = *s++; 92 | if (c >= 0xC0) { 93 | *s++; 94 | } 95 | } 96 | return count; 97 | } 98 | 99 | void substrUTF8(const char* source, char* dest, uint8_t fromPos, uint8_t count) { 100 | unsigned char c; 101 | while ((fromPos-- >= 1) and (*source)) { 102 | c = *source++; 103 | if (c >= 0xC0) 104 | *source++; 105 | } 106 | while ((count-- > 0) and (*source)) { 107 | c = *source++; 108 | *dest++ = c; 109 | if (c >= 0xC0) 110 | *dest++ = *source++; 111 | } 112 | *dest = 0; 113 | } 114 | const uint8_t charLen = 2; 115 | #else 116 | #define charCount strlen 117 | const uint8_t charLen = 1; 118 | #endif 119 | 120 | // When the display powers up, it is configured as follows: 121 | // 122 | // 1. Display clear 123 | // 2. Function set: 124 | // DL = 1; 8-bit interface data 125 | // N = 0; 1-line display 126 | // F = 0; 5x8 dot character font 127 | // 3. Display on/off control: 128 | // D = 0; Display off 129 | // C = 0; Cursor off 130 | // B = 0; Blinking off 131 | // 4. Entry mode set: 132 | // I/D = 1; Increment by 1 133 | // S = 0; No shift 134 | // 135 | // Note, however, that resetting the Arduino doesn't reset the LCD, so we 136 | // can't assume that its in that state when a sketch starts (and the 137 | // LiquidCrystal constructor is called). 138 | 139 | LiquidCrystal_I2C_Menu::LiquidCrystal_I2C_Menu(uint8_t lcd_addr, uint8_t lcd_cols, uint8_t lcd_rows, uint8_t charsize) 140 | { 141 | _addr = lcd_addr; 142 | _cols = lcd_cols; 143 | _rows = lcd_rows; 144 | _charsize = charsize; 145 | _backlightval = LCD_BACKLIGHT; 146 | } 147 | 148 | void LiquidCrystal_I2C_Menu::begin() { 149 | Wire.begin(); 150 | _displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS; 151 | 152 | if (_rows > 1) { 153 | _displayfunction |= LCD_2LINE; 154 | } 155 | 156 | // for some 1 line displays you can select a 10 pixel high font 157 | if ((_charsize != 0) && (_rows == 1)) { 158 | _displayfunction |= LCD_5x10DOTS; 159 | } 160 | 161 | // SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION! 162 | // according to datasheet, we need at least 40ms after power rises above 2.7V 163 | // before sending commands. Arduino can turn on way befer 4.5V so we'll wait 50 164 | delay(50); 165 | 166 | // Now we pull both RS and R/W low to begin commands 167 | expanderWrite(_backlightval); // reset expanderand turn backlight off (Bit 8 =1) 168 | delay(1000); 169 | 170 | //put the LCD into 4 bit mode 171 | // this is according to the hitachi HD44780 datasheet 172 | // figure 24, pg 46 173 | 174 | // we start in 8bit mode, try to set 4 bit mode 175 | write4bits(0x03 << 4); 176 | delayMicroseconds(4500); // wait min 4.1ms 177 | 178 | // second try 179 | write4bits(0x03 << 4); 180 | delayMicroseconds(4500); // wait min 4.1ms 181 | 182 | // third go! 183 | write4bits(0x03 << 4); 184 | delayMicroseconds(150); 185 | 186 | // finally, set to 4-bit interface 187 | write4bits(0x02 << 4); 188 | 189 | // set # lines, font size, etc. 190 | command(LCD_FUNCTIONSET | _displayfunction); 191 | 192 | // turn the display on with no cursor or blinking default 193 | _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF; 194 | display(); 195 | 196 | // clear it off 197 | clear(); 198 | 199 | // Initialize to default text direction (for roman languages) 200 | _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT; 201 | 202 | // set the entry mode 203 | command(LCD_ENTRYMODESET | _displaymode); 204 | 205 | home(); 206 | } 207 | 208 | /********** high level commands, for the user! */ 209 | void LiquidCrystal_I2C_Menu::clear() { 210 | command(LCD_CLEARDISPLAY);// clear display, set cursor position to zero 211 | delayMicroseconds(2000); // this command takes a long time! 212 | } 213 | 214 | void LiquidCrystal_I2C_Menu::home() { 215 | command(LCD_RETURNHOME); // set cursor position to zero 216 | delayMicroseconds(2000); // this command takes a long time! 217 | } 218 | 219 | void LiquidCrystal_I2C_Menu::setCursor(uint8_t col, uint8_t row) { 220 | int row_offsets[] = { 0x00, 0x40, 0x14, 0x54 }; 221 | if (row > _rows) { 222 | row = _rows - 1; // we count rows starting w/0 223 | } 224 | command(LCD_SETDDRAMADDR | (col + row_offsets[row])); 225 | } 226 | 227 | // Turn the display on/off (quickly) 228 | void LiquidCrystal_I2C_Menu::noDisplay() { 229 | _displaycontrol &= ~LCD_DISPLAYON; 230 | command(LCD_DISPLAYCONTROL | _displaycontrol); 231 | } 232 | void LiquidCrystal_I2C_Menu::display() { 233 | _displaycontrol |= LCD_DISPLAYON; 234 | command(LCD_DISPLAYCONTROL | _displaycontrol); 235 | } 236 | 237 | // Turns the underline cursor on/off 238 | void LiquidCrystal_I2C_Menu::noCursor() { 239 | _displaycontrol &= ~LCD_CURSORON; 240 | command(LCD_DISPLAYCONTROL | _displaycontrol); 241 | } 242 | void LiquidCrystal_I2C_Menu::cursor() { 243 | _displaycontrol |= LCD_CURSORON; 244 | command(LCD_DISPLAYCONTROL | _displaycontrol); 245 | } 246 | 247 | // Turn on and off the blinking cursor 248 | void LiquidCrystal_I2C_Menu::noBlink() { 249 | _displaycontrol &= ~LCD_BLINKON; 250 | command(LCD_DISPLAYCONTROL | _displaycontrol); 251 | } 252 | void LiquidCrystal_I2C_Menu::blink() { 253 | _displaycontrol |= LCD_BLINKON; 254 | command(LCD_DISPLAYCONTROL | _displaycontrol); 255 | } 256 | 257 | // These commands scroll the display without changing the RAM 258 | void LiquidCrystal_I2C_Menu::scrollDisplayLeft(void) { 259 | command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT); 260 | } 261 | void LiquidCrystal_I2C_Menu::scrollDisplayRight(void) { 262 | command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT); 263 | } 264 | 265 | // This is for text that flows Left to Right 266 | void LiquidCrystal_I2C_Menu::leftToRight(void) { 267 | _displaymode |= LCD_ENTRYLEFT; 268 | command(LCD_ENTRYMODESET | _displaymode); 269 | } 270 | 271 | // This is for text that flows Right to Left 272 | void LiquidCrystal_I2C_Menu::rightToLeft(void) { 273 | _displaymode &= ~LCD_ENTRYLEFT; 274 | command(LCD_ENTRYMODESET | _displaymode); 275 | } 276 | 277 | // This will 'right justify' text from the cursor 278 | void LiquidCrystal_I2C_Menu::autoscroll(void) { 279 | _displaymode |= LCD_ENTRYSHIFTINCREMENT; 280 | command(LCD_ENTRYMODESET | _displaymode); 281 | } 282 | 283 | // This will 'left justify' text from the cursor 284 | void LiquidCrystal_I2C_Menu::noAutoscroll(void) { 285 | _displaymode &= ~LCD_ENTRYSHIFTINCREMENT; 286 | command(LCD_ENTRYMODESET | _displaymode); 287 | } 288 | 289 | // Allows us to fill the first 8 CGRAM locations 290 | // with custom characters 291 | void LiquidCrystal_I2C_Menu::createChar(uint8_t location, uint8_t charmap[]) { 292 | location &= 0x7; // we only have 8 locations 0-7 293 | command(LCD_SETCGRAMADDR | (location << 3)); 294 | for (int i = 0; i < 8; i++) { 295 | write(charmap[i]); 296 | } 297 | } 298 | 299 | // Turn the (optional) backlight off/on 300 | void LiquidCrystal_I2C_Menu::noBacklight(void) { 301 | _backlightval = LCD_NOBACKLIGHT; 302 | expanderWrite(0); 303 | } 304 | 305 | void LiquidCrystal_I2C_Menu::backlight(void) { 306 | _backlightval = LCD_BACKLIGHT; 307 | expanderWrite(0); 308 | } 309 | bool LiquidCrystal_I2C_Menu::getBacklight() { 310 | return _backlightval == LCD_BACKLIGHT; 311 | } 312 | 313 | /*********** extended functions ***********/ 314 | 315 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, const String &s) { 316 | setCursor(col, row); 317 | print(s); 318 | } 319 | 320 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, const char str[]) { 321 | setCursor(col, row); 322 | print(str); 323 | } 324 | 325 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, char c) { 326 | setCursor(col, row); 327 | print(c); 328 | } 329 | 330 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, unsigned char b, int base) { 331 | setCursor(col, row); 332 | print(b, base); 333 | } 334 | 335 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, int n, int base) { 336 | setCursor(col, row); 337 | print(n, base); 338 | } 339 | 340 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, unsigned int n, int base) { 341 | setCursor(col, row); 342 | print(n, base); 343 | } 344 | 345 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, long n, int base) { 346 | setCursor(col, row); 347 | print(n, base); 348 | } 349 | 350 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, unsigned long n, int base) { 351 | setCursor(col, row); 352 | print(n, base); 353 | } 354 | 355 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, double d, int digits) { 356 | setCursor(col, row); 357 | print(d, digits); 358 | } 359 | 360 | void LiquidCrystal_I2C_Menu::printAt(uint8_t col, uint8_t row, const Printable& x) { 361 | setCursor(col, row); 362 | print(x); 363 | } 364 | 365 | void LiquidCrystal_I2C_Menu::printf(const char * format, ...) { 366 | char buf[80]; 367 | va_list args; 368 | va_start (args, format); 369 | vsnprintf(buf, 80, format, args); 370 | va_end (args); 371 | print(buf); 372 | } 373 | 374 | void LiquidCrystal_I2C_Menu::printfAt(uint8_t col, uint8_t row, const char * format, ...) { 375 | char buf[80]; 376 | va_list args; 377 | va_start (args, format); 378 | vsnprintf(buf, 80, format, args); 379 | va_end (args); 380 | setCursor(col, row); 381 | print(buf); 382 | } 383 | 384 | void LiquidCrystal_I2C_Menu::attachEncoder(uint8_t pinA, uint8_t pinB, uint8_t pinBtn) { 385 | _pinA = pinA; 386 | _pinB = pinB; 387 | _pinBtn = pinBtn; 388 | pinMode(_pinA, INPUT_PULLUP); 389 | pinMode(_pinB, INPUT_PULLUP); 390 | pinMode(_pinBtn, INPUT_PULLUP); 391 | _prevPoolTime = 0; 392 | } 393 | 394 | eEncoderState LiquidCrystal_I2C_Menu::getEncoderState() { 395 | bool encoderA, encoderB; 396 | eEncoderState Result = eNone; 397 | if (millis() - _prevPoolTime > ENCODER_POOL_DELAY) { 398 | _prevPoolTime = millis(); 399 | if (digitalRead(_pinBtn) == LOW ) { 400 | if (_pinButtonPrev) { 401 | _pinButtonPrev = 0; 402 | Result = eButton; 403 | } 404 | } 405 | else { 406 | _pinButtonPrev = 1; 407 | encoderA = digitalRead(_pinA); 408 | encoderB = digitalRead(_pinB); 409 | if ((!encoderA) && (_pinAPrev)) { 410 | if (encoderB) Result = eRight; 411 | else Result = eLeft; 412 | } 413 | _pinAPrev = encoderA; 414 | } 415 | } 416 | #if defined(INACTIVITY_TIMEOUT) 417 | if (Result != eNone) lastActivityTime = millis(); 418 | #endif 419 | return Result; 420 | } 421 | 422 | bool LiquidCrystal_I2C_Menu::printTitle(const char title[]) { 423 | uint8_t l = charCount(title); 424 | char * buffer; 425 | clear(); 426 | if (l > 0) { 427 | l = min(l, _cols); 428 | #ifdef CYRILLIC_DISPLAY 429 | buffer = (char*) malloc (l * 2 + 1); 430 | substrUTF8(title, buffer, 0, l); 431 | #else 432 | buffer = (char*) malloc(l + 1); 433 | memcpy(buffer, title, l); 434 | buffer[l] = '\0'; 435 | #endif 436 | printAt(0, 0, buffer); 437 | free(buffer); 438 | return 1; 439 | } 440 | else return 0; 441 | } 442 | 443 | void LiquidCrystal_I2C_Menu::printMultiline(const String &s) { 444 | printMultiline(s.c_str()); 445 | } 446 | 447 | void LiquidCrystal_I2C_Menu::printMultiline(const char str[]) { 448 | uint8_t offset = 0; 449 | uint8_t lineLength = _cols - 1; 450 | bool needRepaint = 1; 451 | #ifdef CYRILLIC_DISPLAY 452 | char buffer[_cols * 2]; 453 | #else 454 | char buffer[_cols]; 455 | #endif 456 | #if defined(INACTIVITY_TIMEOUT) 457 | lastActivityTime = millis(); 458 | #endif 459 | eEncoderState encoderState = eNone; 460 | createChar(0, scrollUp); 461 | createChar(1, scrollDown); 462 | while (encoderState != eButton) { 463 | if (needRepaint) { 464 | needRepaint = 0; 465 | clear(); 466 | // Вывод фрамента текстовой строки 467 | for (uint8_t i = 0; i < min(_rows, (charCount(str) + lineLength - 1) / lineLength); i++) { 468 | #ifdef CYRILLIC_DISPLAY 469 | substrUTF8(str, buffer, offset + i * lineLength, lineLength); 470 | #else 471 | memcpy(buffer, &str[offset + i * lineLength], lineLength); 472 | buffer[lineLength] = '\0'; 473 | #endif 474 | printAt(0, i, buffer); 475 | } 476 | // Отображение полосы прокрутки 477 | if (offset > 0) { 478 | setCursor(lineLength, 0); 479 | write(0); // стрелка вверх 480 | } 481 | if (charCount(str) > (unsigned)(offset + _rows * lineLength)) { 482 | setCursor(lineLength, _rows - 1); 483 | write(1); // стрелка вниз 484 | } 485 | } 486 | 487 | encoderState = getEncoderState(); 488 | #ifdef INACTIVITY_TIMEOUT 489 | // Выход по таймауту 490 | if (millis() - lastActivityTime > INACTIVITY_TIMEOUT) return; 491 | #endif 492 | if (encoderState == eNone) { 493 | encoderIdle(); // При бездействии выполняем encoderIdle 494 | } 495 | else if (encoderState == eLeft) { 496 | // Выполнить прокрутку вверх 497 | if (offset > 0) { 498 | offset -= lineLength; 499 | needRepaint = 1; 500 | } 501 | } 502 | else if (encoderState == eRight) { 503 | // Выполнить прокрутку вниз 504 | if (charCount(str) > (unsigned)(offset + _rows * lineLength)) { 505 | offset += lineLength; 506 | needRepaint = 1; 507 | } 508 | } 509 | } 510 | clear(); 511 | } 512 | 513 | bool LiquidCrystal_I2C_Menu::isEditable(const char* ch, const char availSymbols[]) { 514 | if (ch[0] == '\0') return 1; 515 | byte l = strlen(availSymbols); 516 | #ifdef CYRILLIC_DISPLAY 517 | byte i = 0; 518 | while (i < l) { 519 | if (availSymbols[i] == ch[0]) { 520 | if (uint8_t(ch[0]) < 0xC0) return 1; 521 | i++; 522 | if (availSymbols[i] == ch[1]) return 1; 523 | } 524 | else if (uint8_t(availSymbols[i]) >= 0xC0) i++; 525 | i++; 526 | } 527 | #else 528 | for (byte i = 0; i < l; i++) { 529 | if (availSymbols[i] == *ch) return 1; 530 | } 531 | #endif 532 | return 0; 533 | } 534 | 535 | bool LiquidCrystal_I2C_Menu::getNextEditable(char S[], uint8_t lenS, const char availSymbols[], uint8_t ¤tPos, bool direction) { 536 | int i; 537 | if (currentPos == 254) i = lenS; 538 | else i = currentPos; 539 | while (1) { 540 | if (direction) i++; else i--; 541 | if (i < 0) return 0; 542 | if (i == lenS) { 543 | currentPos = 254; 544 | return 0; 545 | } 546 | if (isEditable(&S[i * charLen], availSymbols)) { 547 | currentPos = i; 548 | return 1; 549 | } 550 | } 551 | } 552 | 553 | bool LiquidCrystal_I2C_Menu::_inputStrVal(const char title[], char buffer[], uint8_t len, const char availSymbols[], bool _signed) { 554 | eEncoderState encoderState = eNone; 555 | uint8_t pos = 0; 556 | uint8_t hasTitle = printTitle(title); 557 | uint8_t l = len; 558 | 559 | if (l > _cols - 4) l = _cols - 4; 560 | char *tmpBuffer = (char*) malloc (l * charLen + 1); 561 | memset(tmpBuffer, '\0', l * charLen + 1); 562 | #ifdef CYRILLIC_DISPLAY 563 | substrUTF8(buffer, tmpBuffer, 0, l); 564 | #else 565 | memcpy(tmpBuffer, buffer, l); 566 | #endif 567 | 568 | printAt(0, hasTitle, tmpBuffer); 569 | #if defined(INACTIVITY_TIMEOUT) 570 | lastActivityTime = millis(); 571 | #endif 572 | 573 | createChar(0, iconOk); 574 | createChar(1, iconCancel); 575 | setCursor(_cols - 3, hasTitle); // Ok 576 | write(0); 577 | setCursor(_cols - 1, hasTitle); // Cancel 578 | write(1); 579 | 580 | #ifdef CYRILLIC_DISPLAY 581 | // Зарезервируем второй байт для однобайтных символов 582 | char *s = buffer; 583 | char *d = tmpBuffer; 584 | for (uint8_t i = 0; i < l; i++) { 585 | *d++ = *s; 586 | if (uint8_t(*s++) >= 0xC0) 587 | *d++ = *s++; 588 | else 589 | *d++ = 1; 590 | } 591 | *d = '\0'; 592 | #endif 593 | 594 | // Переместимся к первому редактируемому символу 595 | if (!_signed & (!isEditable(&tmpBuffer[pos], availSymbols))) { 596 | if (!getNextEditable(tmpBuffer, len, availSymbols, pos, 1)) 597 | setCursor(_cols - 3, hasTitle); 598 | else 599 | setCursor(pos, hasTitle); 600 | } 601 | else setCursor(pos, hasTitle); 602 | 603 | cursor(); 604 | 605 | // Основной цикл - выбор символа для редактирования или OK/Cancel 606 | while (1) { 607 | encoderState = getEncoderState(); 608 | #ifdef INACTIVITY_TIMEOUT 609 | // Выход по таймауту 610 | if (millis() - lastActivityTime > INACTIVITY_TIMEOUT) { 611 | noCursor(); 612 | clear(); 613 | return false; 614 | } 615 | #endif 616 | switch (encoderState) { 617 | case eNone: { 618 | encoderIdle(); 619 | continue; 620 | } 621 | case eLeft: { // Переместиться влево 622 | if (pos == 0) continue; // Левее уже некуда 623 | if ((pos == 1) & (_signed)) { 624 | setCursor(--pos, hasTitle); 625 | continue; 626 | } 627 | if (pos == 255) { // Курсор на кнопке Cancel, переместим его к OK 628 | setCursor(_cols - 3, hasTitle); 629 | pos--; 630 | continue; 631 | } 632 | if (getNextEditable(tmpBuffer, len, availSymbols, pos, 0)) 633 | setCursor(pos, hasTitle); 634 | continue; 635 | } 636 | case eRight: { // Переместиться вправо 637 | if (pos == 255) continue; // Правее уже некуда 638 | if (pos == 254) { // Курсор на кнопке Ok, переместим его к Cancel 639 | setCursor(_cols - 1, hasTitle); 640 | pos++; 641 | continue; 642 | } 643 | if (pos == l - 1) { // Курсор на последнем символе, переместим его к OK 644 | setCursor(_cols - 3, hasTitle); 645 | pos = 254; 646 | continue; 647 | } 648 | if (getNextEditable(tmpBuffer, len, availSymbols, pos, 1)) 649 | setCursor(pos, hasTitle); 650 | else { 651 | pos = 254; 652 | setCursor(_cols - 3, hasTitle); 653 | } 654 | continue; 655 | } 656 | case eButton: { // Нажата кнопка. Анализируем положение курсора 657 | if (pos == 255) { // Выбран Cancel 658 | noCursor(); 659 | clear(); 660 | return 0; // Cancel 661 | } 662 | if (pos == 254) { // Выбран OK 663 | noCursor(); 664 | clear(); 665 | #ifdef CYRILLIC_DISPLAY 666 | // Удалить лишние байты из буфера 667 | char *s = tmpBuffer; 668 | char *d = buffer; 669 | while (*s) { 670 | if (*s != 1) 671 | *d++ = *s++; 672 | else 673 | s++; 674 | } 675 | *d = '\0'; 676 | #else 677 | memcpy(buffer, tmpBuffer, l); 678 | #endif 679 | return 1; 680 | } 681 | // Редактирование выбранного символа 682 | encoderState = eNone; 683 | //setCursor(pos, hasTitle); 684 | blink(); 685 | while (encoderState != eButton) 686 | { 687 | encoderState = getEncoderState(); 688 | #ifdef INACTIVITY_TIMEOUT 689 | // Выход по таймауту 690 | if (millis() - lastActivityTime > INACTIVITY_TIMEOUT) { 691 | noCursor(); 692 | clear(); 693 | return false; 694 | } 695 | #endif 696 | switch (encoderState) { 697 | case eNone: { 698 | encoderIdle(); 699 | continue; 700 | } 701 | case eLeft: { 702 | if ((_signed) & (pos == 0)) { 703 | if (!getNextSymbol(&tmpBuffer[pos * charLen], 0, NUMERIC_SIGNS, 1)) continue; 704 | } 705 | else { 706 | if (!getNextSymbol(&tmpBuffer[pos * charLen], 0, availSymbols)) continue; 707 | } 708 | printAt(pos, hasTitle, tmpBuffer[pos * charLen]); 709 | #ifdef CYRILLIC_DISPLAY 710 | if (uint8_t(tmpBuffer[pos * charLen]) >= 0xC0) 711 | print(tmpBuffer[pos * charLen + 1]); 712 | #endif 713 | setCursor(pos, hasTitle); 714 | continue; 715 | } 716 | case eRight: { 717 | if ((_signed) & (pos == 0)) { 718 | if (!getNextSymbol(&tmpBuffer[pos * charLen], 1, NUMERIC_SIGNS, 1)) continue; 719 | } 720 | else { 721 | if (!getNextSymbol(&tmpBuffer[pos * charLen], 1, availSymbols)) continue; // Можно и здесь установить флаг looped 722 | } 723 | printAt(pos, hasTitle, tmpBuffer[pos * charLen]); 724 | #ifdef CYRILLIC_DISPLAY 725 | if (uint8_t(tmpBuffer[pos * charLen]) >= 0xC0) 726 | print(tmpBuffer[pos * charLen + 1]); 727 | #endif 728 | setCursor(pos, hasTitle); 729 | continue; 730 | } 731 | case eButton: break; 732 | } 733 | } 734 | noBlink(); 735 | continue; 736 | } 737 | } 738 | } 739 | } 740 | 741 | bool LiquidCrystal_I2C_Menu::getNextSymbol(char *ch, bool direction, const char availSymbols[], bool looped) { 742 | if (ch[0] == '\0') { 743 | ch[0] = availSymbols[0]; 744 | #ifdef CYRILLIC_DISPLAY 745 | if (uint8_t(ch[0]) >= 0xC0) 746 | ch[1] = availSymbols[1]; 747 | else 748 | ch[1] = 1; 749 | #endif 750 | return 1; 751 | } 752 | #ifdef CYRILLIC_DISPLAY 753 | bool match = false; 754 | uint8_t b = 0; 755 | uint8_t i = 0; 756 | while (i < strlen(availSymbols)) { 757 | match = (availSymbols[i] == ch[0]); 758 | if ((match) and (uint8_t(ch[0]) >= 0xC0)) { 759 | match = (availSymbols[i + 1] == ch[1]); 760 | b = 1; 761 | } 762 | if (match) { 763 | if (direction) { 764 | i = i + b; 765 | if (++i < strlen(availSymbols)); 766 | else if (looped) i = 0; 767 | else return 0; 768 | 769 | ch[0] = availSymbols[i]; 770 | if (uint8_t(ch[0]) >= 0xC0) ch[1] = availSymbols[i + 1]; 771 | else ch[1] = 1; 772 | return 1; 773 | } 774 | else { 775 | if (i > 0) i--; 776 | else if (looped) i = strlen(availSymbols) - 1; 777 | else return 0; 778 | if ((i > 0) and (uint8_t(availSymbols[i - 1]) >= 0xC0)) { 779 | ch[1] = availSymbols[i]; 780 | i--; 781 | } 782 | else ch[1] = 1; 783 | ch[0] = availSymbols[i]; 784 | return 1; 785 | } 786 | return 0; 787 | } 788 | i++; 789 | } 790 | #else 791 | for (uint8_t i = 0; i < strlen(availSymbols); i++) { 792 | if (availSymbols[i] == ch[0]) { 793 | if (direction) { 794 | if (++i < strlen(availSymbols)) ch[0] = availSymbols[i]; 795 | else if (looped) ch[0] = availSymbols[0]; 796 | return 1; 797 | } 798 | else { 799 | if (i > 0) ch[0] = availSymbols[--i]; 800 | else if (looped) ch[0] = availSymbols[strlen(availSymbols) - 1]; 801 | return 1; 802 | } 803 | return 0; 804 | } 805 | } 806 | #endif 807 | 808 | return 0; 809 | } 810 | 811 | bool LiquidCrystal_I2C_Menu::inputStrVal(const String &title, char buffer[], uint8_t len, const char availSymbols[]) { 812 | return _inputStrVal(title.c_str(), buffer, len, availSymbols, 0); 813 | } 814 | 815 | bool LiquidCrystal_I2C_Menu::inputStrVal(const char title[], char buffer[], uint8_t len, const char availSymbols[]) { 816 | return _inputStrVal(title, buffer, len, availSymbols, 0); 817 | } 818 | 819 | uint8_t LiquidCrystal_I2C_Menu::selectVal(const String &title, const char **list, uint8_t count, bool show_selected, uint8_t preselected) { 820 | return _selectVal(title.c_str(), list, count, show_selected, preselected); 821 | } 822 | 823 | uint8_t LiquidCrystal_I2C_Menu::selectVal(const char title[], const char **list, uint8_t count, bool show_selected, uint8_t preselected) { 824 | return _selectVal(title, list, count, show_selected, preselected); 825 | } 826 | 827 | uint8_t LiquidCrystal_I2C_Menu::selectVal(const String &title, String list[], uint8_t count, bool show_selected, uint8_t preselected) { 828 | return _selectVal(title.c_str(), list, count, show_selected, preselected); 829 | } 830 | 831 | uint8_t LiquidCrystal_I2C_Menu::selectVal(const char title[], String list[], uint8_t count, bool show_selected, uint8_t preselected) { 832 | return _selectVal(title, list, count, show_selected, preselected); 833 | } 834 | 835 | uint8_t LiquidCrystal_I2C_Menu::selectVal(const String &title, int list[], uint8_t count, bool show_selected, uint8_t preselected) { 836 | return _selectVal(title.c_str(), list, count, show_selected, preselected); 837 | } 838 | 839 | uint8_t LiquidCrystal_I2C_Menu::selectVal(const char title[], int list[], uint8_t count, bool show_selected, uint8_t preselected) { 840 | return _selectVal(title, list, count, show_selected, preselected); 841 | } 842 | 843 | void LiquidCrystal_I2C_Menu::_prepareForPrint(char buffer[], int value, uint8_t len) { 844 | char format[8] = {'\0'}; 845 | sprintf(format, "%%-%dd", len); 846 | sprintf(buffer, format, value); 847 | } 848 | 849 | void LiquidCrystal_I2C_Menu::_prepareForPrint(char buffer[], char *value, uint8_t len) { 850 | char format[12] = {'\0'}; 851 | sprintf(format, "%%-%d.%ds", len, len); 852 | sprintf(buffer, format, value); 853 | } 854 | 855 | void LiquidCrystal_I2C_Menu::_prepareForPrint(char buffer[], String value, uint8_t len) { 856 | char format[12] = {'\0'}; 857 | sprintf(format, "%%-%d.%ds", len, len); 858 | sprintf(buffer, format, value.c_str()); 859 | } 860 | 861 | template 862 | uint8_t LiquidCrystal_I2C_Menu::_selectVal(const char title[], T list[], uint8_t count, bool show_selected, uint8_t preselected) { 863 | uint8_t offset = 0; 864 | uint8_t cursorOffset = 0; 865 | uint8_t lineLength = _cols - 3; 866 | uint8_t selected = preselected; 867 | bool needRepaint = 1; 868 | char *buffer; 869 | uint8_t hasTitle = printTitle(title); 870 | 871 | createChar(0, scrollUp); 872 | createChar(1, scrollDown); 873 | createChar(2, scrollBoth); 874 | createChar(3, iconOk); 875 | 876 | #if defined(INACTIVITY_TIMEOUT) 877 | lastActivityTime = millis(); 878 | #endif 879 | 880 | eEncoderState encoderState = eNone; 881 | while (1) { 882 | if (needRepaint) { 883 | needRepaint = 0; 884 | // Перерисовка всего содержимого экрана кроме заголовка 885 | #ifdef CYRILLIC_DISPLAY 886 | buffer = (char*) malloc (_cols * 2 + 1); 887 | #else 888 | buffer = (char*) malloc (_cols + 1); 889 | #endif 890 | for (uint8_t i = 0; i < min(count, _rows - hasTitle); i++) { 891 | _prepareForPrint(buffer, list[offset + i], lineLength); 892 | printfAt(0, i + hasTitle, " %s ", buffer); 893 | if (show_selected) { 894 | if (offset + i == selected) { 895 | setCursor(1, i + hasTitle); 896 | write(3); 897 | } 898 | } 899 | } 900 | free(buffer); 901 | // Вывод курсора 902 | printAt(0, cursorOffset + hasTitle, '>'); 903 | // Отображение полосы прокрутки 904 | if ((_rows - hasTitle == 1) and (offset > 0) and (offset + _rows - hasTitle < count)) { 905 | setCursor(_cols - 1, hasTitle); 906 | write(2); // двойная стрелка 907 | } 908 | else { 909 | if (offset > 0) { 910 | setCursor(_cols - 1, hasTitle); 911 | write(0); // стрелка вверх 912 | } 913 | if (offset + _rows - hasTitle < count) { 914 | setCursor(_cols - 1, _rows - 1); 915 | write(1); // стрелка вниз 916 | } 917 | } 918 | } 919 | encoderState = getEncoderState(); 920 | #ifdef INACTIVITY_TIMEOUT 921 | // Выход по таймауту 922 | if (millis() - lastActivityTime > INACTIVITY_TIMEOUT) { 923 | clear(); 924 | return preselected; 925 | } 926 | #endif 927 | switch (encoderState) { 928 | case eNone: { 929 | encoderIdle(); 930 | continue; 931 | } 932 | case eLeft: { 933 | if (cursorOffset > 0) { 934 | printAt(0, cursorOffset + hasTitle, ' '); 935 | printAt(0, --cursorOffset + hasTitle, '>'); 936 | } 937 | else if (offset > 0) { 938 | offset--; 939 | needRepaint = 1; 940 | } 941 | continue; 942 | } 943 | case eRight: { 944 | if (cursorOffset < min(_rows - hasTitle, count) - 1) { 945 | printAt(0, cursorOffset + hasTitle, ' '); 946 | printAt(0, ++cursorOffset + hasTitle, '>'); 947 | } 948 | else if ((cursorOffset + hasTitle + 1 == _rows) & (offset + _rows - hasTitle < count)) { 949 | offset++; 950 | needRepaint = 1; 951 | } 952 | continue; 953 | } 954 | case eButton: { 955 | if ((show_selected) & (offset + cursorOffset != selected)) { 956 | // Изменился выбранный элемент 957 | if ((selected >= offset) & (selected < offset + _rows - hasTitle)) 958 | printAt(1, selected - offset + hasTitle, ' '); 959 | selected = offset + cursorOffset; 960 | setCursor(1, cursorOffset + hasTitle); 961 | write(3); 962 | continue; 963 | } 964 | else { 965 | clear(); 966 | return offset + cursorOffset; 967 | } 968 | } 969 | 970 | } 971 | 972 | } 973 | 974 | } 975 | 976 | uint8_t LiquidCrystal_I2C_Menu::showMenu(sMenuItem menu[], uint8_t menuLen, bool showTitle) { 977 | _showMenuTitle = showTitle; 978 | _menu = menu; 979 | _menuLen = menuLen; 980 | uint8_t selectedItem; 981 | #if defined(INACTIVITY_TIMEOUT) 982 | lastActivityTime = millis(); 983 | #endif 984 | 985 | createChar(0, scrollUp); 986 | createChar(1, scrollDown); 987 | createChar(2, scrollBoth); 988 | selectedItem = showSubMenu(1); 989 | clear(); 990 | return selectedItem; 991 | } 992 | 993 | uint8_t LiquidCrystal_I2C_Menu::showSubMenu(uint8_t key) { 994 | uint8_t result, subMenuLen = 0, subMenuIndex, offset = 0, cursorOffset = 0; 995 | sMenuItem **subMenu = NULL; 996 | bool needRepaint = 1; 997 | char *buffer; 998 | eEncoderState encoderState; 999 | subMenuLen = 0; 1000 | uint8_t itemMaxLength = _cols - 1; 1001 | // Формируем массив подменю 1002 | for (uint8_t i = 0; i < _menuLen; i++) { 1003 | if (_menu[i].key == key) 1004 | subMenuIndex = i; 1005 | else if (_menu[i].parent == key) { 1006 | subMenuLen++; 1007 | subMenu = (sMenuItem**) realloc (subMenu, subMenuLen * sizeof(void*)); 1008 | subMenu[subMenuLen - 1] = &_menu[i]; 1009 | } 1010 | } 1011 | 1012 | if (subMenuLen == 0) // Подменю нет 1013 | return key; // Вернем ключ выбранного пункта 1014 | 1015 | // Отображение подменю 1016 | #ifdef SCROLL_LONG_CAPTIONS 1017 | _scrollPos = 0; 1018 | _scrollTime = millis() + DELAY_BEFORE_SCROLL; 1019 | #endif 1020 | 1021 | if (subMenuLen > _rows - _showMenuTitle) itemMaxLength--; 1022 | do { 1023 | if (needRepaint) { 1024 | needRepaint = 0; 1025 | clear(); 1026 | buffer = (char*) malloc (_cols * 2 + 1); 1027 | if (_showMenuTitle) { 1028 | #ifdef CYRILLIC_DISPLAY 1029 | substrUTF8(_menu[subMenuIndex].caption, buffer, 0, _cols); 1030 | #else 1031 | memcpy(buffer, _menu[subMenuIndex].caption, _cols); 1032 | buffer[_cols] = 0; 1033 | #endif 1034 | printAt(0, 0, buffer); 1035 | } 1036 | //buffer[_cols] = 0; 1037 | for (uint8_t i = 0; i < min(subMenuLen, _rows - _showMenuTitle); i++) { 1038 | #ifdef CYRILLIC_DISPLAY 1039 | substrUTF8(subMenu[offset + i]->caption, buffer, 0, itemMaxLength); 1040 | #else 1041 | memcpy(buffer, subMenu[offset + i]->caption, itemMaxLength); 1042 | buffer[itemMaxLength] = 0; 1043 | #endif 1044 | printAt(1 , i + _showMenuTitle, buffer); 1045 | } 1046 | 1047 | printAt(0, cursorOffset + _showMenuTitle, '>'); 1048 | if ((_rows - _showMenuTitle == 1) and (offset > 0) and (offset + _rows - _showMenuTitle < subMenuLen)) { 1049 | setCursor(_cols - 1, _showMenuTitle); 1050 | write(2); // двойная стрелка 1051 | } 1052 | else { 1053 | if (offset > 0) { 1054 | setCursor(_cols - 1, _showMenuTitle); 1055 | write(0); 1056 | } 1057 | if (offset + _rows - _showMenuTitle < subMenuLen) { 1058 | setCursor(_cols - 1, _rows - 1); 1059 | write(1); 1060 | } 1061 | } 1062 | free(buffer); 1063 | } 1064 | encoderState = getEncoderState(); 1065 | #ifdef INACTIVITY_TIMEOUT 1066 | // Выход по таймауту 1067 | if (millis() - lastActivityTime > INACTIVITY_TIMEOUT) { 1068 | free(subMenu); 1069 | return 0; 1070 | } 1071 | #endif 1072 | switch (encoderState) { 1073 | case eLeft: { 1074 | // Перемещение курсора вверх 1075 | #ifdef SCROLL_LONG_CAPTIONS 1076 | _scrollTime = millis() + DELAY_BEFORE_SCROLL; 1077 | #endif 1078 | if (cursorOffset > 0) { // Перемещаем курсор без перерисовки экрана 1079 | #ifdef SCROLL_LONG_CAPTIONS 1080 | if (_scrollPos) { 1081 | // Если предыдущий пункт прокручивался, то печатаем его заново 1082 | #ifdef CYRILLIC_DISPLAY 1083 | buffer = (char*) malloc (_cols * 2 + 1); 1084 | substrUTF8(subMenu[cursorOffset + offset]->caption, buffer, 0, itemMaxLength); 1085 | #else 1086 | buffer = (char*) malloc (_cols + 1); 1087 | memcpy(buffer, subMenu[cursorOffset + offset]->caption, itemMaxLength); 1088 | #endif 1089 | printAt(1, cursorOffset + _showMenuTitle, buffer); 1090 | free(buffer); 1091 | _scrollPos = 0; 1092 | } 1093 | #endif 1094 | printAt(0, cursorOffset + _showMenuTitle, ' '); 1095 | printAt(0, --cursorOffset + _showMenuTitle, '>'); 1096 | } 1097 | else if (offset > 0) { 1098 | // Курсор уже в верхней позиции. Если есть пункты выше, выведем их на экран 1099 | offset--; 1100 | needRepaint = 1; 1101 | } 1102 | break; 1103 | } 1104 | case eRight: { 1105 | // Перемещение курсора вниз 1106 | #ifdef SCROLL_LONG_CAPTIONS 1107 | _scrollTime = millis() + DELAY_BEFORE_SCROLL; 1108 | #endif 1109 | if (cursorOffset < min(_rows - _showMenuTitle, subMenuLen) - 1) {// Moving cursor down if possible 1110 | #ifdef SCROLL_LONG_CAPTIONS 1111 | if (_scrollPos) { 1112 | // Если предыдущий пункт прокручивался, то печатаем его заново 1113 | #ifdef CYRILLIC_DISPLAY 1114 | buffer = (char*) malloc (_cols * 2 + 1); 1115 | substrUTF8(subMenu[cursorOffset + offset]->caption, buffer, 0, itemMaxLength); 1116 | #else 1117 | buffer = (char*) malloc (_cols + 1); 1118 | memcpy(buffer, subMenu[cursorOffset + offset]->caption, itemMaxLength); 1119 | #endif 1120 | printAt(1, cursorOffset + _showMenuTitle, buffer); 1121 | free(buffer); 1122 | _scrollPos = 0; 1123 | } 1124 | #endif 1125 | printAt(0, cursorOffset + _showMenuTitle, ' '); 1126 | printAt(0, ++cursorOffset + _showMenuTitle, '>'); 1127 | } 1128 | else if ((cursorOffset + _showMenuTitle + 1 == _rows) & (offset + _rows - _showMenuTitle < subMenuLen)) { 1129 | // Курсор уже в нижней позиции. Если есть пункты ниже, выведем их на экран 1130 | offset++; 1131 | needRepaint = 1; 1132 | } 1133 | break; 1134 | } 1135 | case eButton: { 1136 | // Выбор пункта меню 1137 | if (subMenu[cursorOffset + offset]->key == 0) { // It's "Back" 1138 | free(subMenu); 1139 | return 0; 1140 | } 1141 | // Если для данного пункта есть обработчик ... 1142 | _selectedMenuItem = subMenu[cursorOffset + offset]->key; 1143 | if (subMenu[cursorOffset + offset]->handler != NULL) { 1144 | (*subMenu[cursorOffset + offset]->handler)(); // ... то выполним его 1145 | // Стрелки для прокрутки могли испортить, создадим их заново 1146 | createChar(0, scrollUp); 1147 | createChar(1, scrollDown); 1148 | } 1149 | else {// В противном случае пробуем вывести подменю для данного пункта 1150 | result = showSubMenu(subMenu[cursorOffset + offset]->key); 1151 | if (result != 0) { 1152 | free(subMenu); 1153 | return result; 1154 | } 1155 | } 1156 | #ifdef SCROLL_LONG_CAPTIONS 1157 | // Отсрочка прокрутки дисплея 1158 | _scrollTime = millis() + DELAY_BEFORE_SCROLL; 1159 | #endif 1160 | needRepaint = 1; 1161 | break; 1162 | } 1163 | case eNone: { 1164 | #ifdef SCROLL_LONG_CAPTIONS 1165 | if (charCount(subMenu[cursorOffset + offset]->caption) > itemMaxLength) { 1166 | if (_scrollTime < millis()) { 1167 | _scrollPos++; 1168 | if (_scrollPos == charCount(subMenu[cursorOffset + offset]->caption) - itemMaxLength) 1169 | _scrollTime = millis() + DELAY_AFTER_SCROLL; 1170 | else if (_scrollPos > charCount(subMenu[cursorOffset + offset]->caption) - itemMaxLength) { 1171 | _scrollPos = 0; 1172 | _scrollTime = millis() + DELAY_BEFORE_SCROLL; 1173 | } 1174 | else 1175 | _scrollTime = millis() + SCROLL_DELAY; 1176 | #ifdef CYRILLIC_DISPLAY 1177 | buffer = (char*) malloc (itemMaxLength * 2 + 1); 1178 | substrUTF8(subMenu[cursorOffset + offset]->caption, buffer, _scrollPos, itemMaxLength); 1179 | #else 1180 | memcpy(buffer, subMenu[cursorOffset + offset]->caption + _scrollPos, itemMaxLength); 1181 | buffer = (char*) malloc (itemMaxLength + 1); 1182 | #endif 1183 | printAt(1, cursorOffset + _showMenuTitle, buffer); 1184 | free(buffer); 1185 | } 1186 | } 1187 | #endif // SCROLL_LONG_CAPTIONS 1188 | encoderIdle(); 1189 | } 1190 | } 1191 | } while (1); 1192 | } 1193 | 1194 | void LiquidCrystal_I2C_Menu::attachIdleFunc(void (*IdleFunc)(void)) { 1195 | _IdleFunc = IdleFunc; 1196 | } 1197 | 1198 | void LiquidCrystal_I2C_Menu::encoderIdle() { 1199 | if (_IdleFunc != NULL) (*_IdleFunc)(); 1200 | } 1201 | 1202 | 1203 | /*********** mid level commands, for sending data/cmds */ 1204 | 1205 | inline void LiquidCrystal_I2C_Menu::command(uint8_t value) { 1206 | send(value, 0); 1207 | } 1208 | 1209 | inline size_t LiquidCrystal_I2C_Menu::write(uint8_t value) { 1210 | #ifdef CYRILLIC_DISPLAY 1211 | if ((value == 208) or (value == 209)) 1212 | prevChar = value; 1213 | else if (prevChar == 208) { // А - п 1214 | if ((value >= 144) and (value <= 191)) 1215 | send(pgm_read_word_near(rusRecodeTable + value - 144), Rs); 1216 | else if (value == 129) // Ё 1217 | send(162, Rs); 1218 | prevChar = 0; 1219 | } 1220 | else if (prevChar == 209) { // р - я 1221 | if ((value >= 128) and (value <= 143)) 1222 | send(pgm_read_word_near(rusRecodeTable + value - 80), Rs); 1223 | else if (value == 145) // ё 1224 | send(181, Rs); 1225 | prevChar = 0; 1226 | } 1227 | else 1228 | #endif 1229 | send(value, Rs); 1230 | return 1; 1231 | } 1232 | 1233 | 1234 | /************ low level data pushing commands **********/ 1235 | 1236 | // write either command or data 1237 | void LiquidCrystal_I2C_Menu::send(uint8_t value, uint8_t mode) { 1238 | uint8_t highnib = value & 0xf0; 1239 | uint8_t lownib = (value << 4) & 0xf0; 1240 | write4bits((highnib) | mode); 1241 | write4bits((lownib) | mode); 1242 | } 1243 | 1244 | void LiquidCrystal_I2C_Menu::write4bits(uint8_t value) { 1245 | expanderWrite(value); 1246 | pulseEnable(value); 1247 | } 1248 | 1249 | void LiquidCrystal_I2C_Menu::expanderWrite(uint8_t _data) { 1250 | Wire.beginTransmission(_addr); 1251 | Wire.write((int)(_data) | _backlightval); 1252 | Wire.endTransmission(); 1253 | } 1254 | 1255 | void LiquidCrystal_I2C_Menu::pulseEnable(uint8_t _data) { 1256 | expanderWrite(_data | En); // En high 1257 | delayMicroseconds(1); // enable pulse must be >450ns 1258 | 1259 | expanderWrite(_data & ~En); // En low 1260 | delayMicroseconds(40); // commands need > 37us to settle 1261 | } 1262 | 1263 | void LiquidCrystal_I2C_Menu::load_custom_character(uint8_t char_num, uint8_t *rows) { 1264 | createChar(char_num, rows); 1265 | } 1266 | 1267 | void LiquidCrystal_I2C_Menu::setBacklight(uint8_t new_val) { 1268 | if (new_val) { 1269 | backlight(); // turn backlight on 1270 | } else { 1271 | noBacklight(); // turn backlight off 1272 | } 1273 | } 1274 | 1275 | void LiquidCrystal_I2C_Menu::printstr(const char c[]) { 1276 | //This function is not identical to the function used for "real" I2C displays 1277 | //it's here so the user sketch doesn't have to be changed 1278 | print(c); 1279 | } 1280 | --------------------------------------------------------------------------------