В данной статье я бы хотел поделиться опытом создания простого универсального текстового меню для ардуино. Его можно использовать в совершенно различных проектах, меню легко масштабируется под различное количество пунктов, основные его возможности такие:
- Создание простых текстовых меню с необходимым количеством экранов и строк на экране.
- Вывод на экран значения переменной отдельно для каждой строки.
- Создание экранов с различающимся количеством строк.
- Назначение до двух независимых функций для каждой строки экрана.
Итак, для начала немного теории, разберемся с основными принципам построения структуры меню. Для упрощения задачи я отказался от многоуровневой реализации пунктов меню, в основе лежит классический принцип: несколько экранов, на каждом экране несколько строк (пунктов меню), все как в ресторане. Соответственно, общее количество пунктов меню равно произведению количества экранов на количество строк в каждом экране.
Такая структура представляет собой двумерный массив: одно измерение — это номера экранов, второе — номера строк. Для перемещения по такому меню достаточно использовать две переменных: номер экрана и номер строки соответственно.
Но меню подразумевает не только отображение названий своих пунктов, но так же и возможность назначить на каждый пункт какую-то определенную функцию, а также реализовывать обратную связь с пользователем, например, отображая в конкретном пункте меню значение изменяемой переменной (или вообще любой другой переменной).
Логично для этих целей также использовать двумерные массивы такого же размера, как и основной массив с названиями пунктов меню. Тогда переменные, отвечающие за номер экрана и строки на экране, могут быть использованы также для однозначного указания на внешнюю функцию, прикрепляемую к пункту меню, и для выбора переменной, значение которой необходимо отобразить на экране.
В итоге у нас получается 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(); };
На этом, пожалуй, все, если что-то непонятно, задавайте вопросы в комментариях. В целом я старался довольно подробно комментировать скетч, так что, думаю, в нем будет несложно разобраться, если это потребуется. Всем спасибо за внимание!