Покритикуйте функцию меню для LCD1602

Гоните в шею помощника, который гадит в код!

И вообще, не привыкайте перекладывать ответственность на кого-то, код Ваш - значит Вам определять какие в нём комментарии и Вам за них отвечать.

3 лайка

При правильной организации списка нумерация в принципе не нужна

А если средствами ООП меню решать, то и списки не нужны.

согласен послушать ваши предложения и готов переписать под любой вариант.

я пытался написать такую функцию,которая не зависела бы от вложений . Пишешь только список ,как оглавление, а в функцию лезть не надо.

Дофига придется слушать. Я не готов столько говорить. Пересказывать книжки не мой конёк. Вот готовое примитивное меню. Я на нем постигал основы ООП, пока учил язык. Динамическое меню, то-есть контекстно-зависимое. Есть и серьезный косяк. Мне там нужен был вектор, и я ничего умнее на тот момент не придумал, чем использовать QList.
Кстати, итератор - экземпляр этого же класса.

#include "QList.h"

class Item {
  public:
    Item(Item* parent= nullptr) {_parent = parent;}
    const char* _name {"Main"};                     // name of current menu item
    Item* _parent = nullptr;                        // parents pointer. Nullptr for root
    uint8_t _retFrom {0};                           // number of child-item, from witch was "go to parent" executed. first (0) by default
    QList <Item*> child;                            // children refs list 
    size_t childCount {0};                          // children qty
    uint8_t _nrAtParentsList {0};                   // items number at parent's child-list
    Item* newChild(const char* nme, void (*ptr)()); // create a new child end-point with action
    void (*actionF)() {nullptr};                    // pointer to executable routine   
  private:
};


Item* Item::newChild(const char* nme, void (*ptrA)() = nullptr) {
  Item* p = new(Item);
  p->_name = nme;
  p->_parent = this;
  p->_nrAtParentsList = childCount++;
  p->actionF = ptrA;
  child.push_back(p);
  return p;
}

Как видишь, код не сильно длинный

И да, я знаю, на деструкторы я забил))

так в библиотеке более 1000 строк кода… :frowning_face: ее ж запихать нужно , а память она не бесконечная…

Я и говорю, это косяк. Более того, в 2.ХХ она не компилируется больше. Но суть та же. Вектор свой можешь написать. Мне без надобности, у меня нигде физическое управление не используется, только mqtt

И да, поржал с 1000 строк и объема памяти)) Ты прям так и думаешь, что там вся программа так записывается? Прям СЛОВАМИ? )))))))))

Напиши вектор на 100 строк кода

Сильно не ругайте. :worried:

#include "esp_task_wdt.h"
#include <HardwareSerial.h>
#include "ESP32Servo.h"
#include "WiFi.h"         //   библиотека подключения к сети wi-fi
//#include <PubSubClient.h> //   Библиотека работы по протоколу mqtt

//============Плейер===============================================================================
#include <DFMiniMp3.h> // Библиотека работы с мелодиями dfplayer
class Mp3Notify; 
typedef DFMiniMp3<HardwareSerial, Mp3Notify> DfMp3; // define a handy type using serial and our notify class
DfMp3 dfmp3(Serial1);// instance a DfMp3 object, 
class Mp3Notify
{
public:
  static void PrintlnSourceAction(DfMp3_PlaySources source, const char* action)
  {
    if (source & DfMp3_PlaySources_Sd) 
    {
        Serial.print("SD Card, ");
    }
    if (source & DfMp3_PlaySources_Usb) 
    {
        Serial.print("USB Disk, ");
    }
    if (source & DfMp3_PlaySources_Flash) 
    {
        Serial.print("Flash, ");
    }
    Serial.println(action);
  }
  static void OnError([[maybe_unused]] DfMp3& mp3, uint16_t errorCode)
  {
    // see DfMp3_Error for code meaning
    Serial.println();
    Serial.print("Com Error ");
    Serial.println(errorCode);
  }
  static void OnPlayFinished([[maybe_unused]] DfMp3& mp3, [[maybe_unused]] DfMp3_PlaySources source, uint16_t track)
  {
    Serial.print("Play finished for #");
    Serial.println(track);  
  }
  static void OnPlaySourceOnline([[maybe_unused]] DfMp3& mp3, DfMp3_PlaySources source)
  {
    PrintlnSourceAction(source, "online");
  }
  static void OnPlaySourceInserted([[maybe_unused]] DfMp3& mp3, DfMp3_PlaySources source)
  {
    PrintlnSourceAction(source, "inserted");
  }
  static void OnPlaySourceRemoved([[maybe_unused]] DfMp3& mp3, DfMp3_PlaySources source)
  {
    PrintlnSourceAction(source, "removed");
  }
};


// Подключаем библиотеку русских сиволов
#define _LCD_TYPE 1       // Тип подключения дисплея: 1 - по шине I2C, 2 - десятиконтактное. Обязательно указывать ДО подключения библиотеки LCD_1602_RUS_ALL.h
#include "LiquidCrystal_I2C.h" 
#include <LCD_1602_RUS_ALL.h>
LCD_1602_RUS lcd(0x3F, 16, 2);

//========= НАСТРОЙКИ =============================================================================
//======= адресная лента ==========================================================================
#include <Adafruit_NeoPixel.h>    //  Библиотека адресной ленты
#define PIN_WS2812B	19            //  Пин подключения адресной ленты
#define NUM_PIXELS  5             //  Колличество светодиодов в адресной ленте WS2812b, равно колличеству рюмок
Adafruit_NeoPixel ws2812b(NUM_PIXELS, PIN_WS2812B, NEO_GRB + NEO_KHZ800); //  Создаем объект адресной ленты

