// /* Hello Wokwi! */
// #include <LiquidCrystal_I2C.h>
// LiquidCrystal_I2C LCD(0x27, 20, 4);
// void setup() {
// LCD.init();
// LCD.backlight();
// LCD.setCursor(1, 0);
// LCD.print("Hello, Wokwi!");
// }
// void loop() {
// LCD.setCursor(7, 1);
// LCD.print(millis() / 1000);
// }
#include "LCD_1602_RUS.h"
LCD_1602_RUS LCD(0x27, 16, 2);
// ***** I2C дисплей *****
// #include <LiquidCrystal_I2C.h> // https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library
#define cols 16
#define rows 2
// LiquidCrystal_I2C LCD(0x27, cols, rows);
char *Blank;
// ********** Параметры меню **********
#define ShowScrollBar 1 // Показывать индикаторы прокрутки (0/1)
#define ScrollLongCaptions 1// Прокручивать длинные названия (0/1)
#define ScrollDelay 800 // Задержка при прокрутке текста
#define BacklightDelay 20000// Длительность подсветки
#define ReturnFromMenu 0 // Выходить из меню после выбора элемента(0/1)
enum eMenuKey {mkNull, mkBack, mkRoot, mkQuad, mkQuadSetA, mkQuadSetB, mkQuadSetC, mkQuadCalc, mkMulti, mkSettings, mkSetMotors,
mkMotorsAuto, mkMotorsManual, mkSetSensors, mkSetUltrasonic, mkSetLightSensors, mkSetDefaults
};
// ********** Переменные для энкодера ***************
#define pin_CLK 2 // Энкодер пин A
#define pin_DT 4 // Энкодер пин B
#define pin_Btn 3 // Кнопка
unsigned long CurrentTime, PrevEncoderTime;
enum eEncoderState {eNone, eLeft, eRight, eButton};
eEncoderState EncoderState;
int EncoderA, EncoderB, EncoderAPrev, counter;
bool ButtonPrev;
// ********** Прототипы функций ***************
eEncoderState GetEncoderState();
void LCDBacklight(byte v = 2);
eMenuKey DrawMenu(eMenuKey Key);
// ********** Обработчики для пунктов меню **********
int InputValue(char* Title, int DefaultValue, int MinValue, int MaxValue) {
// Вспомогательная функция для ввода значения
LCD.clear();
LCD.print(Title);
LCD.setCursor(0, 1);
LCD.print(DefaultValue);
delay(100);
while (1)
{
EncoderState = GetEncoderState();
switch (EncoderState) {
case eNone: {
LCDBacklight();
continue;
}
case eButton: {
LCDBacklight(1);
return DefaultValue;
}
case eLeft: {
LCDBacklight(1);
if (DefaultValue > MinValue) DefaultValue--;
break;
}
case eRight: {
LCDBacklight(1);
if (DefaultValue < MaxValue) DefaultValue++;
break;
}
}
LCD.setCursor(0, 1);
LCD.print(Blank);
LCD.setCursor(0, 1);
LCD.print(DefaultValue);
}
};
int A = 2, B = 5, C = -3;
void Demo() {
LCD.clear();
LCD.print("It's just a demo");
while (GetEncoderState() == eNone) LCDBacklight();
};
void InputA() {
A = InputValue("Input A", A, -10, 10);
while (A == 0) {
LCD.clear();
LCD.print("Shouldn't be 0!");
LCD.setCursor(0, 1);
LCD.print("Input another value");
while (GetEncoderState() == eNone) LCDBacklight();
A = InputValue("Input A", A, -10, 10);
}
};
void InputB() {
B = InputValue("Input B", B, -10, 10);
};
void InputC() {
C = InputValue("Input C", C, -10, 10);
};
void Solve() {
int D;
float X1, X2;
LCD.clear();
LCD.print(A);
LCD.print("X^2");
if (B >= 0) LCD.print("+");
LCD.print(B);
LCD.print("X");
if (C >= 0) LCD.print("+");
LCD.print(C);
LCD.print("=0");
D = B * B - 4 * A * C;
LCD.setCursor(0, 1);
if (rows > 2) {
LCD.print("D=");
LCD.print(D);
LCD.setCursor(0, 2);
}
if (D == 0) {
X1 = -B / 2 * A;
LCD.print("X1=X2="); LCD.print(X1);
}
else if (D > 0) {
X1 = (-B - sqrt(B * B - 4 * A * C)) / (2 * A);
X2 = (-B + sqrt(B * B - 4 * A * C)) / (2 * A);
LCD.print("X1="); LCD.print(X1);
LCD.print(";X2="); LCD.print(X2);
}
else
LCD.print("Roots are complex");
while (GetEncoderState() == eNone) LCDBacklight();
};
// ******************** Меню ********************
byte ScrollUp[8] = {0x4, 0xa, 0x11, 0x1f};
byte ScrollDown[8] = {0x0, 0x0, 0x0, 0x0, 0x1f, 0x11, 0xa, 0x4};
byte ItemsOnPage = rows; // Максимальное количество элементов для отображения на экране
unsigned long BacklightOffTime = 0;
unsigned long ScrollTime = 0;
byte ScrollPos;
byte CaptionMaxLength;
struct sMenuItem {
eMenuKey Parent; // Ключ родителя
eMenuKey Key; // Ключ
char *Caption; // Название пункта меню
void (*Handler)(); // Обработчик
};
sMenuItem Menu[] = {
{mkNull, mkRoot, "Menu", NULL},
{mkRoot, mkQuad, "Quadratic Equation Calculator", NULL},
{mkQuad, mkQuadSetA, "Enter value A", InputA},
{mkQuad, mkQuadSetB, "Enter value B", InputB},
{mkQuad, mkQuadSetC, "Enter value C", InputC},
{mkQuad, mkQuadCalc, "Solve", Solve},
{mkQuad, mkBack, "Назад", NULL},
{mkRoot, mkMulti, "Multi-level menu example", NULL},
{mkMulti, mkSettings, "Настройки", NULL},
{mkSettings, mkSetMotors, "Motors", NULL},
{mkSetMotors, mkMotorsAuto, "Auto calibration", Demo},
{mkSetMotors, mkMotorsManual, "Manual calibration", Demo},
{mkSetMotors, mkBack, "Назад", NULL},
{mkSettings, mkSetSensors, "Sensors", NULL},
{mkSetSensors, mkSetUltrasonic, "Ultrasonic", Demo},
{mkSetSensors, mkSetLightSensors, "Light sensors", Demo},
{mkSetSensors, mkBack, "Назад", NULL},
{mkSettings, mkSetDefaults, "Restore defaults", Demo},
{mkSettings, mkBack, "Назад", NULL},
{mkMulti, mkBack, "Назад", NULL}
};
const int MenuLength = sizeof(Menu) / sizeof(Menu[0]);
void LCDBacklight(byte v) { // Управление подсветкой
if (v == 0) { // Выключить подсветку
BacklightOffTime = millis();
LCD.noBacklight();
}
else if (v == 1) { //Включить подсветку
BacklightOffTime = millis() + BacklightDelay;
LCD.backlight();
}
else { // Выключить если время вышло
if (BacklightOffTime < millis())
LCD.noBacklight();
else
LCD.backlight();
}
}
eMenuKey DrawMenu(eMenuKey Key) { // Отрисовка указанного уровня меню и навигация по нему
eMenuKey Result;
int k, l, Offset, CursorPos, y;
sMenuItem **SubMenu = NULL;
bool NeedRepaint;
String S;
l = 0;
LCDBacklight(1);
// Запишем в SubMenu элементы подменю
for (byte i = 0; i < MenuLength; i++) {
if (Menu[i].Key == Key) {
k = i;
}
else if (Menu[i].Parent == Key) {
l++;
SubMenu = (sMenuItem**) realloc (SubMenu, l * sizeof(void*));
SubMenu[l - 1] = &Menu[i];
}
}
if (l == 0) { // l==0 - подменю нет
if ((ReturnFromMenu == 0) and (Menu[k].Handler != NULL)) (*Menu[k].Handler)(); // Вызываем обработчик если он есть
LCDBacklight(1);
return Key; // и возвращаем индекс данного пункта меню
}
// Иначе рисуем подменю
CursorPos = 0;
Offset = 0;
ScrollPos = 0;
NeedRepaint = 1;
do {
if (NeedRepaint) {
NeedRepaint = 0;
LCD.clear();
y = 0;
for (int i = Offset; i < min(l, Offset + ItemsOnPage); i++) {
LCD.setCursor(1, y++);
LCD.print(String(SubMenu[i]->Caption).substring(0, CaptionMaxLength));
}
LCD.setCursor(0, CursorPos);
LCD.print(">");
if (ShowScrollBar) {
if (Offset > 0) {
LCD.setCursor(cols - 1, 0);
LCD.write(0);
}
if (Offset + ItemsOnPage < l) {
LCD.setCursor(cols - 1, ItemsOnPage - 1);
LCD.write(1);
}
}
}
EncoderState = GetEncoderState();
switch (EncoderState) {
case eLeft: {
// Прокрутка меню вверх
LCDBacklight(1);
ScrollTime = millis() + ScrollDelay * 5;
if (CursorPos > 0) { // Если есть возможность, поднимаем курсор
if ((ScrollLongCaptions) and (ScrollPos)) {
// Если предыдущий пункт меню прокручивался, то выводим его заново
LCD.setCursor(1, CursorPos);
LCD.print(Blank);
LCD.setCursor(1, CursorPos);
LCD.print(String(SubMenu[Offset + CursorPos]->Caption).substring(0, CaptionMaxLength));
ScrollPos = 0;
}
// Стираем курсор на старом месте, рисуем в новом
LCD.setCursor(0, CursorPos--);
LCD.print(" ");
LCD.setCursor(0, CursorPos);
LCD.print(">");
}
else if (Offset > 0) {
//Курсор уже в крайнем положении. Если есть пункты выше, то перерисовываем меню
Offset--;
NeedRepaint = 1;
}
break;
}
case eRight: {
// Прокрутка меню вниз
LCDBacklight(1);
ScrollTime = millis() + ScrollDelay * 5;
if (CursorPos < min(l, ItemsOnPage) - 1) {// Если есть возможность, то опускаем курсор
if ((ScrollLongCaptions) and (ScrollPos)) {
// Если предыдущий пункт меню прокручивался, то выводим его заново
LCD.setCursor(1, CursorPos);
LCD.print(Blank);
LCD.setCursor(1, CursorPos);
LCD.print(String(SubMenu[Offset + CursorPos]->Caption).substring(0, CaptionMaxLength));
ScrollPos = 0;
}
// Стираем курсор на старом месте, рисуем в новом
LCD.setCursor(0, CursorPos++);
LCD.print(" ");
LCD.setCursor(0, CursorPos);
LCD.print(">");
}
else {
// Курсор уже в крайнем положении. Если есть пункты ниже, то перерисовываем меню
if (Offset + CursorPos + 1 < l) {
Offset++;
NeedRepaint = 1;
}
}
break;
}
case eButton: {
// Выбран элемент меню. Нажатие кнопки Назад обрабатываем отдельно
LCDBacklight(1);
ScrollTime = millis() + ScrollDelay * 5;
if (SubMenu[CursorPos + Offset]->Key == mkBack) {
free(SubMenu);
return mkBack;
}
Result = DrawMenu(SubMenu[CursorPos + Offset]->Key);
if ((Result != mkBack) and (ReturnFromMenu)) {
free(SubMenu);
return Result;
}
NeedRepaint = 1;
break;
}
case eNone: {
if (ScrollLongCaptions) {
// При бездействии прокручиваем длинные названия
S = SubMenu[CursorPos + Offset]->Caption;
if (S.length() > CaptionMaxLength)
{
if (ScrollTime < millis())
{
ScrollPos++;
if (ScrollPos == S.length() - CaptionMaxLength)
ScrollTime = millis() + ScrollDelay * 2; // Небольшая задержка когда вывели все название
else if (ScrollPos > S.length() - CaptionMaxLength)
{
ScrollPos = 0;
ScrollTime = millis() + ScrollDelay * 5; // Задержка перед началом прокрутки
}
else
ScrollTime = millis() + ScrollDelay;
LCD.setCursor(1, CursorPos);
LCD.print(Blank);
LCD.setCursor(1, CursorPos);
LCD.print(S.substring(ScrollPos, ScrollPos + CaptionMaxLength));
}
}
}
LCDBacklight();
}
}
} while (1);
}
//****************************************
void setup() {
pinMode(pin_CLK, INPUT);
pinMode(pin_DT, INPUT);
pinMode(pin_Btn, INPUT_PULLUP);
LCD.init();
LCD.backlight();
CaptionMaxLength = cols - 1;
Blank = (char*) malloc(cols * sizeof(char));
for (byte i = 0; i < CaptionMaxLength; i++)
Blank[i] = ' ';
if (ShowScrollBar) {
CaptionMaxLength--;
LCD.createChar(0, ScrollUp);
LCD.createChar(1, ScrollDown);
}
Blank[CaptionMaxLength] = 0;
}
void loop() {
DrawMenu(mkRoot);
}
// ******************** Энкодер с кнопкой ********************
eEncoderState GetEncoderState() {
// Считываем состояние энкодера
eEncoderState Result = eNone;
CurrentTime = millis();
if (CurrentTime >= (PrevEncoderTime + 5)) {
PrevEncoderTime = CurrentTime;
if (digitalRead(pin_Btn) == LOW ) {
if (ButtonPrev) {
Result = eButton; // Нажата кнопка
ButtonPrev = 0;
}
}
else {
ButtonPrev = 1;
EncoderA = digitalRead(pin_DT);
EncoderB = digitalRead(pin_CLK);
if ((!EncoderA) && (EncoderAPrev)) { // Сигнал A изменился с 1 на 0
if (EncoderB) Result = eRight; // B=1 => энкодер вращается по часовой
else Result = eLeft; // B=0 => энкодер вращается против часовой
}
EncoderAPrev = EncoderA; // запомним текущее состояние
}
}
return Result;
}