четверг, 4 января 2018 г.

Текстовое меню на Ардуино

Текстовое меню для Ардуино на ЖК дисплее и энкодере вращения

В данной публикации я хочу поделиться опытом создания текстового меню на Ардуино. Я буду использовать жидкокристаллический I2C дисплей 20x4 и энкодер вращения KY-040 в качестве элемента управления. О подключении энкодера к Ардуино и подавлении дребезга контактов было написано уже достаточно, с подключением дисплея вопросов тоже быть не должно. Поэтому сегодня я уделю внимание именно программной реализации.

Итак, имеем:
  • Клон Arduino UNO;
  • I2C дисплей 20x4. Для работы с ним необходима библиотека LiquidCrystal_I2C. Думаю, вместо него можно использовать дисплей без I2C интерфейса и библиотеку LiquidCrystal. В этом случае правки скетча если и потребуются, то минимальные;
  • Энкодер вращения с кнопкой. Я использую модуль KY-040.
  • Микросхема MC14490 для подавления дребезга. С ней энкодер работает значительно лучше. Но в принципе можно обойтись и без неё или любого другого аппаратного подавителя дребезга, в этом случае выводы энкодера подключите непосредственно ко входам Ардуино.

Соединяем всё это на макетной плате по следующей схеме:
Текстовое меню на Ардуино. Схема с энкодером вращения KY-040

Мои основные требования к реализации:
  • Возможность удобного описания меню;
  • Возможность построения многоуровнего меню - само собой;
  • Прокрутка длинных пунктов меню;
  • Фишки вроде индикаторов, подсказывающих, что не все пункты уместились на экране или автоотключение подсветки тоже будут кстати.
Сразу приведу пример использования меню, чтобы было проще объяснять его работу. Скачать скетч можно по ссылке.

Обратите внимание! После написания данной статьи я создал библиотеку LiquidCrystal_I2C_Menu. В ней вы найдёте функции для построения меню, ввода и выбора значений и многое другое. 

// ***** I2C дисплей *****
#include <LiquidCrystal_I2C.h> // https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library
#define cols 20
#define rows 4
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, "Back", NULL},
    {mkRoot, mkMulti, "Multi-level menu example", NULL},
      {mkMulti, mkSettings, "Settings", NULL},
        {mkSettings, mkSetMotors, "Motors", NULL},
          {mkSetMotors, mkMotorsAuto, "Auto calibration", Demo},
          {mkSetMotors, mkMotorsManual, "Manual calibration", Demo},
          {mkSetMotors, mkBack, "Back", NULL},
        {mkSettings, mkSetSensors, "Sensors", NULL},
          {mkSetSensors, mkSetUltrasonic, "Ultrasonic", Demo},
          {mkSetSensors, mkSetLightSensors, "Light sensors", Demo},
          {mkSetSensors, mkBack, "Back", NULL},
        {mkSettings, mkSetDefaults, "Restore defaults", Demo},
        {mkSettings, mkBack, "Back", NULL},
      {mkMulti, mkBack, "Back", 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.begin();
  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;
}

Меню - это массив элементов sMenuItem, каждый из которых имеет свой уникальный ключ и ключ родителя для создания иерархии, а также название и ссылку на функцию-обработчик. В качестве ключа я использую перечисления, т.к. они удобнее чем просто числовые значения. Ключ mkBack имеет особое назначение: он нужен для пунктов меню "Back" - возврат на вышестоящий уровень меню.

Реакция на выбор определенного пункта меню может быть построена двумя способами:
  1. Задание функции обработчика. В этом случае параметр ReturnFromMenu должен быть установлен в 0. Если выбранный элемент не содержит дочерних элементов (т.е. это не подменю) и если для него задана функция-обработчик, то она будет вызвана. После выполнения функции управление будет передано обратно в меню.
  2. Анализ значения, возвращаемого функцией DrawMenu. Для этого параметр ReturnFromMenu должен быть установлен в 1. Анализ возвращаемого значения (ключа выбранного элемента меню) легко осуществить при помощи оператора switch.
В данном примере я использую обработчики. Кроме упомянутого параметра ReturnFromMenu в скетче есть и другие: ShowScrollBar, ScrollLongCaptions, ScrollDelay, BacklightDelay. Их назначение понятно из названия.

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

Меню верхнего уровня в данном скетче содержит 2 элемента: первый для решения квадратных уравнений, второй просто как пример построения многоуровнего меню. Параметр ReturnFromMenu установлен в 0 и при выборе пунктов меню вызываются соответствующие обработчики. Так осуществляется задание коэффициентов квадратного уравнения и нахождение его корней (функции InputA, InputB, InputC, Solve). В обработчиках следует помнить об управлении подсветкой дисплея, для этого регулярно вызывается функция LCDBacklight. Функция loop содержит единственный вызов DrawMenu. Вся дальнейшая работа будет заключаться в отрисовке меню и выполнении функций-обработчиков.