//======= Wi-Fi и MQTT брокер =====================================================================
const char* ssid = "##########";       //  имя сети wi-fi
const char* password =  "#########";  //  пароль сети wi-fi

// MQTT данные с wqtt.ru
// const char* mqtt_server = "m8.wqtt.ru";
// const int mqtt_port = 18170;
// const char* mqtt_user = "#######";
// const char* mqtt_password = "######";

// WiFiClient espClient;
// PubSubClient client(espClient);

// Рэле включения от алисы
const String relay_topic = "drink";
const int RELAY = 18;             //  Временно выводим состояние mqtt брокера , потом нужно буде запускать процедуру по приходящим данным
bool relay_on = false;

#define BUSY_PIN 23               //  пин готовности DF плеера низким, когда играет песня, и высоким, когда ничего не играет, не подключать на gpio12
Servo myservo;                    //  create servo object to control a servo
uint32_t servo_time_work  = 1000; //  Время задержки перед включением помпы необходимое для поворота сервы
#define SERVO_ATTACH  13          //  attaches the servo on pin 13 to the servo object
#define SHORT_PRESS_TIME 1000     //  1000 milliseconds
#define LONG_PRESS_TIME 1000      //  1000 milliseconds
#define SW_PIN 15                 //  пин GPIO15 ESP32 назначаем для кнопки SW энкодера
#define DT_PIN 2                  //  пин GPIO2 ESP32 назначаем для DT выхода энкодера
#define CLK_PIN 4                 //  пин GPIO4 ESP32 назначаем для CLK выхода энкодера
#define PUMP_PIN 14               //  Пин GPIO14 ESP32 назначаем для помпы. 
uint32_t  pump_time_work  = 3000; //  Время работы помпы
const uint8_t SW_pins[] = {32, 33, 25, 26, 27};   //  пины концевиков ESP32 для 5 стопок
const uint8_t Pos[] = {15, 45, 90, 130, 170, 0};  //  массив,в котором хранятся позиции сервы, в последней переменной хранится позиция парковки
//#define  SIZE_PRIMARY_MENU  4     //  Указать колличество пунктов меню считая с "0"
//static PROGMEM const char   так надо записать - помещает переменную во флешпамять
// static PROGMEM const char* menu_bar[] = {
//   "      МЕНЮ      ",
//   ">   Промывка   <",
//   ">    Налив     <",
//   ">   Настройка  <",
//   ">    Выход     <"
// };
//uint8_t currentMenuItem = 0;      //  Текущий пункт меню

#define T_FOLDER         0 //   Тип меню - папка
#define T_APP            1 //   Функция для выполнения кода
struct menuStruct {               // создаём ярлык структуры меню
	uint8_t type;   //  тип меню папка или функция , если функция , то mode1 - номер функции, mode2 - параметры функций
	uint8_t parent; //  номер родителя, верхний уровень 0. 
	uint8_t mode1;  //  стартовый номер дочернего пункта меню в массиве MenuTable
	uint8_t mode2;  //  колличество  пунктов меню
	char name[32];  //  название пункта меню
};
//массив структуры меню
static PROGMEM  menuStruct MenuTable[] = {
	T_FOLDER,     0, 1, 3,"      МЕНЮ      ", //  Меню 0 Это MenuLevel
		T_FOLDER,   0, 2, 3,">   Промывка   <", // 1
			T_APP,    1, 2, 3,">Старт промывки<", // 2
			T_APP,    1, 2, 3,"> Стоп промывки<", // 3
      T_APP,    1, 2, 3,">    Выход     <", // 4
		T_FOLDER,   0, 6, 5,">   Настройка  <", // 5
			T_FOLDER, 5, 6, 2,">   Громкость  <",
				T_APP,  6, 6, 2,">Уровень умолч.<",
				T_APP,  6, 6, 2,">    Выход     <",
			T_FOLDER, 5, 10,3,">    Налив     <",
				T_APP,  6, 1, 3,    "Уровень",
				T_APP,  6, 2, 3,    "Проверка",
				T_APP,  6, 3, 3,    "Выход",
			T_FOLDER, 5, 14,3,">  Тип тостов  <",
				T_APP,  6, 1, 3,    "Случайно",
				T_APP,  6, 2, 3,    "По порядку",
				T_APP,  6, 3, 3,    "Выход",
			T_FOLDER, 5, 18,3,">  Тип налива  <",
				T_APP,  6, 1, 1,    "Стандартный",
				T_APP,  6, 2, 1,    "Игра",
				T_APP,  6, 3, 1,">    Выход     <",
			T_APP,    5, 1, 1,">    Выход     <",
		T_APP,      0, 2, 1,">    Выход     <"
	};

uint8_t menuLevel = 0;  //  Текущий уровень меню
//======= Таски ===================================================================================
//TaskHandle_t TaskVolume;  //   Задача регулирования грамкости

//======= Энкодер =================================================================================
// Обязательно на сигнальные провода энкодера вешать керамические конденсаторы 0.1мкф
#include "ESP32Encoder.h"
#include <ezButton.h>           // Библиотека для использования SW  пина энкодера
bool isPressing = false;
bool isLongDetected = false;
unsigned long pressedTime  = 0; //  Время нажатия на кнопку энкодера
unsigned long releasedTime = 0; //  Время отпускания кнопки энкодера
ezButton button(SW_PIN);        //  создаем обьект ezButton на 15 пине;
// Обработчик прерываний
static IRAM_ATTR void enc_cb(void* arg) {
  ESP32Encoder* enc = (ESP32Encoder*) arg;
}
ESP32Encoder encoder(true, enc_cb);

