Текстовое меню на Arduino для дисплея 20х4

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

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

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

Такая структура представляет собой двумерный массив: одно измерение — это номера экранов, второе — номера строк. Для перемещения по такому меню достаточно использовать две переменных: номер экрана и номер строки соответственно. 

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

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

В итоге у нас получается 4 двумерных массива с одинаковой размерностью: первый — названия пунктов меню, второй и третий — прикрепленные функции (по две на каждый пункт, поэтому массива с функциями два), и четвертый — это массив переменных отображаемых в пунктах меню. 

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

Сам экран подключен по IC, в качестве органа управления — энкодер, также при необходимости можно сделать управление на кнопках, но, на мой взгляд, энкодер удобней. 

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

 В прикрепленном файле: #include <Wire.h> #include <LiquidCrystal_I2C.h>

И в основном подключается библиотека энкодера и собственно файл с кодом меню:
 

 #include "GyverEncoder.h" #include "MatroskinMenu.h"

Далее назначаются пины энкодера и опционально алгоритм его работы. 

После можно указать требуемое количество экранов и строк на каждом экране:
 

 //#define QUANTITY_SCREENS 5                               // количество экранов //#define QUANTITY_LINES 7                                 // количество строк

Если не указать, то по умолчанию будет четыре экрана по пять строк.

После создаются объекты энкодера и меню, объект меню создается самым последним, т.к. он использует вышеперечисленные объекты. Далее следует объявление переменных и первоначальная настройка в секции setup, там рассказывать особенно нечего, если что-то непонятно — задавайте вопросы в комментариях. 

Теперь непосредственно создание и заполнение меню. Для задания названия пункта меню используется функция void SetNames (uint8_t s, uint8_t l, String n, uint16_t ind) или void SetNames (uint8_t s, uint8_t l, String n). Она перегружена и принимает три или четыре параметра, номер экрана (uint8_t s), номер строки (uint8_t l), название строки (String n) и еще может принимать переменную для отображения в конце строки (uint16_t ind), но можно и без нее. Как не трудно догадаться, эта функция присваивает название соответствующей строке меню и передает указатель на переменную, которую мы хотим видеть на этой строке. Следует обратить внимание, что переменная для корректного отображения должна иметь тип uint16_t, на экране под нее отведено три знакоместа, что дает нам корректное отображение чисел от 1 до 999 (0 отображаться не будет) . Также важный момент: эта переменная хранится в массиве как указатель, поэтому в функцию мы ее передаем по ссылке (&) . Пример использования функции:

   menu_one.SetNames(0, 0, "First Screen");   menu_one.SetNames(0, 1, "1_line Val", &pokazometr);   menu_one.SetNames(0, 2, "2_line");   menu_one.SetNames(0, 3, "3_line");

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

После задания названий пунктов меню нужно поставить в соответствие или, проще говоря, прикрепить какие-либо внешние функции. Для каждого пункта меню предусмотрено максимально две функции, они независимы друг от друга, для их назначения используются функции void SetFunc1(uint8_t s, uint8_t l, void *p) и void SetFunc1(uint8_t s, uint8_t l, void *p) соответственно. Синтаксис вызова функций похож на функцию присвоения названий пунктам меню, с той лишь разницей, что кроме координат (номера экрана и номера строки) в функцию передается указатель на прикрепляемую внешнюю функцию. Пример использования:
 

   menu_one.SetFunc1 (1, 2, LedOn);   menu_one.SetFunc2 (1, 2, LedOff);

Здесь мы задали для второй строки первого экрана две функции: включения и выключения светодиода (не забываем, что нумерация элементов массивов начинается с нуля ).

После всего этого остается только отобразить наше меню на экране, для этого используется функция void MakeMenu (uint8_t f, uint8_t s), у нее два принимаемых аргумента, это номер активной строки uint8_t f (фокуса меню) и номер экрана uint8_t s. Для каждого из этих параметров возможно три варианта развития событий. Первый — параметр равен единице, это означает, что мы хотим переключиться на следующую по порядку строку/экран. Второй, — когда  предаваемый параметр равен нулю, — предыдущая строка/экран. И во всех остальных случаях (в скетче для этого используется двойка) переключения не произойдет, текущий экран и активная строка будут просто перерисованы на дисплее без изменений (обновление содержимого экрана). Примеры вызова этих функций по событиям энкодера:

   enc1.tick();   if (enc1.isRight()) {                // Событие перемещения по списку строк вниз (следующая строка)     menu_one.MakeMenu( 1, 2);   };   if (enc1.isLeft()) {                 // Событие перемещения по списку строк вверх (предыдущая строка)     menu_one.MakeMenu( 0, 2);   };   if (enc1.isClick()) {                // Событие переключения следующего экрана меню     menu_one.MakeMenu( 2, 1);   }

Событие для переключения к предыдущему экрану (menu_one.MakeMenu( 2, 0)) у меня не задано, т.к. количество экранов небольшое и они просто циклично переключаются по кругу, но при необходимости его не трудно добавить.

Осталась функция вызова прикрепленных функций, здесь все просто: void RunFunction1() и void RunFunction2() , для первой и второй прикрепленной функции соответственно. Эти функции не принимают ни каких параметров, они просто запускают внешние функции согласно текущему экрану и активной строке на нем. Их вызов по зажатому повороту энкодера:

 if (enc1.isRightH()) {               //Событие для прикрепленной функции 1 (ПРАВЫЙ нажатый поворот энкодера)     menu_one.RunFunction1();   };    if (enc1.isLeftH()) {                //Событие для прикрепленной функции 2 (ЛЕВЫЙ нажатый поворот энкодера)     menu_one.RunFunction2();   };

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