Вот в общем-то и всё, остальное должно быть понятно из примера. Ну и в заключение дочитавшим до конца предлагаю видео работы этого меню:


91 комментарий:

  1. Здравствуйте тёзка. Я "полный новичок" в ардуино, самостоятельно накидать менюшку с управлением энкодером, для меня, как-то сложновато. Поиск дал кучу вариантов.
    Но Ваша работа наиболее качественная, и фишки в виде бегущего текста, вообще супер.
    Но, сходу использовать скетч, не прокатило.
    На версии IDE 1.6.7 (Uno R3) заругалась на строку "eMenuKey DrawMenu(eMenuKey Key)" словами "does not name a type". Подскажите, что можно сделать? И ой, чувствую, вопросов будет куча.

    ОтветитьУдалить
    Ответы
    1. Здравствуйте!
      Когда готовил публикацию у меня не было такой ошибки. А сейчас тоже поймал ее. Заметил, что если путь к файлу скетча содержит русские буквы, то ошибка не возникает. Пока не понял с чем это связано, попробую разобраться.
      Вопросу будут - задавайте.

      Удалить
    2. Попробовал оба варианта путей к скетчу, и с "C:\Новая папка\Arduino_KY040_Menu" и просто из корневухи С:\Arduino_KY040_Menu. Не выходит. Выдает целый список "недовольств":

      Arduino: 1.6.7 (Windows 7), Плата:"Arduino/Genuino Uno"
      Arduino_KY040_Menu:192: error: 'eMenuKey' does not name a type

      eMenuKey DrawMenu(eMenuKey Key) { // Отрисовка указанного уровня меню и навигация по нему
      ^
      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      };
      ^
      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      Arduino_KY040_Menu:172: error: invalid conversion from 'void* (*)()' to 'void (*)()' [-fpermissive]

      C:\Users\foto\AppData\Local\Temp\arduino_0e105e996471fbde0dc2a2dfa2384703\Arduino_KY040_Menu.ino: In function 'void LCDBacklight(byte)':

      Arduino_KY040_Menu:175: error: default argument given for parameter 1 of 'void LCDBacklight(byte)' [-fpermissive]

      void LCDBacklight(byte v = 2) { // Управление подсветкой

      ^
      Arduino_KY040_Menu:30: error: after previous specification in 'void LCDBacklight(byte)' [-fpermissive]

      void LCDBacklight(byte v = 2);
      ^
      exit status 1
      'eMenuKey' does not name a type

      Удалить
    3. Я использую версию 1.6.12. Она таких ошибок не выдавала, как и web редактор create.arduino.cc.
      Скачал 1.6.7, немного изменил скетч, скомпилировалось без ошибок. Пробуйте.

      Удалить
  2. По-быстрому спаял прототип KY-040, заменил lcd.begin на lcd.init, и всё заработало!
    Слов нет, одни ассемблер-еденицы в адрес Вашей кармы! Шикарно! Да ещё и с большинством комментариев, да на русском .... мне, как чайнику, просто сказка!
    Но, когда через 20 сек., погас экран, у меня что-то внутри ёкнуло!
    Да Вы, батенька, шутник!
    Такой "спящий режим", для работы от батареек, нужная функция. Она отключаема?
    И ещё момент: подскажите, как можно вставить первой строкой - просто текст (заголовок меню/подменю), и чтобы курсор заголовка не касался? По-простому lcd.print"Заголовок", не прокатило. :)
    Ещё раз - не выражаемая Вам благодарность за труды!

    ОтветитьУдалить
    Ответы
    1. Для управления подсветкой по скетчу разбросаны вызовы LCDBacklight: без параметров функция гасит подсветку через заданный интервал времени (BacklightDelay); параметры 0 и 1 - для выключения и включения подсветки соответственно. Если подсветка не должна отключаться автоматически, то можно просто выкинуть из программы все вызовы LCDBacklight без параметра и явно включать/выключать ее где нужно.

      А чтобы выводился заголовок можно попробовать следующее:
      1. В переменной ItemsOnPage хранится количество пунктов меню, выводимых на экран. По умолчанию ее значение равно количеству строк. Уменьшите ее на единицу, чтобы зарезервировать одну строку для вывода заголовка.
      2. В функции DrawMenu поправить все вызовы lcd.setCursor(x, y): их нужно привести к виду lcd.setCursor(x, y + 1) - чтобы пункты меню и курсор выводились со второй строки. Не обязательно жестко прописывать +1, можно использовать переменную, чтобы была возможность обнулить ее и не резервировать место для заголовка в проектах где он не нужен.
      3. Освободившуюся первую строку теперь можно использовать для вывода заголовка. Делать это лучше внутри условия if (NeedRepaint) {, иначе (если выводить заголовок перед циклом) при возврате из подменю заголовок не обновится. Ну и чтобы не печатать его заново каждый раз при прокрутке меню, когда NeedRepaint присваивается значение 1, я бы ввел еще одно значение - 2. То есть:
      NeedRepaint = 0 - перерисовывать меню/заголовок не требуется;
      NeedRepaint = 1 - нужно прокрутить меню вверх/вниз, заголовок не трогаем;
      NeedRepaint = 2 - используется когда нужно вывести заголовок и меню, а именно после возврата из подменю (в ветке case eButton:).

      Но это всё так, навскидку.
      Разумеется, заголовок прокручиваться не будет, это надо учитывать. Добавить прокрутку можно, но, мне кажется, это перегрузит меню.

      Рад, что моя работа оказалась полезной)

      Удалить
  3. добрый, хочу сделать термостат на вашем меню - подскажите как мне сделать чтоб при бездействии на экран выводились состояние датчиков и выводов. второе не повлечет ли последствия добавление в void loop функций термостатирования?
    зы - у меня не тухнет подсветка через 20 сек...

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Посмотрите видео в этой публикации: https://tsibrov.blogspot.com/2018/06/ad9833.html
      Там на дисплее отображается информация о работе генератора, а вызов меню происходит при нажатии на кнопку. Вам такое же поведение нужно?
      С кодом и по поводу подсветки смогу помочь вечером.

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

      Удалить
    3. Попробуйте для создания меню использовать эту библиотеку: http://clc.la/LiquidCrystal_I2C_Ext
      Как руки дойдут, я допилю ее и напишу статью с описанием библиотеки и ее функций. В архиве есть примеры, разобраться по ним можно. Будут вопросы - пишите.

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

      #include
      LiquidCrystal_I2C_Ext lcd(0x27, 20, 4);

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

      // Перечислим значения, используемые в меню для задания связи родитель-потомок
      enum {mkBack, mkRoot, mkRun, mkOptions, mkMode, mkSpeed, mkAbout};

      // Опишем меню
      sMenuItem menu[] = {
      {mkBack, mkRoot, "Menu demo"},
      {mkRoot, mkRun, "Run"},
      {mkRoot, mkOptions, "Options"},
      {mkOptions, mkMode, "Mode"},
      {mkOptions, mkSpeed, "Speed"},
      {mkOptions, mkBack, "Back"},
      {mkRoot, mkAbout, "About"},
      {mkRoot, mkBack, "Exit menu"}
      };

      // Определим колчество элементов в меню
      uint8_t menuLen = sizeof(menu) / sizeof(sMenuItem);

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

      void loop() {
      uint8_t selectedMenuItem;
      eEncoderState EncoderState = lcd.getEncoderState();
      // Будем показывать меню при нажатии на кнопку
      if (EncoderState == eButton) {
      // Показываем меню
      selectedMenuItem = lcd.showMenu(menu, menuLen, 1);
      // Анализируем возвращенное значение
      if (selectedMenuItem == mkRun)
      lcd.print("Run selected");
      else if (selectedMenuItem == mkMode)
      lcd.print("Mode selected");
      else if (selectedMenuItem == mkSpeed)
      lcd.print("Speed selected");
      else if (selectedMenuItem == mkAbout)
      lcd.print("About selected");
      else if (selectedMenuItem == mkBack)
      lcd.print("Exit selected");

      // После работы с меню нужно перерисовать дисплей (если это не сделано выше внутри IF)
      lcd_repaint();
      }

      // Здесь можно опрашивать датчики, обновлять информацию на дисплее и т.п.
      }


      void lcd_repaint() {
      // Полная перерисовка содержимого дисплея
      }

      Удалить
    5. ну как разобрался... в програмировании я уже давно - наверное уже целых две недели будет....
      как сделать правильно чтоб при бездействии вместе с затуханием подсветки все возвращалось на главный экран?

      Удалить
    6. Напишите мне на VladimirTsibrov@yandex.ru
      Так будет проще

      Удалить
  4. Попытался создать меню на русскоязычном дисплее, но выводиться только половина названий строк, как с этим бороться?

    ОтветитьУдалить
    Ответы
    1. Добрый вечер!
      Русские буквы кодируются 2 байтами, похоже, дело в этом. Попробую разобраться.

      Удалить
    2. Если везде удваивать captionmaxlength то получается что то похожее на правду но хочется разобраться)

      Удалить
    3. Если в названии будут английские буквы, цифры или другие символы, которые занимают 1 байт, то, удвоив captionmaxlength, вы получите строку большей длины. Тут нужно написать свои функции substring и length, заточенные под utf-8. В сети есть примеры для c++, посмотрите их. У меня пока нет возможности поправить код, чтобы он учитывал русские символы, попозже займусь.

      Удалить
    4. Решил проблему отказавшись от liquid...RUS. написал названия меню через коды символов. Спасибо Вам.

      Удалить
  5. Здравствуйте! срочно надо сделать меню на энкодере и дисплее 16 2 за сколько вы можете написать код

    ОтветитьУдалить
    Ответы
    1. Добрый день! Так вот код, уже написан, используйте его. Или библиотеку: https://tsibrov.blogspot.com/2018/10/LiquidCrystalI2CExt.html
      С ней еще проще, примеры есть, функции описаны. Будут конкретные вопросы - задавайте.

      Удалить
  6. Не сайт а находка, столько ценного в одном месте, где же вы раньше были) Какие коррективы надо вводить если энкодер EC-11, а не стандартный ардуиновский, он вроде по полному импульсу считает или нет?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Посмотрел описание EC-11, он по сути такой же, как использованный мной в этом проекте. Есть даже вариант с кнопкой. Поэтому можете смело использовать его. Разница в том, что у меня энкодер распаян на плату и его выводы A и B уже подтянуты к питанию. И в скетче я подтягивал только кнопку:
      pinMode(pin_CLK, INPUT);
      pinMode(pin_DT, INPUT);
      pinMode(pin_Btn, INPUT_PULLUP);
      Поэтому если у вас "голый" энкодер, то его выводы нужно подтянуть самостоятельно - измените в приведенном фрагменте INPUT на INPUT_PULLUP. Ставить внешние резисторы смысла нет. Соответственно, общий провод подключаем к земле. Должно заработать. Разве что направление вращения может определяться некорректно, в этом случае поменяйте выводы энкодера местами (можно в скетче где они объявляются, можно физически).
      И спасибо за хороший отзыв.
      PS Если планируете делать меню на энкодере и подобном ЖК дисплее c I2C интерфейсом, загляните еще в эту публикацию: https://tsibrov.blogspot.com/2018/10/LiquidCrystalI2CExt.html

      Удалить
  7. У вас в ссылке скачать скетч лежит файл от другого проекта.....

    ОтветитьУдалить
    Ответы
    1. Эх... Нехорошо получилось. Спасибо, что написали. Поправил ссылку.

      Удалить
  8. Я могу попросить помощи (можно не безвозмездной) с объединением кода работы с меню со схемой подавления дребезга контактов с помощью SN74HC14N
    Схему собрал. демо скетч работает, теперь нужно что бы меню работало с этой схемой.

    ОтветитьУдалить
    Ответы
    1. Добрый вечер!
      А что сейчас не работает при объединении схемы подавления дребезга и этого кода? Можете написать на VladimirTsibrov@yandex.ru

      Удалить
  9. Доброго времени суток. Огромное спасибо за предоставленную информацию, самому такое сделать еще не по силам. Подскажите пожалуйста, как обрабатывать события при установке курсора на определенный пункт меню? Например в рут меню есть три пункта, курсор на первом пункте - присваиваем переменной какое-то значение, на втором - другое и т.д.
    И еще вопрос такой, как вывести на дисплей русские буквы? Дисплей 1602 не русифицированный, через битовую маску символами выводил нужный текст через lcd.createChar и lcd.Print, а как это сделать в Вашем скетче не знаю.

    ОтветитьУдалить
    Ответы
    1. Добрый вечер.
      В приведенном примере при нажатии на кнопку отрабатывает обработчик нажатия - функция, привязанная к данному пункту меню (Demo, InputA, InputB, InputC, Solve). После выполнения обработчика управление передается обратно в меню.

      Обработку нажатия можно сделать и по-другому:
      1. Установить параметр ReturnFromMenu равным 1 в начале скетча (#define ReturnFromMenu 1). Тогда после нажатия кнопки будет происходить возврат из функции DrawMenu.
      2. В функции loop после вызова DrawMenu анализировать значение, которое вернула данная функция и выполнять нужные действия. Возвращаемые значения будут типа eMenuKey - это перечисление, которое вы должны скорректировать под себя, как и само меню.

      Пример есть в публикации про AD9833 https://tsibrov.blogspot.com/2018/06/ad9833.html, прямая ссылка для скачивания скетча http://clc.la/ad9833_generator

      Русские буквы в меню вывести не получится. Вы по прежнему можете использовать lcd.create и lcd.write (только учтите, что первые 2 символа используются в меню для вывода "полосы прокрутки"). Но названия пунктов меню могут быть только английскими. Дело не в коде, а в дисплее.

      Можете еще попробовать библиотеку для создания меню: https://tsibrov.blogspot.com/2018/10/LiquidCrystalI2CExt.html
      Но я не тестировал ее с дисплеями 1602

      Удалить
  10. Здравствуйте! Я в общем то тоже уже чайник, лет тридцать уже не кодировал, поэтому большое спасибо за тему и за ваш стиль программирования - все понятно и закоментировано. У меня два вопроса:
    1. По фотке с макеткой: вижу конденсатор, но не понятно какой у него номинал.
    2. Скажите, эта микросхема сколько каналов одновременно сможет защитить от дребезга? Извините может вопрос некорректный, но электронщик я вообще никакой.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      1. Емкость нужна малая, я использовал конденсатор 68пФ.
      2. В микросхеме MC14490 6 подавителей дребезга. У меня есть про нее отдельная публикация: https://tsibrov.blogspot.com/2017/11/2-mc14490.html

      Удалить
  11. Добрый день. Вы не могли бы мне помочь, не безвозмездно, я совсем недавно увлекся ардуино и не понимаю многих вещей. Вы могли бы на основе кода меню которое вы написали, объяснить что к чему и помочь сделать так что бы меню вызывалось по кнопке. Мой ветсап 87015194751. Очень надеюсь что вы мне не откажите.

    ОтветитьУдалить
  12. а какт зациклить пункт solve. вставил туда свою программу с использованием переменных input a,b,c
    она выполнгяетс я1 раз и выкидывает назад в предыдущий пункт.а мне нужно чтобы оно работало пока не нажму кнопку на энкодере или прокручу его

    ОтветитьУдалить
  13. Добрый вечер, Владимир.
    Подскажите, пожалуйста, возможно на данном коде сделать вход в меню по удержанию клавиши?

    ОтветитьУдалить
    Ответы
    1. Добрый вечер.
      Возможно, конечно. Нужен лишь сам код для отслеживания удержания кнопки. И когда оно происходит вызывать DrawMenu(mkRoot);

      Удалить
    2. В принципе, разобрался

      Удалить
  14. Здравствуйте, Владимир. Хотелось выразить благодарность за данный код! С библиотекой пока не разобрался, а тут получилось. Переделал управление с энкодера на ИК-пульт, и добавил отдельную кнопку для возврата на предыдущий уровень меню. Хотелось бы сделать поле для ввода значения переменной в функции Input (чтобы можно было использовать ввод цифровыми клавишами). Если есть время и мысли, был бы благодарен за подсказку - в каком направлении двигаться. Спасибо!

    ОтветитьУдалить
    Ответы
    1. Повторно перечитав комментарии нашел ваше сообщение: "Посмотрите видео в этой публикации: https://tsibrov.blogspot.com/2018/06/ad9833.html"
      Как раз там реализовано похожее поле ввода)

      Удалить
    2. Спасибо! Значит не зря старался.

      Удалить
    3. Пока вот что получилось: названия пунктов не менял, вдруг кому пригодится) https://youtu.be/dVq5lA-Hngs

      Удалить
  15. Добрый вечер, Владимир. Благодарю за код меню, мне он очень помог. Один глупый вопрос: можно ли как-то выйти из меню и вернуться в основной цикл при ReturnFromMenu = 0? Новичку вроде меня приходит мысль добавить в одну из вызываемых функций-обработчиков оператор goto c меткой в начало основного цикла, но оказалось так сделать нельзя. Может быть есть какой-то встроенный способ создать пункт "выход из меню"? Просто с функциями-обработчиками очень удобно и просто работать, а при ReturnFromMenu = 1 в switch case не получается грамотно реализовать возврат в нужный пункт меню если это необходимо, чтобы постоянно не заходить в нужную ветку меню заново

    ОтветитьУдалить
    Ответы
    1. Добрый вечер!
      Если нужно решение в лоб для перехода из одной функции к метке в другой, то смотрите в сторону setjmp и longjmp.
      А вообще я бы предложил попробовать мою библиотеку https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu
      В ней можно использовать оба способа обработки выбранного пункта меню: как через обработчики, так и через анализ выбранного пункта в switch case после возврата в вызывающую функцию. При этом не обязательно делать для всех пунктов свои обработчики, можно обойтись одним и внутри него анализировать выбранный пункт. По-моему, получилось удачно. Посмотрите пример такого меню в этом скетче: https://drive.google.com/open?id=1JMZ4eYFq9gewEKC_nozH89yJ13N1kI6u

      Удалить
    2. если принципиально использовать меню из данной публикации, а не библиотеку, то другой вариант - объявить глобальную переменную, устанавливать ее в обработчике и проверять в функции меню после вызова обработчика: флаг установлен? - делаем return из меню. И аналогичную проверку флага в месте, где вызывается подменю, чтобы выход был с учетом вложенности. Более детально можно подумать, если пойдете по этому пути. Но библиотека интереснее.

      Удалить
  16. Добрый день.

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

    У меня 2 вопроса (один вырос из другого):
    1. Как сделать закрытие меню после выполнения функции в подпункте. ReturnFromMenu не подходит из-за его работы во всех пунктах, а мне нужно только в конкретном. Задача: я сделал пункт "Восстановление значений по умолчанию":

    {mkRoot, mkRestDef, "Restore defaults", NULL},
    {mkRestDef, mkBack, "BACK", NULL},
    {mkRestDef, mkRestDefConfirm, "Yes, restore", restore_def},


    void restore_def() {
    for (int i=0; i<4; i++) {
    _pump_temperature[i] = _def_pump_temperature[i];
    EEPROM.write(_eeprom_address[i], _def_pump_temperature[i]);
    }

    lcd.clear();
    lcd.setCursor(4, 0);
    lcd.print("RESTORED");
    lcd.setCursor(1, 1);
    lcd.print("click to close");

    while (GetEncoderState() == eNone) LCDBacklight();
    lcd_baseCover(); // Отрисовка основного экрана, но она не работает,
    }

    … и хотелось бы, чтобы по нажатии/прокручивани энкодера, произошло выпадание из всего меню в основной экран lcd_baseCover(. Сейчас это не работает.

    2. Вопрос связанный с первым – закрытие меню по таймауту. Я пока не понимаю как вообще его закрыть кроме как из самого меню путем выбора – mkBack из mkRoot

    Заранее благодарю.

    ОтветитьУдалить
    Ответы
    1. Сам отвечу на свой вопрос :)
      Пришлось внести в enum eMenuKey {… mkExit, …
      Заменить обработчик с void на – bool (*Handler)();

      Поправить этот кусок:
      if (l == 0) { // l==0 - подменю нет
      if ((ReturnFromMenu == 0) and (Menu[k].Handler != NULL)) {
      if ((*Menu[k].Handler)()) { // проверяем ответ функции, если true, то:
      return mkExit; // возвращаем команду на выход
      }
      }
      LCDBacklight(1);
      return Key; // и возвращаем индекс данного пункта меню
      }

      И в "case eButton:" проверить, если вернулась команда на выход:
      Result = DrawMenu(SubMenu[CursorPos + Offset]->Key);
      if (Result == mkExit or ((Result != mkBack) and (ReturnFromMenu))) {
      free(SubMenu);
      lcd_baseCover(); // стираем меню и перерисовываем главный экран
      return Result;
      }

      mkExit можно использовать вместо mkBack в описании структуры меню (sMenuItem Menu[])– такая конструкция позволит осуществить выход из любого места меню любой вложенности

      Удалить
    2. Добрый день!
      Не успел ответить. Ну и отлично, что все получилось.
      Могу предложить еще посмотреть библиотеку https://github.com/VladimirTsibrov/LiquidCrystal_I2C_Menu
      она основана на данном коде, плюс добавлены другие полезные функции для организации ввода и выбора значений.
      "ReturnFromMenu не подходит из-за его работы во всех пунктах" - в меню из этой библиотеки можно использовать как обработчики, так и возврат выбранного пункта. Вам бы это как раз подошло.

      Удалить
  17. Добрый день, а что нужно поменять в скетче для дисплея без I2C ?

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Не работал с дисплеем без I2C, поэтому точно не скажу. Как минимум нужно заменить библиотеку в скетче на LiquidCrystal, изменить объявление объекта lcd - смотрите примеры, идущие с библиотекой, и перенести в вызов lcd.begin указание количества строк и столбцов. Остальное по идее должно работать.

      Удалить
    2. И для платы Arduino Mega 2560 не компилируется.

      exit status 1
      Ошибка компиляции для платы Arduino/Genuino Mega or Mega 2560.

      Удалить
    3. Спасибо, с не I2C дисплеем разобрался.
      Если кому нужно :
      // ***** Дисплей *****
      #include
      #define cols 20
      #define rows 4
      // initialize the library by associating any needed LCD interface pin
      // with the arduino pin number it is connected to
      const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
      LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
      char *Blank;

      и далее, где begin:
      // устанавливаем размер (количество столбцов и строк) экрана
      lcd.begin(20, 4);
      у меня экран 20х4

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

    ОтветитьУдалить
    Ответы


    1. E:\text_menu\text_menu.ino: In function 'void InputA()':

      E:\text_menu\text_menu.ino:84:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      A = InputValue("Input A", A, -10, 10);

      ^

      E:\text_menu\text_menu.ino:91:41: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      A = InputValue("Input A", A, -10, 10);

      ^

      E:\text_menu\text_menu.ino: In function 'void InputB()':

      E:\text_menu\text_menu.ino:96:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      B = InputValue("Input B", B, -10, 10);

      ^

      E:\text_menu\text_menu.ino: In function 'void InputC()':

      E:\text_menu\text_menu.ino:100:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      C = InputValue("Input C", C, -10, 10);

      ^

      E:\text_menu\text_menu.ino: At global scope:

      E:\text_menu\text_menu.ino:175:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      };

      ^

      E:\text_menu\text_menu.ino:175:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\text_menu.ino:175:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      C:\Users\Sergey\AppData\Local\Temp\ccu7Oj7Z.ltrans0.ltrans.o: In function `InputValue(char*, int, int, int) [clone .constprop.7]':

      ccu7Oj7Z.ltrans0.o:(.text+0x8dc): undefined reference to `LCDBacklight(unsigned char)'

      ccu7Oj7Z.ltrans0.o:(.text+0x8f0): undefined reference to `LCDBacklight(unsigned char)'

      ccu7Oj7Z.ltrans0.o:(.text+0x902): undefined reference to `LCDBacklight(unsigned char)'

      ccu7Oj7Z.ltrans0.o:(.text+0x92c): undefined reference to `LCDBacklight(unsigned char)'

      ccu7Oj7Z.ltrans0.o:(.text+0x9ba): undefined reference to `LCDBacklight(unsigned char)'

      C:\Users\Sergey\AppData\Local\Temp\ccu7Oj7Z.ltrans0.ltrans.o:ccu7Oj7Z.ltrans0.o:(.text+0x9d6): more undefined references to `LCDBacklight(unsigned char)' follow

      collect2.exe: error: ld returned 1 exit status

      exit status 1
      Ошибка компиляции для платы Arduino/Genuino Mega or Mega 2560.

      Удалить
    2. Я так понимаю ему LCDBacklight не нравится?

      Удалить
    3. Похоже на то. Закомментируйте вызовы или добавьте в библиотеку свою функцию управления подсветкой, как это сделано в LiquidCrystal_I2C

      Удалить
    4. А вот это что такое?

      E:\text_menu\text_menu.ino:91:41: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      A = InputValue("Input A", A, -10, 10);

      Удалить
  19. Мне управление подсветкой не нужно, пусть горит.
    Попробую.

    ОтветитьУдалить
  20. Backlight я выкорчевал, но сейчас в обоих вариантах и и2с и без, выдает одинаковую ошибку.

    E:\text_menu\text_menu.ino: In function 'void InputA()':

    E:\text_menu\text_menu.ino:65:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

    A = InputValue("Input A", A, -10, 10);




    E:\text_menu\menu_I2C\menu_I2C.ino: In function 'void InputA()':

    E:\text_menu\menu_I2C\menu_I2C.ino:81:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

    A = InputValue("Input A", A, -10, 10);

    ОтветитьУдалить
    Ответы
    1. Вот полный лог:

      E:\text_menu\menu_I2C\menu_I2C.ino: In function 'void InputA()':

      E:\text_menu\menu_I2C\menu_I2C.ino:81:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      A = InputValue("Input A", A, -10, 10);

      ^

      E:\text_menu\menu_I2C\menu_I2C.ino:88:41: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      A = InputValue("Input A", A, -10, 10);

      ^

      E:\text_menu\menu_I2C\menu_I2C.ino: In function 'void InputB()':

      E:\text_menu\menu_I2C\menu_I2C.ino:93:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      B = InputValue("Input B", B, -10, 10);

      ^

      E:\text_menu\menu_I2C\menu_I2C.ino: In function 'void InputC()':

      E:\text_menu\menu_I2C\menu_I2C.ino:97:39: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      C = InputValue("Input C", C, -10, 10);

      ^

      E:\text_menu\menu_I2C\menu_I2C.ino: At global scope:

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      };

      ^

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      E:\text_menu\menu_I2C\menu_I2C.ino:172:1: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      Скетч использует 10182 байт (4%) памяти устройства. Всего доступно 253952 байт.
      Глобальные переменные используют 796 байт (9%) динамической памяти, оставляя 7396 байт для локальных переменных. Максимум: 8192 байт.

      Удалить
  21. Ура! Заработало!
    У меня как оказалось энкодер глючил, поменял и все заработало.
    Моя конфигурация: MEGA 2560, экран 20х4 без I2C

    ОтветитьУдалить
  22. Спасибо большое!
    Теперь буду пилить меню под свой проект.

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

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

    ОтветитьУдалить
    Ответы
    1. Опишите подробней. Не понимаю, что значит не вбиваются.

      Удалить
  24. когда кручу энкодер значение не меняется.

    ОтветитьУдалить
    Ответы
    1. Видимо, что-то задели, когда правили функцию. Могу посмотреть, если пришлете код

      Удалить
    2. напишите вот сюда : mr.dimm@ya.ru, я отвечу.
      а то у меня там уже много.

      Удалить
  25. Добрый день! Не могли бы вы подсказать как снимать постоянное значение с переменных A B С. Спасибо.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Не понимаю что значит "снимать постоянное значение с переменных". Что вы хотите сделать?

      Удалить
  26. здравствуйте, понимаю что времени у вас очень мало, но не могли бы помочь, я в вашем меню в часть программы под названием "Solve" поставил обычную моргалку встроенным светодиодом,но оно как бы не работает, точнее работает но частично, моргнет раз и вылетает, потом снова нажимаешь и снова все повторяется, так вот, не подскажите как сделать чтобы этот цикл повторялся пока не нажмешь кнопку энкодера?ну или вращение, без разницы, главное чтобы не вылетала сразу программа...(кстати, в вашем скетче где ссылка на пример, у меня такая же история)

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Советую присмотреться к этой библиотеке: https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html
      - в ней есть функции для построения меню, ввода и выбора значений из списка, форматированного вывода, и многое другое. Попробуйте сначала её. С библиотекой идут примеры использования, плюс в статье всё расписано.

      Если всё же хотите использовать этот скетч, то вам нужно в функции solve сделать цикл. типа такого:
      void Solve() {
      while (GetEncoderState() == eNone) {
      digitalWrite(LED_BUILTIN, HIGH);
      delay(1000);
      digitalWrite(LED_BUILTIN, LOW);
      delay(1000);
      }
      }

      Удалить
    2. да, заработало!!!!спасибо огромное!! и за библиотеку спасибо! с ней намного проще!

      Удалить
    3. а можно еще вопрос? можно ли тут сделать, чтобы добавлялось например сразу по 10 единиц?

      Удалить
    4. Если вы про функцию inputVal библиотеки LiquidCrystal_I2C_Menu, то можно. Шаг приращения определяется параметром step, по умолчанию шаг = 1. Посмотрите описание этой функции в статье или в примере inputVal.ino

      Удалить
    5. Все, разобрался! Большое спасибо!

      Удалить
  27. Здравствуйте. Подскажите, что нужно сделать, чтобы енкодер срабатывал по каждому щелчку. У меня, почему-то реагирует только после второго. И кнопка енкодера срабатывает только после длинного нажатия. Спасибо.

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

      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 => энкодер вращается против часовой
      }
      else if ((EncoderA) && (!EncoderAPrev)) { // Сигнал A изменился с 0 на 1
      if (EncoderB) Result = eLeft;
      else Result = eRight;
      }
      EncoderAPrev = EncoderA; // запомним текущее состояние
      }
      }
      return Result;
      }

      Вставьте эту функцию взамен текущей, в конце скетча.
      Предлагаю ещё заглянуть в эту публикацию: https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html
      В ней много полезных функций. Но функцию опроса энкодера придется тоже подправить под ваш энкодер.

      Удалить
  28. Доброго времени суток! Меню сногсшибательное! Подскажите, пожалуйста, как снять значения переменных A,B,C? То есть я задал значения в Input A=3 допустим. Теперь мне нужно подать это число "3" на таймер, чтобы мой реле работал по таймеру. Пытался вытащить в монитор порта, но не получается вывести эти данные туда.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      А что именно не получается? Если вы ввели значения в переменные A, B, C, то потом можно их использовать, выводить в Serial и т.д.
      A = InputValue("Input A", A, -10, 10);
      Serial.println(A);

      Удалить
    2. спасибо за ответ. я тоже выводил в сериал, но ничего не выводит. там пусто. подскажите, куда нужно вставить строку Serial.println(A);? я пробовал в коней, в loop, в внутри скобок в условиях if. не получается вывести(

      Удалить
    3. вот так тоже ничего не выводит:
      void InputA() {
      A = InputValue("Input A", A, 1, 10);
      Serial.println(A);
      };

      Просто вот это значение А мне нужно использовать как значение для установки времени и периода для РЕЛЕ. Помогите пожалуйста.

      Удалить
    4. Не мог ответить раньше.
      Должно работать. Что-то вы делаете не так.
      Посмотрите вот эту библиотеку: https://tsibrov.blogspot.com/2020/09/LiquidCrystal-I2C-Menu.html
      Она основана на этом скетче, но удобнее в использовании и есть много других полезных функций. В том числе для ввода значений. С библиотекой идут примеры. Попробуйте.

      Удалить
  29. ESP32 версия Arduino IDE 1.8.15
    При компиляции получаю ошибку "no matching function for call to 'min(int&, byte&)'" пока не разобрался почему.

    ОтветитьУдалить
  30. Пришлось привести типы переменных к одному виду:
    if (CursorPos < min(l, int(ItemsOnPage) - 1)) {

    Меню просто огонь!

    ОтветитьУдалить
    Ответы
    1. Помнится, я тоже сталкивался с такой ошибкой на какой-то платформе. Я тогда объявил свой макрос min:
      #define min(a,b) ((a)<(b)?(a):(b))
      в таком виде он объявляется в файле Arduino.h

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

    ОтветитьУдалить
  32. Доброго времени суток. Меню действительно хорошее.
    Но понимаю, что без помощи автора мне не осилить.
    Перевел функцию энкодера на аналоговую клавиатуру.
    // ******************** Кнопри на аналоговом входе ********************
    eEncoderState GetEncoderState() {
    // Считываем состояние энкодера
    eEncoderState Result = eNone;
    CurrentTime = millis();
    //if (CurrentTime >= (PrevEncoderTime + 5)) {
    // PrevEncoderTime = CurrentTime;
    data = analogRead (pin_Btn);
    if (abs(data-data_old) > 50){
    data_old = data ;
    if (data < 950) {
    if (data < 60) Result = eButton; // Нажата кнопка
    else if (data < 200) Result = eRight;
    else if (data < 400) Result = eLeft;
    else if (data < 600) Result = eBack;
    else if (data < 800) Result = eSelect;
    }
    else Result = eNone;
    }
    return Result;
    }
    Но так и не смог прописать кнопку eBack и выход на уровень выше по таймауту, вплоть до полного выхода из меню. Помогите пожалуйста.

    ОтветитьУдалить
  33. Уважаемый автор, спасибо за отличный скрипт! Вопрос такой: Кроме прорисовки меню может ли что-то ещё быть в loop() ? Если я вставляю другую функцию в loop() до меню, то не отображается меню, а если после меню - не работает вставленная функция. Пробовал таймеры и прерывания с миллис, не помогает

    ОтветитьУдалить