//============Таймер===============================================================================
//создадим указатель на переменную с именем encButton_timer и типом type hw_timer_t для того чтобы в последующем производить настройку таймера.
hw_timer_t *encButton_timer = NULL; // обьявляем переменную таймера

//======= Функция подключения к MQTT брокеру ======================================================
// void reconnect() {
//   while (!client.connected()) {
//     Serial.print("Попытка подключения по протоколу MQTT...");
//     String clientId = "ESP32-" + WiFi.macAddress();
//     if (client.connect(clientId.c_str(), mqtt_user, mqtt_password) ) {
//       Serial.println("Подключено");
      
//       client.subscribe( (relay_topic + "/#").c_str() ); // подписка на определенный топик

//     } else {
//       Serial.print("ошибка, rc=");
//       Serial.print(client.state());
//       Serial.println(" повторим попытку через 5 секунд");
//       delay(5000);
//     }
//   }
// }
//======= Функция светомозыки =====================================================================

//======= Функция светомозыки =====================================================================
void CvetoMuzik() {
  while (!digitalRead(BUSY_PIN)){   // Пока проигрывает музыка выполняем 
  // Rainbow cycle along whole ws2812b. Pass delay time (in ms) between frames.
  // Hue of first pixel runs 3 complete loops through the color wheel.
  // Color wheel has a range of 65536 but it's OK if we roll over, so
  // just count from 0 to 3*65536. Adding 256 to firstPixelHue each time
  // means we'll make 3*65536/256 = 768 passes through this outer loop:
    for(long firstPixelHue = 0; firstPixelHue < 3*65536; firstPixelHue += 256) {
      for(int i=0; i<ws2812b.numPixels(); i++) { // For each pixel in ws2812b...
        // Offset pixel hue by an amount to make one full revolution of the
        // color wheel (range of 65536) along the length of the ws2812b
        // (ws2812b.numPixels() steps):
        int pixelHue = firstPixelHue + (i * 65536L / ws2812b.numPixels());
        // ws2812b.ColorHSV() can take 1 or 3 arguments: a hue (0 to 65535) or
        // optionally add saturation and value (brightness) (each 0 to 255).
        // Here we're using just the single-argument hue variant. The result
        // is passed through ws2812b.gamma32() to provide 'truer' colors
        // before assigning to each pixel:
        ws2812b.setPixelColor(i, ws2812b.gamma32(ws2812b.ColorHSV(pixelHue)));
      }
      ws2812b.show(); // Update ws2812b with new contents
      delay(1);  // Pause for a moment
    }
  }
}

//======= Функция налива ==========================================================================
void naliv() {

  //периодический вызов функции df mp3.loop() 
  // позволяет обрабатывать уведомления без прерываний
  //dfmp3.loop();
  lcd.clear();
  lcd.setCursor(3, 0);              // Устанавливает курсор в (позицию,Строка)
  lcd.print("Наливаем в"); 
  lcd.setCursor(4, 1);              // Устанавливает курсор в (позицию,Строка) 
  lcd.print("Рюмку"); 
  for (int i = 0; i < 5; i++) {     // опрашиваем концевики
    if(!digitalRead(SW_pins[i])){
      lcd.setCursor(10, 1);         // Устанавливает курсор в (Позиция,Строка) 
      lcd.print(i+1, DEC);          // Печатаем номер рюмки в которую льем
      myservo.write(Pos[i]);        //  Предвигаем серву в позицию " i "
      delay(servo_time_work);       //  Время задержки перед включением помпы
      digitalWrite(PUMP_PIN, HIGH); // ВКЛЮЧАЕМ помпу
      delay(pump_time_work);        // время налива 
      digitalWrite(PUMP_PIN, LOW);  // ВыКЛЮЧАЕМ помпу
      delay(1000);
    }  
  }
  myservo.write(Pos[5]);            //  возвращаем серву в парковочную позицию
  lcd.clear(); 
  lcd.setCursor(5, 0);
  lcd.print("Налито");              
  lcd.setCursor(3, 1);
  lcd.print("Тост пошел");          
  dfmp3.nextTrack();
  Serial.println(millis());
  delay(700);                 //  Обязательная задержка для опроса мр3 плейера                       
  CvetoMuzik();               //  воспроизводим светомузыку пока идет тост
  lcd.setCursor(3, 1);
  lcd.print("Приступаем");          


}

// void updateStatePins(void){ // временная процедура включения реле
//     if(relay_on){
//       digitalWrite(RELAY, HIGH);
//       Serial.println("вкл алиса");
//       naliv();
//     }else{
//       digitalWrite(RELAY, LOW);
//        Serial.println("алиса выключила лампу!");
//     }

// }

//======= функция обработки которая вызывается в client.loop ======================================
// void callback(char* topic, byte* payload, unsigned int length) {
    
//   String data_pay;
//   for (int i = 0; i < length; i++) {
//     data_pay += String((char)payload[i]);
//   }

//     Serial.println(data_pay);
    
//   if( String(topic) == relay_topic ){
//         if(data_pay == "ON" || data_pay == "1") relay_on = true;
//         if(data_pay == "OFF" || data_pay == "0") relay_on = false;
//     }

//     updateStatePins();
// }

//  Функция вывод на экран уровня громкости
void  volum_level(bool step){
  int i=0;
  lcd.clear(); 
  lcd.setCursor(0, 0);
  lcd.print("Громкость");
  lcd.setCursor(10, 0);  
  int volume  = dfmp3.getVolume();
  if (step) {
    if (volume==30) lcd.print("МАКС");
      else{
        dfmp3.setVolume(volume+1); //  Увеличивавем громкость динамика
        lcd.print(volume+1, DEC);  //  Выводит на экран переменную в десятичном формате
      }
  }
  else if (volume == 0) lcd.print("ВЫКЛ");  //  Выводит на экран символы цифр;
    else {
      dfmp3.setVolume(volume-1); //  Уменьшаем громкость динамика         
      lcd.print(volume-1, DEC);  //  Выводит на экран переменную в десятичном формате
    }
  for ( i = 0; i < ceil( volume/2 ); i++) // округление до большего числа вверх
  //for ( i = 0; i < round( volume/2 ); i++)
  {
    lcd.setCursor(i,1);
    lcd.write(255); //  Выводит на экран символы по коду  
  }
}

//  Функция опроса энкодера
int opros_encoder(){ 
  int8_t otvet = 0;
  button.loop();  // опрос кнопки энкодера
  if(button.isPressed()){  // Кнопка энкодера нажата
    pressedTime = millis();
    isPressing = true;
    isLongDetected = false;
    }     
  if(button.isReleased()){  // Кнопка энкодера отпущена
    isPressing = false;
    releasedTime = millis();
    long pressDuration = releasedTime - pressedTime;
    if ( pressDuration < SHORT_PRESS_TIME ){
      //Serial.println("Короткое нажатие зафиксировано"); 
      otvet = 1;
    }
  }
  if (isPressing == true && isLongDetected == false) {
    long pressDuration = millis() - pressedTime;
    if ( pressDuration > LONG_PRESS_TIME ) {
      //Serial.println("Длинное нажатие определено");
      isLongDetected = true;
      otvet = 2;
    }
  }
  delay(10);
  int i=encoder.getCount();
  if (i>0){  //  Вращение вправо
    //Serial.println("Повернули вправо");
    otvet = 3;
  }
  else if (i<0){ //Вращение влево
   // Serial.println("Повернули влево");
    otvet = 4;
  }
  encoder.clearCount();
  //encoder.setCount(0);  //  обнуляем энкодер
  return(otvet);
}

//======= Промывка системы ========================================================================
// Функция запускает насос на пробный налив, итоговый результат налива неопределен
// в рюмку может быть налито сколько угодно
void flushSystem(){
  myservo.write(Pos[0]);          //  Предвигаем серву в позицию " 0 "
  if(digitalRead(SW_pins[0])){    //  опрашиваем нулевой концевик
    delay(servo_time_work);       //  Время задержки перед включением помпы
    digitalWrite(PUMP_PIN, HIGH); //  ВКЛЮЧАЕМ помпу
    delay(pump_time_work);        //  время налива 
    digitalWrite(PUMP_PIN, LOW);  //  ВыКЛЮЧАЕМ помпу
  }
  return;                         //  Если рюмка не стоит или пролили систему, то выходим
  // опросить концевики
  // если нет рюмки
  // выйти из промывки и сообщить , что нужна тара
  // если рюмка стоит, то налить 50- мл.
  // запустить какое нибудь прикольное сообщение
}
//======= Аварийная остановка промывки ============================================================
void stopFlushSystem(){ 
  digitalWrite(PUMP_PIN, LOW);  //  ВыКЛЮЧАЕМ помпу
  return;
}
//======= Выход из меню ===========================================================================
void menuExit(){
  menuLevel = MenuTable[menuLevel].parent; //  Устанавливает номер родительского меню в массиве меню;
}
//======= Выполнение команды меню =================================================================
void menuCommand(int command){
  switch (command)
  {
  case 2: // Промывка
    flushSystem(); 
    break;
  case 3: // Аварийная остановка промывки
    stopFlushSystem(); 
    break;
  case 4: // Выход из меню
    menuExit();
    break;  
  // default:
  //   break;
  }
  return;
}

//======= Функция обработки меню ==================================================================
void menuEnter(int command){            //  command - это номер родителя меню
  menuLevel = MenuTable[command].mode1; //  Устанавливает номер дочернего меню в массиве меню
  int i =0;
  int timeavtoexitstart = millis();
  lcd.clear() ;
  u_int8_t currentMenuItem = 1; //  Устанавливает номер текущего пункта меню
  while ((millis() - timeavtoexitstart < 60000) || i==2) //  автовыход из пунктов меню через 1 минуту
    {
      lcd.setCursor(0, 0);    // Устанавливает курсор в (позицию,Строка) 
      lcd.print(MenuTable[command].name);
      lcd.setCursor(0, 1);  // Устанавливает курсор в (позицию,Строка)
      lcd.print(MenuTable[menuLevel].name); 
      i = opros_encoder();   
      switch (i) {
        case 0:
        // выполнить, если значение == 0
        break; 
        case 1:
        // выполнить, если значение == 1 
          {
            if (MenuTable[menuLevel].type==0)
            {
              int menuLevelOld = menuLevel;
              menuEnter(MenuTable[menuLevel].mode1-1);  //  передали в функцию меню номер родителя
              menuLevel = menuLevelOld;
            }
            if (MenuTable[menuLevel].type==1)
            {
              // Выполнить функцию привязанную к данному меню
              menuCommand(menuLevel);
              return;
            }
          }
        break;
        case 2:
        // выполнить, если значение == 2  
        break;
        case 3:
        // выполнить, если значение == 3  
          { //Serial.println(" отработали козу Повернули вправо шаг меню вниз");
            currentMenuItem++;
            if (currentMenuItem>MenuTable[command].mode2) 
              {
                currentMenuItem --;
                menuLevel --;
              }
            do {
              menuLevel++;
            } while (command!=MenuTable[menuLevel].parent);    
            timeavtoexitstart = millis();
          }
        break;
        case 4:
        // выполнить, если значение == 4
          { //Serial.println(" отработали козу Повернули влево шаг меню вверх"); 
            currentMenuItem--;
            if (currentMenuItem<1) 
              {
                currentMenuItem ++;
                menuLevel ++;
              }
            do {
              menuLevel--;
            } while (command!=MenuTable[menuLevel].parent);    
            timeavtoexitstart = millis();
          }   
        break;
      }
    }
  lcd.clear();
  lcd.setCursor(3, 0);  // Первый аргумент позиция, второй строка
  lcd.print("Наливатор"); 
  lcd.setCursor(4, 1);
  lcd.print("Версия 1");
  menuLevel = command;  //  Устанавливает уровень меню в начальное состояние
}

//======= Опрос концевиков ========================================================================
// Опрашивает концевики и синий цвет если рюмки нет, красный цвет если рюмка стоит
void opros_Pins(){

 for (int i = 0; i < 5; i++) {     // опрашиваем концевики
  ws2812b.setPixelColor(i, ws2812b.Color(0, 0, 250));  // указываем синий цвет пикселя
    if(!digitalRead(SW_pins[i])){
      ws2812b.setPixelColor(i, ws2812b.Color(250, 0, 0));  // указываем красный цвет пикселя
    }  
  }
   ws2812b.show();  //  Передаем состояние пикселей в ленту
}

void setup() {
  Serial.begin(115200);
  //===== Подключение к сети Wi-Fi ================================================================
  WiFi.begin(ssid, password);
   while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.println("Подключение к WiFi..");
  }
  Serial.println("Подключились к сети WiFi");
  
  //===== Реле включения от Алисы и настройки mqtt брокера ========================================
  // pinMode(RELAY, OUTPUT);   // Пин реле как выход
  // digitalWrite(RELAY, LOW); // Состояние выхода в "0"
  // client.setServer(mqtt_server, mqtt_port);
  // client.setCallback(callback);
  
  //===== Помпа ===================================================================================
  pinMode(PUMP_PIN, OUTPUT);    // настроем пин помпы в качестве выходного вывода
  digitalWrite(PUMP_PIN, LOW);  // ВЫКЛЮЧАЕМ помпу
  
  //===== Таски ===================================================================================

  
  //===== Лента адресных светодиодов ==============================================================
  ws2812b.begin();  //  инициализация адресной ленты WS2812B 
  pinMode(PIN_WS2812B, OUTPUT);
 
  //===== Серва ===================================================================================
  myservo.attach(SERVO_ATTACH);  
  myservo.write(Pos[5]);

  //===== Экран ===================================================================================
  lcd.init();           //Инициализация LCD (по умолчанию для ESP32 30Pin: 21 - SDA, 22 - SCL)
  lcd.backlight();
  lcd.setCursor(3, 0);  // Первый аргумент позиция, второй строка
  lcd.print("Наливатор"); 
  lcd.setCursor(4, 1);
  lcd.print("Версия 1");
  
  //===== Энкодер =================================================================================
    encoder.attachSingleEdge(DT_PIN, CLK_PIN);
    encoder.clearCount();
    encoder.setFilter(1023);// Задержка для фильтрации дребезга. На медленных чипах нужно уменьшать 
    // esp_task_wdt_add(loopTaskHandle);
    button.setDebounceTime(50); // устанавливаем время восстановления 50 миллисекундам

  //======= Концевики =============================================================================== 
    for (int i = 0; i < 5; i++) {         // установить концевики в положение высокого уровня 
      pinMode(SW_pins[i], INPUT_PULLUP);  // настроем пины рюмок в качестве входного вывода и включим внутренний подтягивающий резистор   
    }   
  
  //========Плейер=================================================================================
    pinMode(BUSY_PIN, INPUT); 
    dfmp3.begin(16,17);// UART0 является основным UART на ESP32 и по умолчанию подключается к пинам GPIO1 (TX0) и GPIO3 (RX0).
  // Он часто используется для связи с компьютером через серийный монитор и также используется для прошивки платы ESP32 новыми программами. 
  //Сообщения могут выводиться в консоль с помощью Serial.println().
  // UART1 Не разведен и используется для связи с помятью
  // Используем UART2 на пинах RX 16 и TX 17
  // for boards that support hardware arbitrary pins
  // dfmp3.begin(10, 11); // RX, TX

  // during development, it's a good practice to put the module
  // into a known state by calling reset().  
  // You may hear popping when starting and you can remove this 
  // call to reset() once your project is finalized
  dfmp3.reset(); 
  //uint16_t version = dfmp3.getSoftwareVersion();
  //Serial.print("version ");
  //Serial.println(version);

  // show some properties and set the volume
  //uint16_t volume = dfmp3.getVolume();
  dfmp3.setVolume(2); //Установка громкости динамика
  
  //uint16_t count = dfmp3.getTotalTrackCount(DfMp3_PlaySource_Sd); // колличсество треков на флешке
  //Serial.print("files ");
  //Serial.println(count);

 // uint16_t mode = dfmp3.getPlaybackMode();
 // Serial.print("playback mode ");
 // Serial.println(mode);
 // Serial.println("starting...");   
  dfmp3.playMp3FolderTrack(1); 
  delay(800);                       //  Обязательная задержка для опроса мр3 плейера
  CvetoMuzik();                     //  Играем огнями пока звучит приветственная музыка
  ws2812b.clear();
  ws2812b.show();
}

void loop() {
  // if (!client.connected()) { // Постояння проверка подключение к mqtt брокеру
  //   reconnect();
  // }
  // client.loop();
  opros_Pins();
  int i = opros_encoder();
  switch (i) {
  //  case 0:
      // выполнить, если значение == 0
  //  break;
    case 1:
      // выполнить, если значение == 1 Это короткое нажатие на энкодер
      //Serial.println("Короткое нажатие");
      naliv() ; // Запускаем налив
    break;
    case 2:
      // выполнить, если значение == 2 Это длинное нажатие на энкодер
      //Serial.println("Длинное нажатие");
      {
        //menu();  //  Переходим в меню
        menuEnter(0);  //  выполнить функцию привязанной к данному меню
      }
    break;
    case 3:
      // выполнить, если значение == 3 Это поворот вправо
      //Serial.println(" отработали козу Повернули вправо");
      volum_level(1);
    break;
    case 4:
      // выполнить, если значение == 4 Это поворот влево
      //Serial.println(" отработали козу Повернули влево");
      volum_level(0);
    break;
  }
}


Да, тяжко тебе будет. Никто же еще (из оставивших ранее комментарии) даже не пытался «ругать»…

Началось с

а теперь

вот я всегда знал, что “покритикуйте” в посте, на деле означает “похвалите”. Как только что-то иное, сразу проблемы :frowning:

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

Да, я уж сколько раз раз пробовал. Но у вашего брата – вопрошающего, обычно бывает такая тонкая душевная организация, только чуть не то слово подберёшь – обиды и срач на весь форум. Кроме того, на любое “если делать универсально, то можно будет потом “и то и это”” тут же следует ответ – мне всего этого не надо, ты по делу говори и начинаешь теряться, что же там, блин, “по делу”, а что – нет.

Если правда хотите серьёзного разговора, я готов ещё раз попробовать, но давайте сделаем нормальную программу для этого – только меню и пустые заглушки для исполнительных функций (они просто печатают в сериал “выбран такой-то пункт меню”), иначе программа огромная, зачем на “вся эта цветомузыка” и сотни строк работы с нею?

Хотите – попробуем, но при первых обидках и оправданиях, я сливаюсь – неинтересно (можно засчитывать слив). Если будете пробовать, для начала уберите из программы все “исполнительные” функции – замените заглушками и выложите.

Вот так?

// Подключаем библиотеку русских сиволов
#define _LCD_TYPE 1       // Тип подключения дисплея: 1 - по шине I2C, 2 - десятиконтактное. Обязательно указывать ДО подключения библиотеки LCD_1602_RUS_ALL.h
#include "LiquidCrystal_I2C.h" 
#include <LCD_1602_RUS_ALL.h>
LCD_1602_RUS lcd(0x3F, 16, 2);

#define SHORT_PRESS_TIME 1000     //  1000 milliseconds
#define LONG_PRESS_TIME 1000      //  1000 milliseconds
#define SW_PIN 15                 //  пин GPIO15 ESP32 назначаем для кнопки SW энкодера
#define DT_PIN 2                  //  пин GPIO2 ESP32 назначаем для DT выхода энкодера
#define CLK_PIN 4                 //  пин GPIO4 ESP32 назначаем для CLK выхода энкодера
#define T_FOLDER         0 //   Тип меню - папка
#define T_APP            1 //   Функция для выполнения кода
struct menuStruct {
	uint8_t type;   //  тип меню папка или функция , если функция , то mode1 - номер функции, mode2 - параметры функций
	uint8_t parent; //  номер родителя, верхний уровень 0. 
	uint8_t mode1;  //  стартовый номер дочернего пункта меню в массиве MenuTable
	uint8_t mode2;  //  колличество  пунктов меню
	char name[32];  //  название пункта меню
};
//массив структуры меню
static PROGMEM  menuStruct MenuTable[] = {
	T_FOLDER,     		0, 1, 3,"      МЕНЮ      ", //  Меню 0 Это MenuLevel
		T_FOLDER,   	0, 2, 3,">   Промывка   <", // 1
			T_APP,  	1, 2, 3,">Старт промывки<", // 2
			T_APP,  	1, 2, 3,"> Стоп промывки<", // 3
      T_APP,    		1, 2, 3,">    Выход     <", // 4
		T_FOLDER,   	0, 6, 5,">   Настройка  <", // 5
			T_FOLDER, 	5, 6, 2,">   Громкость  <",
				T_APP,  6, 7, 2,">Уровень умолч.<",
				T_APP,  6, 7, 2,">    Выход     <",
			T_FOLDER, 	5, 10,3,">    Налив     <",
				T_APP,  6, 10,3,    "Уровень",
				T_APP,  6, 10,3,    "Проверка",
				T_APP,  6, 10,3,    "Выход",
			T_FOLDER, 	5, 14,3,">  Тип тостов  <",
				T_APP,  6, 14,3,    "Случайно",
				T_APP,  6, 14,3,    "По порядку",
				T_APP,  6, 14,3,    "Выход",
			T_FOLDER, 	5, 18,3,">  Тип налива  <",
				T_APP,  6, 18,3,    "Стандартный",
				T_APP,  6, 18,3,    "Игра",
				T_APP,  6, 18,3,">    Выход     <",
			T_APP,    	5, 21,1,">    Выход     <",
		T_APP,      	0, 22,1,">    Выход     <"
	};

uint8_t menuLevel = 0;  //  Текущий уровень меню

//======= Энкодер =================================================================================
// Обязательно на сигнальные провода энкодера вешать керамические конденсаторы 0.1мкф
#include "ESP32Encoder.h"
#include <ezButton.h>           // Библиотека для использования SW  пина энкодера
bool isPressing = false;		//  Признак нажатия энкодера
bool isLongDetected = false;	//  Признак длинного нажатия
unsigned long pressedTime  = 0; //  Время нажатия на кнопку энкодера
unsigned long releasedTime = 0; //  Время отпускания кнопки энкодера
ezButton button(SW_PIN);        //  создаем обьект ezButton на 15 пине;
// Обработчик прерываний
static IRAM_ATTR void enc_cb(void* arg) {
  ESP32Encoder* enc = (ESP32Encoder*) arg;
}
ESP32Encoder encoder(true, enc_cb);

//  Функция опроса энкодера
//  Возвращает: 0 - нет нажатия, 1 - короткое нажатие, 2 - длинное нажатие, 
//              3 - поворот вправо, 4 - поворот влево
int opros_encoder(){ 
  int8_t otvet = 0;
  button.loop();  // опрос кнопки энкодера
  if(button.isPressed()){  // Кнопка энкодера нажата
    pressedTime = millis();
    isPressing = true;
    isLongDetected = false;
    }     
  if(button.isReleased()){  // Кнопка энкодера отпущена
    isPressing = false;
    releasedTime = millis();
    long pressDuration = releasedTime - pressedTime;
    if ( pressDuration < SHORT_PRESS_TIME ){
      Serial.println("Короткое нажатие зафиксировано"); 
      otvet = 1;
    }
  }
  if (isPressing == true && isLongDetected == false) {
    long pressDuration = millis() - pressedTime;
    if ( pressDuration > LONG_PRESS_TIME ) {
      Serial.println("Длинное нажатие определено");
      isLongDetected = true;
      otvet = 2;
    }
  }
  delay(10);
  int i=encoder.getCount();
  if (i>0){  //  Вращение вправо
    Serial.println("Повернули вправо");
    otvet = 3;
  }
  else if (i<0){ //Вращение влево
    Serial.println("Повернули влево");
    otvet = 4;
  }
  encoder.clearCount();
  return(otvet);
}

//======= Выполнение команды меню =================================================================
// функция принимает числовое значение команды и отрабатывает в зависимости от команды
// привязанную к нему функцию	
void menuCommand(int command){
  switch (command)
  {
  case 2: // Промывка
    Serial.println("Функция Промывка 2 позиция в таблице меню");
    break;
  case 3: // Аварийная остановка промывки
	Serial.println("Функция Аварийная остановка промывки 3 позиция в таблице меню");
    break;
  case 4: // Выход из меню
	Serial.println("Функция Выход из меню 4 позиция в таблице меню");
    break;
	//......
	//......
	//......
  case 21: //выход из меню	
	Serial.println("Функция Выход из меню 21 позиция в таблице меню");
	break;  
  // Ну и так далее
  }
  return;
}

//======= Функция обработки меню ==================================================================
void menuEnter(int command){            //  command - это номер родителя меню
  menuLevel = MenuTable[command].mode1; //  Устанавливает номер дочернего меню в массиве меню
  int i =0;
  int timeavtoexitstart = millis();		//  Время начала автовыхода
  lcd.clear() ;
  u_int8_t currentMenuItem = 1; //  Устанавливает номер текущего пункта меню
  while ((millis() - timeavtoexitstart < 60000) || i==2) //  автовыход из пунктов меню через 1 минуту
    {
      lcd.setCursor(0, 0);    
      lcd.print(MenuTable[command].name); //  Выводим название родительского меню
      lcd.setCursor(0, 1);  
      lcd.print(MenuTable[menuLevel].name);//  Выводим название дочернего меню 
      i = opros_encoder();   
      switch (i) {
        case 1:
          {
            if (MenuTable[menuLevel].type==0)	// Если меню с подпунктами
            {
              int menuLevelOld = menuLevel;		//  запоминаем текущий уровень меню
              menuEnter(MenuTable[menuLevel].mode1-1);  //  передали в функцию меню номер родителя
              menuLevel = menuLevelOld;			//  возвращаем текущий уровень меню
            }
            if (MenuTable[menuLevel].type==1)	// Если меню без подпунктов
            {
              menuCommand(menuLevel);	// Выполнить функцию привязанную к данному меню
              return;
            }
          }
        break;
        case 2:
        // Можно что-то выполнить, если длительное нажатие на энкодер 
        break;
        case 3:
		// поворот энкодера вправо увеличивает номер меню, если достигла последнего пункта меню,
		// то поворот вправо не изменяет уровень меню
		// если время автовыхода истекло, то выходим в меню родителя
          {
            currentMenuItem++;
            if (currentMenuItem>MenuTable[command].mode2) 
              {
                currentMenuItem --;
                menuLevel --;
              }
            do {
              menuLevel++;
            } while (command!=MenuTable[menuLevel].parent);    
            timeavtoexitstart = millis();
          }
        break;
        case 4:
        // поворот энкодера влево уменьшает номер меню, если достиг верхнего пункта меню,
		// то поворот вправо не изменяет уровень меню
		// если время автовыхода истекло, то выходим в меню родителя
          { 
            currentMenuItem--;
            if (currentMenuItem<1) 
              {
                currentMenuItem ++;
                menuLevel ++;
              }
            do {
              menuLevel--;
            } while (command!=MenuTable[menuLevel].parent);    
            timeavtoexitstart = millis();
          }   
        break;
      }
    }
  lcd.clear();
  lcd.setCursor(3, 0);  // Первый аргумент позиция, второй строка
  lcd.print("Наливатор"); 
  lcd.setCursor(4, 1);
  lcd.print("Версия 1");
  menuLevel = command;  //  Устанавливает уровень меню в начальное состояние
}

void setup() {
  Serial.begin(115200);
 

  //===== Экран ===================================================================================
  lcd.init();           //Инициализация LCD (по умолчанию для ESP32 30Pin: 21 - SDA, 22 - SCL)
  lcd.backlight();
  lcd.setCursor(3, 0);  // Первый аргумент позиция, второй строка
  lcd.print("Наливатор"); 
  lcd.setCursor(4, 1);
  lcd.print("Версия 1");
  
  //===== Энкодер =================================================================================
    encoder.attachSingleEdge(DT_PIN, CLK_PIN);
    encoder.clearCount();
    encoder.setFilter(1023);// Задержка для фильтрации дребезга. На медленных чипах нужно уменьшать 
    // esp_task_wdt_add(loopTaskHandle);
    button.setDebounceTime(50); // устанавливаем время восстановления 50 миллисекундам

}

void loop() {
  int i = opros_encoder(); // опрос энкодера  в основном цикле имеет 4 варианта
  // 1 - короткое нажатие, 2 - длинное нажатие, 3 - поворот вправо, 4 - поворот влево
  switch (i) {
    case 1:
      //naliv() ; // Запускаем налив
	  Serial.println("короткое нажатие на энкодер в основном цикле выполняет функцию налива");
	  delay(1000);
	break;
    case 2:
      Serial.println("Длинное нажатие на энкодер в основном цикле выполняет функцию переходав меню выбора");
      {
        menuEnter(0); 
      }
    break;
    case 3:
      Serial.println("Поворот вправо энкодер в основном цикле выполняет функцию увеличения громкости");
	  delay(1000);
    break;
    case 4:
      Serial.println("Поворот влево энкодер в основном цикле выполняет функцию ууменьшения громкости");
	  delay(1000);
    break;
  }
}

micromenu и micromenu v2 погугли и посмотри как реализовано. Лаконичнее v2 не встречал. GitHub - abcminiuser/micromenu-v2: Tiny text-orientated menu library in C for embedded use.

Я так и не понял зачем все эти идентификаторы непонятные, с количеством пунктов, подменю, папка/не папка и т.д
Сам делаю как то так:

/*Полный набор из тех пунктов, которые могут быть нужны.
Переходы (следующий/предыдущий пункты)
Вложения (родитель/ребенок)
Возможность наличия переменной.
Действие по кнопке ОК.
Вызов функции.
menu_id пункт необязательный.
*/

typedef struct menuItem{
    int menu_id;
    char *menu_name;
    struct menuItem *parent;
    struct menuItem *child;
    struct menuItem *next;
    struct menuItem *prev;
    struct menuItem *ok;
    void (*Handler)();
    int *value;
};
/*
Далее все пункты объявляем.
Если нужен переход указываем куда.
То же самое на функции и переменные.
Все что не надо запрещаем -затыкаем NULL
*/

struct menuItem 
mOption,
mMain,
mSelect,
mTemperature,
mSelectValue
; 

mMain={1,"MAIN    ",NULL,NULL,&mOption,&mTemperature,&mMain,blink,NULL};
mOption={2,"OPTION    ",NULL,NULL,&mSelect,&mMain,NULL,NULL,NULL};
mSelect={3,"SELECT    ",NULL,NULL,&mTemperature,&mOption,&mSelectValue,NULL,NULL};
mTemperature={4,"TEMP       ",NULL,NULL,&mMain,&mSelect,NULL,NULL,NULL};
mSelectValue={31,"VALUE      ",&mSelect,NULL,NULL,NULL,&mSelect,NULL,&selectValue};

void loop(){
static menuItem *cMenu=&mMain;
}

Далее просто прописываем для каждого из возможных действий свой обработчик.
Примерно вот так идёт обработка энкодера.(У меня свое видение, энкодер возвращает -1/+1 в зависимости от того, куда его вращают)

if(encTick){
        switch(encTick){
            case 1:
            if(cMenu->next)
            cMenu=cMenu->next;
            else{
                if(cMenu->value)
                *cMenu->value+=encTick;
            }
            break;
            case -1:
            if(cMenu->next)
            cMenu=cMenu->prev;
            else{
                if(cMenu->value)
                *cMenu->value+=encTick;
            }
            break;            
        }
    updateFlag=1;
    encTick=0;
    }

if(encBtn.read()==1){
        Serial.println("btn pressed");
        if(cMenu->ok){
   cMenu=cMenu->ok;
        updateFlag=1;
            } else
if(cMenu->Handler){
cMenu.Handler();
}

    }

Как то так.

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

Указатели для меня еще сложно. Пробую .

@smagluk, 36 байтов на один пункт меню - это круто. посмотрите это:

Сейчас уже не помню, но там что-то порядка одного байта на пункт меню при том, что меню формируется динамически в процессе выполнения программы. Другими словами, может быть перестроено.

1 лайк