Создание многоуровневого графического меню

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

ЕвгенийП пишет:

 Давайте Вы таки нарисуете совё дерево и приведёте код.

Код ковыряю Ваш. Но я скорее как первый человек на первом плоту...

struct SMenuItem {
  int id;                 // уникальный идентификатор данного элемента
  SMenuItem * prev;       // следующий элемент данного урвоня (nullptr - если этот - последний)
  SMenuItem * next;       // предыдущий элемент данного урвоня (nullptr - если этот - первый)
  SMenuItem * parent;     // адрес родительского элемента (nullptr - если нет родителей)
  SMenuItem * children;   // адрес списка элементов "подменю" (nullptrll - если это лист)
  const char * itemText;  // Название данного элемента (тут может быть не текст, а адрес картинки. Функция show знает, что с этим делать)

  //
  // Показать данный элемент
  void show(void) {
    // ДОПОЛНЕНИЕ - если мы активны, печатаем звёздочку, иначе - пробел
    Serial.print(iAmActive ? '*' : ' ');

    for (SMenuItem * ptr = parent; ptr; ptr = ptr->parent) {
      if (ptr->parent) Serial.print("   ");
    }
    Serial.println(itemText);
  }

  //
  // Показать всех детей данного элемента
  //  Параметр: глубина показа "внуков". 
  //  Если 1 - только дети, если 2 - то и внук и т.д.
  void showChildren(const int showGrandChilren = 1) {
    if (! showGrandChilren) return;
    for (SMenuItem * ptr = children; ptr; ptr = ptr->next) {
      ptr->show();
      ptr->showChildren(showGrandChilren - 1);
    }
  }

  //
  //  выполнить действие, когда этот элемент выбран
  void action(void) {
    Serial.println("***********************************************");
    Serial.print("***** The menu \"");
    Serial.print(itemText);
    Serial.println("\" is executed");
    Serial.println("***********************************************");
  }

  //
  //  Удаление данного элемент из меню
 /* void removeMe(void) {
    //
    // Если есть следующий элемент, то 
    //  делаем мой предыдущий, его предыдущим
    if (next) next->prev = prev;
    //
    // Если есть предыдущий
    // то делаем наш следущий, его следущим
    // Иначе говорим родителю, что наше next - его первый ребёнок.
    if (prev) prev->next = next; 
    else parent->children = next;
  } */

  //
  // Вставить себя как первого ребёнка объекта _parent
  void insertAsFirstChild(SMenuItem & _parent) {
    // 
    // Вставляем первого ребёнка после себя, а себя делаем первым ребёнком
    next = _parent.children;
    prev = nullptr;
    _parent.children = this;
  }

  //
  // Вставить себя после объекта _prev
  void insertAfter(SMenuItem & _prev) {
    // 
    // Ставим себя следующим объекту _prev, а его следующего - своим следующим
    next = _prev.next;
    _prev.next = this;
  }

  //
  //  Конструктор - создать элемент
  SMenuItem(const char * const _itemText, SMenuItem * _prev = nullptr, SMenuItem * _parent = nullptr) {
    itemText = _itemText;
    prev = _prev;
    parent = _parent;
    children = nullptr;
    next = nullptr;
    //
    // Если у нас есть родитель
    if (parent) {
      // если у родителя пока нет детей, записываемся началом списка детей.
      if (parent->children == nullptr) parent->children = this;
    }
    //
    // Если у нас есть предыдущий элемент
    if (prev) {
      // записываемся ему в "следующие"
      prev->next = this;
    }
    // 
    // ДОПОЛНЕНИЕ - мы неактивны
    iAmActive = false;
  }

//
//  ДОПОЛНЕНИЯ
//
  bool iAmActive;

  // Активировать (параметр - true) или деактивировать
  // Возвращает адрес себя
  SMenuItem * activate(const bool actDeact) {
    iAmActive = actDeact;
    return this;
  }

  // Перейти к следующему (если есть)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goNext(void) {
    // Если есть следующий, то активируем его
    if (next) {
      activate(false); // деактивируем себя
      return next->activate(true);
    }
    return this;
  }

  // Перейти к предыдущему (если есть)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goPrev(void) {
    // Если есть предыдущий, то активируем его
    if (prev) {
      activate(false); // деактивируем себя
      return prev->activate(true);
    }
    return this;
  }

  // Перейти к родителю (если есть)
  // на самом деле переходим только в том случае, если у родителя есть 
  // ещё и свой родитель, т.к. нам не нужно переходиь к общему прародителю
  // (в нашем случае к m0)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goParent(void) {
    // Если есть родитель и у того тоже есть родитель, то активируем родителя
    if (parent && parent->parent) {
      activate(false); // деактивируем себя
      return parent->activate(true);
    }
    return this;
  }

  // Если нет детей, то выполнить данный пункт
  // А если есть дети, то перейти к первому ребёнку 
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goChildOrRun(void) {
    // Если нет детей, то выполняем данный элемент
    if (! children) action();
    else {
      activate(false); // деактивируем себя
      return children->activate(true);
    }
    return this;
  }
};


// Корневой элемент всего меню. Он обычно невидимый. 
//  он родитель элементов верхнего уровня!

SMenuItem m0(""); // у этого нет ни предыдущего, ни родителя

SMenuItem m1("Menu 1", nullptr, & m0);
SMenuItem m2("Menu 2", & m1, &m0);
SMenuItem m3("Menu 3", & m2, &m0);

SMenuItem m21("Submenu 21", nullptr, & m2);// у этого нет предыдущего, но есть родитель
SMenuItem m22("Submenu 22", & m21, & m2); // у этого есть и предыдущий, и родитель
SMenuItem m23("Submenu 23", & m22, & m2); // у этого есть и предыдущий, и родитель
SMenuItem m24("Submenu 24", & m23, & m2); // у этого есть и предыдущий, и родитель

SMenuItem m211("Submenu 211", nullptr, & m21);
SMenuItem m212("Submenu 212", & m211, & m21);
SMenuItem m213("Submenu 213", & m212, & m21);

SMenuItem m221("Submenu 221", nullptr, & m22);
SMenuItem m222("Submenu 222", & m221, & m22);
SMenuItem m223("Submenu 223", & m222, & m22);

SMenuItem m231("Submenu 231", nullptr, & m23);
SMenuItem m232("Submenu 232", & m231, & m23);
SMenuItem m233("Submenu 233", & m232, & m23);

SMenuItem m241("Submenu 241", nullptr, & m24);
SMenuItem m242("Submenu 242", & m241, & m24);
SMenuItem m243("Submenu 243", & m242, & m24);

//
// Ввод числа от 1 до 4
//
int prompt(void) {
  int res = 0;
  while (res < 1 || res > 4) {
    Serial.println ("Введи число от 1 до 4: ");
    Serial.println ("  1 - к предыдущему;");
    Serial.println ("  2 - к следующему;");
    Serial.println ("  3 - к родителю;");
    Serial.println ("  4 - к ребёнку или (если нет детей, то выполнить)");
    res = Serial.parseInt(); // Вводим число
    while(Serial.available()) Serial.read(); // Вычитываем всё, что осталось
  }
  return res;
}

void setup() {
  Serial.begin(115200);
  Serial.setTimeout(0xFFFFFFFF);
  Serial.println("Веселье начинается!");
}

void loop() {
  static SMenuItem * activeElement = m1.activate(true);

  Serial.println("----------------------------------");
  m0.showChildren(1); // Печатаем меню

// Обрабатываем комнду
//  1 - продвинуться по меню вверх, если есть куда (на том же уровне)
//  2 - продвинуться вниз, если есть куда (на том же уровне)
//  3 - перейти на уровень вверх (к родителю)
//  4 - если это не листик, то перейти на уровень вниз, а если листик, то выполнить этот пункт  
  switch (prompt()) {
    case 1:  activeElement = activeElement->goPrev(); break;
    case 2:  activeElement = activeElement->goNext(); break;
    case 3:  activeElement = activeElement->goParent(); break;
    case 4:  activeElement = activeElement->goChildOrRun(); break;
  }
}

Дерево у меня простое.

Для "Подменю 22-24" иерархия такая-же как и для "Подменю 21". Т.е. по 3 дитя у каждого.

Но все это планируется на экран 0,96", а на нем помещается только 4 строки.

Поэтому план такой:

На начальном экране 3 пункта меню. Это все дети m0, поэтому я и обозвал "дети активного уровня"

При выборе 2го пункта отображаются 4 подменю, дети m2

При выборе Подменю-1 отображаются дети m21

В качестве костыля я уже собирался прописывать каждый экран по отдельности... Всего-то 19 экранов :) 

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

Пока все сложновато для меня...

Никак не могу выудить ID активного пункта меню.

Обозначил переменную

String curMenu;

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

В show() добавил

  void show(void) {
    // ДОПОЛНЕНИЕ - если мы активны, печатаем звёздочку, иначе - пробел
    Serial.print(iAmActive ? '*' : ' ');
    if (iAmActive == true){
          curMenu = itemText;
    }
    for (SMenuItem * ptr = parent; ptr; ptr = ptr->parent) {
      if (ptr->parent) Serial.print("   ");
    }
    Serial.println(itemText);
  }

Ну и в loop()

void loop() {
  static SMenuItem * activeElement = m1.activate(true);

  Serial.println("----------------------------------");
  m0.showChildren(1000); // Печатаем меню

  Serial.println();
  Serial.print ("Активное меню ");
  Serial.println(curMenu);
  Serial.println();
// Обрабатываем комнду
//  1 - продвинуться по меню вверх, если есть куда (на том же уровне)
//  2 - продвинуться вниз, если есть куда (на том же уровне)
//  3 - перейти на уровень вверх (к родителю)
//  4 - если это не листик, то перейти на уровень вниз, а если листик, то выполнить этот пункт  
  switch (prompt()) {
    case 1:  activeElement = activeElement->goPrev(); break;
    case 2:  activeElement = activeElement->goNext(); break;
    case 3:  activeElement = activeElement->goParent(); break;
    case 4:  activeElement = activeElement->goChildOrRun(); break;
  }
}

В этом случае удается выловить название активного меню. Но, блин, памяти жрет на 4% больше. Это при условии, что кроме данного скетча ничего больше нет. 

Скетч использует 4844 байт (15%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 1078 байт (52%) динамической памяти, оставляя 970 байт для локальных переменных. Максимум: 2048 байт.
 
Попытался внести изменения в структуру
 
struct SMenuItem {
  int id;                 // уникальный идентификатор данного элемента
  SMenuItem * prev;       // следующий элемент данного урвоня (nullptr - если этот - последний)
  SMenuItem * next;       // предыдущий элемент данного урвоня (nullptr - если этот - первый)
  SMenuItem * parent;     // адрес родительского элемента (nullptr - если нет родителей)
  SMenuItem * children;   // адрес списка элементов "подменю" (nullptrll - если это лист)
  const char * itemText;  // Название данного элемента (тут может быть не текст, а адрес картинки. Функция show знает, что с этим делать)
  const int * itemId;  // ID активного пункта меню

И соответственно, в конструкторе

  SMenuItem(const char * const _itemText, SMenuItem * _prev = nullptr, SMenuItem * _parent = nullptr, const int * const itemId) {
    itemText = _itemText;
    prev = _prev;
    parent = _parent;
    children = nullptr;
    next = nullptr;
    itemId = _itemId;

 И при заполнении

SMenuItem m1("Menu 1", nullptr, & m0, 1);
SMenuItem m2("Menu 2", & m1, &m0, 2);
SMenuItem m3("Menu 3", & m2, &m0, 3);

Но это дает ошибку. Понимаю, что путаю мягкое с теплым, но мозх уже кипит. Сложновато на текущем уровне.

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

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

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

Евгений, скетч ниже:

int curMenu;

struct SMenuItem {
  int id;                 // уникальный идентификатор данного элемента
  SMenuItem * prev;       // следующий элемент данного урвоня (nullptr - если этот - последний)
  SMenuItem * next;       // предыдущий элемент данного урвоня (nullptr - если этот - первый)
  SMenuItem * parent;     // адрес родительского элемента (nullptr - если нет родителей)
  SMenuItem * children;   // адрес списка элементов "подменю" (nullptrll - если это лист)
  const char * itemText;  // Название данного элемента (тут может быть не текст, а адрес картинки. Функция show знает, что с этим делать)

  //
  // Показать данный элемент
  void show(void) {
    // ДОПОЛНЕНИЕ - если мы активны, печатаем звёздочку, иначе - пробел
    Serial.print(iAmActive ? '*' : ' ');
    if (iAmActive == true){
          curMenu = itemText;
    }
    for (SMenuItem * ptr = parent; ptr; ptr = ptr->parent) {
      if (ptr->parent) Serial.print("   ");
    }
    Serial.println("Меню");
  }



  //
  // Показать всех детей данного элемента
  //  Параметр: глубина показа "внуков". 
  //  Если 1 - только дети, если 2 - то и внук и т.д.
  void showChildren(const int showGrandChilren = 1) {
    if (! showGrandChilren) return;
    for (SMenuItem * ptr = children; ptr; ptr = ptr->next) {
      ptr->show();
      ptr->showChildren(showGrandChilren - 1);
    }
  }

  //
  //  выполнить действие, когда этот элемент выбран
  void action(void) {
    Serial.println("***********************************************");
    Serial.print("***** The menu \"");
    Serial.print(curMenu);
    Serial.println("\" is executed");
    Serial.println("***********************************************");
  }

  //
  //  Удаление данного элемент из меню
  void removeMe(void) {
    //
    // Если есть следующий элемент, то 
    //  делаем мой предыдущий, его предыдущим
    if (next) next->prev = prev;
    //
    // Если есть предыдущий
    // то делаем наш следущий, его следущим
    // Иначе говорим родителю, что наше next - его первый ребёнок.
    if (prev) prev->next = next; 
    else parent->children = next;
  } 

  //
  // Вставить себя как первого ребёнка объекта _parent
  void insertAsFirstChild(SMenuItem & _parent) {
    // 
    // Вставляем первого ребёнка после себя, а себя делаем первым ребёнком
    next = _parent.children;
    prev = nullptr;
    _parent.children = this;
  }

  //
  // Вставить себя после объекта _prev
  void insertAfter(SMenuItem & _prev) {
    // 
    // Ставим себя следующим объекту _prev, а его следующего - своим следующим
    next = _prev.next;
    _prev.next = this;
  }

  //
  //  Конструктор - создать элемент
  SMenuItem(const char * const _itemText, SMenuItem * _prev = nullptr, SMenuItem * _parent = nullptr) {
    itemText = _itemText;
    prev = _prev;
    parent = _parent;
    children = nullptr;
    next = nullptr;
    //
    // Если у нас есть родитель
    if (parent) {
      // если у родителя пока нет детей, записываемся началом списка детей.
      if (parent->children == nullptr) parent->children = this;
    }
    //
    // Если у нас есть предыдущий элемент
    if (prev) {
      // записываемся ему в "следующие"
      prev->next = this;
    }
    // 
    // ДОПОЛНЕНИЕ - мы неактивны
    iAmActive = false;
  }

//
//  ДОПОЛНЕНИЯ
//
  bool iAmActive;

  // Активировать (параметр - true) или деактивировать
  // Возвращает адрес себя
  SMenuItem * activate(const bool actDeact) {
    iAmActive = actDeact;
    return this;
  }

  // Перейти к следующему (если есть)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goNext(void) {
    // Если есть следующий, то активируем его
    if (next) {
      activate(false); // деактивируем себя
      return next->activate(true);
//      Serial.print (id);
    }
    return this;
  }

  // Перейти к предыдущему (если есть)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goPrev(void) {
    // Если есть предыдущий, то активируем его
    if (prev) {
      activate(false); // деактивируем себя
      return prev->activate(true);
    }
    return this;
  }

  // Перейти к родителю (если есть)
  // на самом деле переходим только в том случае, если у родителя есть 
  // ещё и свой родитель, т.к. нам не нужно переходиь к общему прародителю
  // (в нашем случае к m0)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goParent(void) {
    // Если есть родитель и у того тоже есть родитель, то активируем родителя
    if (parent && parent->parent) {
      activate(false); // деактивируем себя
      return parent->activate(true);
    }
    return this;
  }

  // Если нет детей, то выполнить данный пункт
  // А если есть дети, то перейти к первому ребёнку 
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goChildOrRun(void) {
    // Если нет детей, то выполняем данный элемент
    if (! children) action();
    else {
      activate(false); // деактивируем себя
      return children->activate(true);
    }
    return this;
  }
};


// Корневой элемент всего меню. Он обычно невидимый. 
//  он родитель элементов верхнего уровня!

SMenuItem m0(""); // у этого нет ни предыдущего, ни родителя

SMenuItem m1(1, nullptr, & m0);
SMenuItem m2(2, & m1, &m0);
SMenuItem m3(3, & m2, &m0);

SMenuItem m21(21, nullptr, & m2);// у этого нет предыдущего, но есть родитель
SMenuItem m22(22, & m21, & m2); // у этого есть и предыдущий, и родитель
SMenuItem m23(23, & m22, & m2); // у этого есть и предыдущий, и родитель
SMenuItem m24(24, & m23, & m2); // у этого есть и предыдущий, и родитель

SMenuItem m211(211, nullptr, & m21);
SMenuItem m212(212, & m211, & m21);
SMenuItem m213(213, & m212, & m21);

SMenuItem m221(221, nullptr, & m22);
SMenuItem m222(222, & m221, & m22);
SMenuItem m223(223, & m222, & m22);

SMenuItem m231(231, nullptr, & m23);
SMenuItem m232(232, & m231, & m23);
SMenuItem m233(233, & m232, & m23);

SMenuItem m241(241, nullptr, & m24);
SMenuItem m242(242, & m241, & m24);
SMenuItem m243(243, & m242, & m24);

//
// Ввод числа от 1 до 4
//
int prompt(void) {

  
  int res = 0;
  while (res < 1 || res > 4) {
    Serial.println ("Введи число от 1 до 4: ");
    Serial.println ("  1 - к предыдущему;");
    Serial.println ("  2 - к следующему;");
    Serial.println ("  3 - к родителю;");
    Serial.println ("  4 - к ребёнку или (если нет детей, то выполнить)");
    res = Serial.parseInt(); // Вводим число
    while(Serial.available()) Serial.read(); // Вычитываем всё, что осталось
  }
  return res;
}

void setup() {
  Serial.begin(115200);
  Serial.setTimeout(0xFFFFFFFF);
  Serial.println("Веселье начинается!");
}

void loop() {
  static SMenuItem * activeElement = m1.activate(true);

  Serial.println("----------------------------------");
  m0.showChildren(1000); // Печатаем меню

  menu_select();
// Обрабатываем комнду
//  1 - продвинуться по меню вверх, если есть куда (на том же уровне)
//  2 - продвинуться вниз, если есть куда (на том же уровне)
//  3 - перейти на уровень вверх (к родителю)
//  4 - если это не листик, то перейти на уровень вниз, а если листик, то выполнить этот пункт  
  switch (prompt()) {
    case 1:  activeElement = activeElement->goPrev(); break;
    case 2:  activeElement = activeElement->goNext(); break;
    case 3:  activeElement = activeElement->goParent(); break;
    case 4:  activeElement = activeElement->goChildOrRun(); break;
  }
}

void menu_select() {
 switch (curMenu) {
  case 1:   Serial.println();
     Serial.print ("Активное меню один");
     Serial.println();
  break;
  case 2:   Serial.println();
     Serial.print ("Активное меню два");
     Serial.println();
  break;
 }
}

Костыльным методом начал получать номер активного пункта меню :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

А что такое активный пункт, напомните. Тот который курсором в данным момент выбран?

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

Да, выбранный.

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

На текущий момент получился вот такой монстр:

#include <iarduino_OLED_txt.h>                             // Подключаем библиотеку iarduino_OLED_txt.
//#include <EEPROM.h>                                        // Подключаем работу с EEPROM

//////////////////////////////////////////////

                                                             //
extern uint8_t MediumFontRus[];                              // Подключаем шрифт MediumFontRus.

iarduino_OLED_txt myOLED(0x3C);                            // Объявляем объект myOLED_1, указывая адрес первого дисплея на шине I2C: 0x3C.
                                                           // 
extern uint8_t MediumFontRus[];                            // Подключаем шрифт MediumFontRus.
extern uint8_t SmallFontRus[];                             // Подключаем шрифт SmallFontRus.
                                                           // Если Вы не используете Кириллицу, то лучше подключить шрифты MediumFont и SmallFont, они займут меньше места в памяти программ.

   #define DEBUG                                           // Если не закомментированно, то, Serial.print() актевен! Только для отладки!!
//   #define DEBUG_R                                       // Настройка радио

////////////////////////////////////////////
// Переменные
////////////////////////////////////////////

boolean programMode = false;                               // режим программирования
boolean editMode = false;                                  // режим изменения параметров
boolean bttnPressed = false;                                // флаг нажатия джойстика
int menuScreen;                                             // Номер экрана меню
int curMenu;                                                // выбранный пункт
int menuCommand;


const int8_t Xaxis  = A0;                                  // Определяем номер вывода, к которому подключен контакт оси Х джойстика
const int8_t Yaxis  = A1;                                  // Определяем номер вывода, к которому подключен контакт оси У джойстика
uint16_t     Xvol = 0, Yvol = 0;                           // Задаём переменные, которые будут принимать значения, считанные с осей джойстика

// Заполняем меню

struct SMenuItem {
  int id;                 // уникальный идентификатор данного элемента
  SMenuItem * prev;       // следующий элемент данного урвоня (nullptr - если этот - последний)
  SMenuItem * next;       // предыдущий элемент данного урвоня (nullptr - если этот - первый)
  SMenuItem * parent;     // адрес родительского элемента (nullptr - если нет родителей)
  SMenuItem * children;   // адрес списка элементов "подменю" (nullptrll - если это лист)
  const char * itemText;  // Название данного элемента (тут может быть не текст, а адрес картинки. Функция show знает, что с этим делать)

  //
  // Показать данный элемент
  void show(void) {
    // ДОПОЛНЕНИЕ - если мы активны, печатаем звёздочку, иначе - пробел
 //   Serial.print(iAmActive ? '*' : ' ');
    if (iAmActive == true){
          curMenu = itemText;
    }
 //   for (SMenuItem * ptr = parent; ptr; ptr = ptr->parent) {
 //     if (ptr->parent) Serial.print("   ");
 //   }
//    Serial.println("Меню");
  }



  //
  // Показать всех детей данного элемента
  //  Параметр: глубина показа "внуков". 
  //  Если 1 - только дети, если 2 - то и внук и т.д.
  void showChildren(const int showGrandChilren = 1) {
    if (! showGrandChilren) return;
    for (SMenuItem * ptr = children; ptr; ptr = ptr->next) {
      ptr->show();
      ptr->showChildren(showGrandChilren - 1);
    }
  }

  //
  //  выполнить действие, когда этот элемент выбран
  void action(void) {
    Serial.println("***********************************************");
    Serial.print("***** The menu \"");
    Serial.print(curMenu);
    Serial.println("\" is executed");
    Serial.println("***********************************************");
  }

  //
  //  Удаление данного элемент из меню
 /* void removeMe(void) {
    //
    // Если есть следующий элемент, то 
    //  делаем мой предыдущий, его предыдущим
    if (next) next->prev = prev;
    //
    // Если есть предыдущий
    // то делаем наш следущий, его следущим
    // Иначе говорим родителю, что наше next - его первый ребёнок.
    if (prev) prev->next = next; 
    else parent->children = next;
  } */

  //
  // Вставить себя как первого ребёнка объекта _parent
  void insertAsFirstChild(SMenuItem & _parent) {
    // 
    // Вставляем первого ребёнка после себя, а себя делаем первым ребёнком
    next = _parent.children;
    prev = nullptr;
    _parent.children = this;
  }

  //
  // Вставить себя после объекта _prev
  void insertAfter(SMenuItem & _prev) {
    // 
    // Ставим себя следующим объекту _prev, а его следующего - своим следующим
    next = _prev.next;
    _prev.next = this;
  }

  //
  //  Конструктор - создать элемент
  SMenuItem(const char * const _itemText, SMenuItem * _prev = nullptr, SMenuItem * _parent = nullptr) {
    itemText = _itemText;
    prev = _prev;
    parent = _parent;
    children = nullptr;
    next = nullptr;
    //
    // Если у нас есть родитель
    if (parent) {
      // если у родителя пока нет детей, записываемся началом списка детей.
      if (parent->children == nullptr) parent->children = this;
    }
    //
    // Если у нас есть предыдущий элемент
    if (prev) {
      // записываемся ему в "следующие"
      prev->next = this;
    }
    // 
    // ДОПОЛНЕНИЕ - мы неактивны
    iAmActive = false;
  }

//
//  ДОПОЛНЕНИЯ
//
  bool iAmActive;

  // Активировать (параметр - true) или деактивировать
  // Возвращает адрес себя
  SMenuItem * activate(const bool actDeact) {
    iAmActive = actDeact;
    return this;
  }

  // Перейти к следующему (если есть)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goNext(void) {
    // Если есть следующий, то активируем его
    if (next) {
      activate(false); // деактивируем себя
      return next->activate(true);
//      Serial.print (id);
    }
    return this;
  }

  // Перейти к предыдущему (если есть)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goPrev(void) {
    // Если есть предыдущий, то активируем его
    if (prev) {
      activate(false); // деактивируем себя
      return prev->activate(true);
    }
    return this;
  }

  // Перейти к родителю (если есть)
  // на самом деле переходим только в том случае, если у родителя есть 
  // ещё и свой родитель, т.к. нам не нужно переходиь к общему прародителю
  // (в нашем случае к m0)
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goParent(void) {
    // Если есть родитель и у того тоже есть родитель, то активируем родителя
    if (parent && parent->parent) {
      activate(false); // деактивируем себя
      return parent->activate(true);
    }
    return this;
  }

  // Если нет детей, то выполнить данный пункт
  // А если есть дети, то перейти к первому ребёнку 
  // Возвращает адрес нового (или старого, если не перешли) активного элемента
  SMenuItem * goChildOrRun(void) {
    // Если нет детей, то выполняем данный элемент
    if (! children) action();
    else {
      activate(false); // деактивируем себя
      return children->activate(true);
    }
    return this;
  }
};


// Корневой элемент всего меню. Он обычно невидимый. 
//  он родитель элементов верхнего уровня!

SMenuItem m0(""); // у этого нет ни предыдущего, ни родителя

SMenuItem m1(1, nullptr, & m0);
SMenuItem m2(2, & m1, &m0);
SMenuItem m3(3, & m2, &m0);

SMenuItem m21(21, nullptr, & m2);// у этого нет предыдущего, но есть родитель
SMenuItem m22(22, & m21, & m2); // у этого есть и предыдущий, и родитель
SMenuItem m23(23, & m22, & m2); // у этого есть и предыдущий, и родитель
SMenuItem m24(24, & m23, & m2); // у этого есть и предыдущий, и родитель

SMenuItem m211(211, nullptr, & m21);
SMenuItem m212(212, & m211, & m21);
SMenuItem m213(213, & m212, & m21);

SMenuItem m221(221, nullptr, & m22);
SMenuItem m222(222, & m221, & m22);
SMenuItem m223(223, & m222, & m22);

SMenuItem m231(231, nullptr, & m23);
SMenuItem m232(232, & m231, & m23);
SMenuItem m233(233, & m232, & m23);

SMenuItem m241(241, nullptr, & m24);
SMenuItem m242(242, & m241, & m24);
SMenuItem m243(243, & m242, & m24);

//
// Ввод числа от 1 до 4
//
int prompt(void) {

  
  int res = 0;
  while (res < 1 || res > 4) {
    Serial.println ("Введи число от 1 до 4: ");
    Serial.println ("  1 - к предыдущему;");
    Serial.println ("  2 - к следующему;");
    Serial.println ("  3 - к родителю;");
    Serial.println ("  4 - к ребёнку или (если нет детей, то выполнить)");
    res = Serial.parseInt(); // Вводим число
    while(Serial.available()) Serial.read(); // Вычитываем всё, что осталось
  }
  return res;
}


//
void setup(){                                              //
   Serial.begin(115200);                                     // Запускаем серийный порт

Screen_Intro();
  delay (2000);                                            // ждем 3 секунды
  if (map(analogRead(A0), 0, 1023, 0, 255) <= 60) {

#ifdef DEBUG
  Serial.println("Program mode = true");                   
#endif
    
    programMode = true;                                   // проверка нажата ли кнопка программирования, если да,
    Screen_Program_intro();                               //  то переход в функцию программирования
  }

}                                                          //
void loop(){                  // Цикл программы
  
  Xvol = map(analogRead(Xaxis), 0, 1023, 0, 255);               // Считываем значения оси Х
  Yvol = map(analogRead(Yaxis), 0, 1023, 0, 255);               // Считываем значения оси У

  if (programMode == true) {                                    // Если в режиме программированивания
    
    Joystic_Read_Programm();                                    // Проверяем что куда смотрит джойстик 

    static SMenuItem * activeElement = m1.activate(true);
    m0.showChildren(1000); // Печатаем меню

// Обрабатываем комнду
//  1 - продвинуться по меню вверх, если есть куда (на том же уровне)
//  2 - продвинуться вниз, если есть куда (на том же уровне)
//  3 - перейти на уровень вверх (к родителю)
//  4 - если это не листик, то перейти на уровень вниз, а если листик, то выполнить этот пункт  
  switch (menuCommand) {
    case 1:  activeElement = activeElement->goPrev(); menuCommand = 0; break;
    case 2:  activeElement = activeElement->goNext(); menuCommand = 0; break;
    case 3:  activeElement = activeElement->goParent(); menuCommand = 0; break;
    case 4:  activeElement = activeElement->goChildOrRun(); menuCommand = 0; break;
   }

  

  }
   
  } // loop

void Joystic_Read_Programm () { // Считывание показаний джойстика в режиме программирования

        if (map(analogRead(A0), 0, 1023, 0, 255) > 60 && map(analogRead(A0), 0, 1023, 0, 255) < 210) {
          bttnPressed =  false;
        }
        if (map(analogRead(A1), 0, 1023, 0, 255) > 60 && map(analogRead(A1), 0, 1023, 0, 255) < 210) {
          bttnPressed =  false;
        }


        if (map(analogRead(A0), 0, 1023, 0, 255) <= 60) {
           if (bttnPressed == false) {
       menuCommand = 4;        // нажата кнопка Enter,
       bttnPressed =  true;
       menu_select();
      #ifdef DEBUG
             Serial.println("Enter");
      #endif            
           }
       
      }
      if (map(analogRead(A0), 0, 1023, 0, 255) >= 210) {
           if (bttnPressed == false) {
       menuCommand = 3;        // нажата кнопка Exit,
       bttnPressed =  true;
       menu_select();
      #ifdef DEBUG
             Serial.println("Exit");
      #endif       
           }

      }
      if (map(analogRead(A1), 0, 1023, 0, 255) <= 60) {
           if (bttnPressed == false) {
       menuCommand = 1;        // нажата кнопка Up,
       bttnPressed =  true;
       menu_select();
      #ifdef DEBUG
             Serial.println("Up");
      #endif       
           }

      }
      if (map(analogRead(A1), 0, 1023, 0, 255) >= 210) {
           if (bttnPressed == false) {
       menuCommand = 2;        // нажата кнопка Down,
       bttnPressed =  true;
       menu_select();
      #ifdef DEBUG
             Serial.println("Down");
      #endif       
           }

      }


}                              // void Joystic_Read_Programm ()

void menu_select() {

   switch (curMenu) {
//------------------------
   case 1:
      Screen_1();
    break;
//------------------------
    case 2:
      Screen_2();
    break;
//------------------------
    case 3:
      Screen_3();
    break;
//------------------------
    case 21:
      Screen_21();
    break;
//------------------------
    case 22:
      Screen_22();
    break;
    
//------------------------
    case 23:
      Screen_23();
    break;
    
//------------------------
    case 24:
      Screen_24();
    break;

//------------------------

    case 211:
      Screen_211();
    break;
//------------------------
    case 212:
      Screen_212();
    break;
    
//------------------------
    case 213:
      Screen_213();
    break;

//------------------------

    case 221:
      Screen_221();
    break;
//------------------------
    case 222:
      Screen_222();
    break;
    
//------------------------
    case 223:
      Screen_223();
    break;

//------------------------

    case 231:
      Screen_231();
    break;
//------------------------
    case 232:
      Screen_232();
    break;
    
//------------------------
    case 233:
      Screen_233();
    break;

//------------------------

    case 241:
      Screen_241();
    break;
//------------------------
    case 242:
      Screen_242();
    break;
    
//------------------------
    case 243:
      Screen_243();
    break;

   }
}

Отрисовку экранов меню вывел в отдельную вкладку



void Screen_Intro() {

    myOLED.begin  ();                                    // Инициируем работу с дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.print  ("Прюветь", OLED_C, 2);                 // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.print  ("Версия 2.0", OLED_C, 5);             // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).

}

void Screen_Program_intro() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.print  ("Режим", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.print  ("настройки", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).

  delay (2000);                                            // ждем 2 секунды

//  Screen_1();

}


void Screen_1() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.invText();
    myOLED.print  ("Меню 1", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 2", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 3", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_2() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 1", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 2", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 3", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_3() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 1", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 2", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 3", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
}

void Screen_21() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.invText();
    myOLED.print  ("Меню 21", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 22", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 23", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_22() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 21", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 22", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 23", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_23() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 22", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 23", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 24", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_24() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 22", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 23", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 24", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
}

void Screen_211() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.invText();
    myOLED.print  ("Меню 211", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 212", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 213", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_212() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 211", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 212", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 213", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_213() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 211", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 212", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 213", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

// ---------==//==-------------

void Screen_221() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.invText();
    myOLED.print  ("Меню 221", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 222", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 223", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_222() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 221", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 222", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 223", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_223() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 221", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 222", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 223", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

// ---------==//==-------------

void Screen_231() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.invText();
    myOLED.print  ("Меню 231", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 232", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 233", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_232() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 231", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 232", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 233", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_233() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 231", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 232", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 233", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

// ---------==//==-------------

void Screen_241() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
    myOLED.invText();
    myOLED.print  ("Меню 241", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 242", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 243", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_242() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 241", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 242", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 243", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

void Screen_243() {

    myOLED.begin  ();                                    // Инициируем работу c дисплеем.
    myOLED.setFont(MediumFontRus);                       // Указываем шрифт который требуется использовать для вывода цифр и текста на первом дисплее.
//    myOLED.invText();
    myOLED.print  ("Меню 241", OLED_C, 1);                  // Выводим текст на первый дисплей по центру 3 строки (высота шрифта 2 строки, он займёт строки 2 и 3).
//    myOLED.invText(false);
    myOLED.invText();
    myOLED.print  ("Меню 242", OLED_C, 4);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
    myOLED.invText(false);
//    myOLED.invText();
    myOLED.print  ("Меню 243", OLED_C, 7);              // Выводим текст на первый дисплей по центру 5 строки (высота шрифта 1 строка).
//    myOLED.invText(false);
}

Понимаю, что с костылями, но работает. Почти.

Переход по пунктам меню при помощи джойстика, подключенного к выводам А0 и А1. Идея - считать командами перемещения по меню крайние положения стиков.

Никак не получается нормализовать перемещение по пунктам в соответствии с нажатиями джойстика.

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

Евгений, можете помочь с оптимизацией меню?

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Я посмотрю, но через несколько дней, сейчас столько всего навалилось по работе.

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

Спасибо!

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

vdm пишет:

Евгений, можете помочь с оптимизацией меню?

А что Вы подразумеваете под "оптимизацией"?

У Вас, насколько я вижу, отводится по 6 переменных типа int/poiter на каждый элемент, включая листья, что соответствует 12 байт для AVR и 24 байта для SAM3/STM32. А, например, здесь: http://arduino.ru/forum/proekty/menyu-dlya-dvukhstrochnogo-displeya на элемент меню приходится только по 6 байт при том, что для листьев память вообще не выделяется. Т.е. в реальном проекте на элемент, включая листья, может расходоваться вообще меньше одного байта памяти. Например, в проекте, над которым я сейчас работаю (и в котором меню уже не двухстрочное, а многострочное со скроллингом), при 948 байтах имеется 1070 листьев.

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

andriano пишет:

А что Вы подразумеваете под "оптимизацией"?

Тут я понимаю упрощение структуры. Или от switch все равно никуда не деться?

andriano пишет:

У Вас, насколько я вижу, отводится по 6 переменных типа int/poiter на каждый элемент, включая листья, что соответствует 12 байт для AVR и 24 байта для SAM3/STM32. А, например, здесь: http://arduino.ru/forum/proekty/menyu-dlya-dvukhstrochnogo-displeya на элемент меню приходится только по 6 байт при том, что для листьев память вообще не выделяется. Т.е. в реальном проекте на элемент, включая листья, может расходоваться вообще меньше одного байта памяти. Например, в проекте, над которым я сейчас работаю (и в котором меню уже не двухстрочное, а многострочное со скроллингом), при 948 байтах имеется 1070 листьев.

Станно, но я вроде в меню не добавлял переменных... И перекидывать на другой проект... Ну это как-бы... А чего сразу туда не направили?

b707
Offline
Зарегистрирован: 26.05.2017

vdm пишет:

перекидывать на другой проект... Ну это как-бы... А чего сразу туда не направили?

а что такого? Зачем тупо повторять за кем-то? чтобы понять общие принципы - полезно изучить 5-6 разных проектов

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

b707 пишет:

а что такого? Зачем тупо повторять за кем-то? чтобы понять общие принципы - полезно изучить 5-6 разных проектов

Вот знаете, я пытался изучить несколько разных проектов. Но 99% - это тупо swich и перелистываение на следующую страницу, ну, или обратно. По факту - один и тот-же проект с энкодером, который повторяется разными повторителями с разной степенью умения делать новое из чужого скеча... В данном проекте нашел то, что мне нужно и решил попытаться своим скудным умишком допилить его до нужного мне состояния. Уперся в стенку, получил направление в другой проект...

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

vdm пишет:
Но 99% - это тупо swich и перелистываение на следующую страницу, ну, или обратно.

Угу. Это как читатель комиксов решил начать перейти на книги и потом жалуется,как можно читать книги, ведь там нет картинок, одни буквы. И да меню  это тупо swich, так же само как программы ветвления это тупо if.

nik182
Offline
Зарегистрирован: 04.05.2015

Квон! С возвращением!

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

qwone, welcome back!

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

qwone пишет:

vdm пишет:
Но 99% - это тупо swich и перелистываение на следующую страницу, ну, или обратно.

Угу. Это как читатель комиксов решил начать перейти на книги и потом жалуется,как можно читать книги, ведь там нет картинок, одни буквы. И да меню  это тупо swich, так же само как программы ветвления это тупо if.

Ух как по тебе тут соскучились.

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

Хотел написать очередной опус о помощи и поддержке знаниями, но понял, что мое мнение, в принципе, кроме меня никого не интересует, ничего писать не буду.

 

Квон! я тоже рад тебя видеть!

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

Хуже нет пытаться разобраться в чужом коде, если половину не понимаешь.

Нужен исполнитель. Готов оплатить работу мозха. Писать на мой ник 161@mail.ru

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

qwone пишет:

И да меню  это тупо swich

Разве?

Специально посмотрел:  том проекте, на который я ссылался выше, нет ни одного switch. Ни в одном из 16 файлов, включая "обвязку". (пост №161 этой темы)

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016
Идет Илья муромец по дороге, видит пещеру, подходит и говорит:
- Выходи Змей из пещеры, драться будем!
- Ну драться, так драться, а зачем мне в жопу орать!

vdm пишет:
Хотел написать очередной опус о помощи и поддержке знаниями, но понял, что мое мнение, в принципе, кроме меня никого не интересует, ничего писать не буду.

Скорее Вы как Илья Муромец приступили к задаче но не видите ее целиком. То что меню для Ардуино написать можно, вопросов нет. Но тем кому надо это меню тупо не имеют полного багажа знаний и даже не представляют объем этих знаний. И даже если вывалить рабочий код, то они в лесу не увидят леса. Несколько десятков деревьев да, а целиком нет. Вот и придется писать под компьютер графическую оболочку которое мышеклик переведет в код меню. Вот почему Nextoin нарисовала свою оболочку. Вот и вам надо такое же но под ваше железо. Но стоимость такой работы вы не потяните, 

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

andriano пишет:
Разве?
Во-во. Именно так. Мне не хочется разжевывать чем отличается программирование под МК от программированием под ПК.  Так у вас 2 вариант. Да он работает. И все же этот вариант использовать под МК + флешка. Иначе нафиг такой паровоз.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

qwone пишет:

andriano пишет:
Разве?
Во-во. Именно так. Мне не хочется разжевывать чем отличается программирование под МК от программированием под ПК.  Так у вас 2 вариант. Да он работает. И все же этот вариант использовать под МК + флешка. Иначе нафиг такой паровоз.

Нич-чего не понял.

Можно то же самое, только по-русски?

И при чем здесь флешка?

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

qwone пишет:

Но тем кому надо это меню тупо не имеют полного багажа знаний и даже не представляют объем этих знаний.

Поэтому им следует идти в пещеру, в которую Илья Муромец орал. Там светодиодиком мигать будут.

qwone пишет:

И даже если вывалить рабочий код, то они в лесу не увидят леса. Несколько десятков деревьев да, а целиком нет.

А если мышеклику разъяснить что это за странные палки в землю воткнуты, может он и догадается? Хотя нет. Это уже слишком! Путь идет учиться с азов и до самостоятельного написания меню. Только к тому времени меню то будет уже не актуально. Этот проект, к которому я пытаюсь меню прикрутить, у меня длится уже 2 года. Да, за это время можно выучить С++ до очень хорошего уровня, но у меня есть еще и другие цели и задачи в жизни.

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

andriano пишет:

И при чем здесь флешка?

Так надо!

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Кому надо?

vdm
vdm аватар
Offline
Зарегистрирован: 29.08.2016

andriano пишет:

Кому надо?

Автору

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Мда. Вот нафиг я на форум пришел. Очевидные вещи рассказывать.

Что такое программа. Это исполняемый код и некие данные. Ну define, константы, PROGMEMы, "данные" ну и так далее. В общем в исходник инкапсюлированы данные. Когда задачи не большие, то можно обойтись маленьким камнем и простым исполняемым кодом, в котором доля на объем данных минимальна. С ростом объема задач процентная доля данных увеличивается, увеличивается и в конце концов появляется новый язык данных. Конечно на языке данных можно удачно оперировать данными. Но как воткнуть в исходник заточенный под один язык данные форматированные под другой язык. Да andriano ваши структуры это зачаток нового языка данных заточных под меню. Удобно , если решите проблему инкасюляции этого языка.

 Теперь про switch. Здесь все просто  один case это одна страница и все что должно отображено на этой странице инкапсюлируется в это case.

ПС: Если кто ничего не понял, то тупо потрите мои посты и меня нет на форуме.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

qwone пишет:

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

О чем Вы?

Это не только не другой язык (подразумевается, естественно, более высокоуровневый), это даже не Си++, это Ваш любимый "Си с классами". Тот, у которого одна инкапсуляция без полиморфизма и наследования (и который, как показывал Ворота, не уступает "чистому Си" по экономичности и компактности).

Насчет объема данных, как раз их у меня, минимум, в два, а на самом деле - в несколько раз меньше, чем у того варианта, который рассматривается в данной теме. Ну чем не вариант для МК? (кстати, для ПК я пишу по-другому)

Цитата:

 Теперь про switch. Здесь все просто  один case это одна страница и все что должно отображено на этой странице инкапсюлируется в это case.

ПС: Если кто ничего не понял, то тупо потрите мои посты и меня нет на форуме.

Лично я последней фразы (до ПС) не понял. В первую очередь потому, что слово "страница" многозначно, и совершенно непонятно, в каком именно значении Вы его здесь используете. Непонятно, какое отношение имеет страница к оператору case, непонятно, какое отношение имеет страница к меню, а потому совершенно нельзя понять, как Вы себе представляете взаимоотношение между case и меню (следовательно, почемe меню без case невозможно).

Нет, я не придираюсь, я действительно не могу понять.

sadman41
Offline
Зарегистрирован: 19.10.2016

Квон отшатнулся от неправославных лямбд и прильнул к посконному switch. Наверняка ещё отпустил бороду и на завалинке в ушанке посиживает.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

andriano пишет:
Лично я последней фразы (до ПС) не понял. В первую очередь потому, что слово "страница" многозначно, и совершенно непонятно, в каком именно значении Вы его здесь используете. Непонятно, какое отношение имеет страница к оператору case, непонятно, какое отношение имеет страница к меню, а потому совершенно нельзя понять, как Вы себе представляете взаимоотношение между case и меню (следовательно, почемe меню без case невозможно).
Не поняли и ладно. Во многих знаниях есть свои печали. Вот только я не пойму, зачем мне это вам расжевывать. Мне что делать нечего и вам под вас учебник писать. Вы что за эту работу денег много отсыпите. 

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Понятно.

В общем-то правила хорошего тона рекомендуют: Сказал "А", говори "Б". 

А когда так: сказал "А", а когда потребовали объяснений, - сразу в кусты... Некрасиво это. За язык ведь никто не тянул.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

andriano пишет:
В общем-то правила хорошего тона рекомендуют: Сказал "А", говори "Б".

Правило хорошего тона говорит что тон задает сам говорящий.В противном случае, он может A-разболтать лишнее,включая секретные материалы, Б- наболтать себе срок, B-и если правила хорошего тона навязывает тебе тот кто что-то от тебя услышал, то гляди в оба.

leks
Offline
Зарегистрирован: 22.10.2017

Читаю,читаю...Разве меню любое не следствие уже работающего скетча? То есть визуализация значений тех же переменных (или ещё чего там) в ходе исполнения программы. Я к тому, что легче сделать изделие работающее без экрана, потом его добавить, например смартфон через самодельное приложение и блютуз модуль от Ардуино (экран вроде бы есть, а вроде и нет). 

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

leks пишет:
Я к тому, что легче сделать изделие работающее без экрана, потом его добавить, например смартфон через самодельное приложение и блютуз модуль от Ардуино (экран вроде бы есть, а вроде и нет). 

Блин. Ну что это за тупая мысль. Если два скетча работают, то если их объединить они тоже заработают.Сколько тем на этом форуме было.Есть мигающий светодиод, есть скетч с работающей кнопкой. А вот совмещая их выходит фигня. Конечно можно написать скетч где может работать и кнопка и светодиод одновременно, но тогда нет гарантии , что не появится заказ на дополнительную кнопку и светодиод. Вот и приходится так организовывать программу по особым правилам, что бы добавление чего-то, не отбрасывало эту работу в начало. 

Цитата:"смартфон через самодельное приложение и блютуз модуль от Ардуино (экран вроде бы есть, а вроде и нет)." Это что. Делайте автомобили так, что бы любая блондинка любого пола могла сесть за руль. Ну сделали, ну продали. И в результате куча ДТП с травмами и убийствами. 

Понятно я объясняю, что легче сделать и что в результате получить не одно и тоже. 

leks
Offline
Зарегистрирован: 22.10.2017

Ну что за тупая мысль? Сначала мигают светодиодом, потом добавляют кнопку, потом светодиоды, кнопки, потом экран, потом меню на нём про светодиоды. Скетч один, а не два, три, четыре. Смысл меню - в блондинках или в экономии времени, не более. Впрочем я не настаиваю, глупость, так глупость :)   

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

qwone пишет:

andriano пишет:
В общем-то правила хорошего тона рекомендуют: Сказал "А", говори "Б".

Правило хорошего тона говорит что тон задает сам говорящий.В противном случае, он может A-разболтать лишнее,включая секретные материалы, Б- наболтать себе срок, B-и если правила хорошего тона навязывает тебе тот кто что-то от тебя услышал, то гляди в оба.

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

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

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

Мда. Вы со своими взглядами немного погодите. А то так можно дойти, что вы сейчас заявите:"что на ваш взгляд,мы здесь на форуме должны стреляться на дуели на смерть."

Я высказал на форуме свое мнение. Вы высказали свое. А теперь я должен по вашему мнению устраивать дуэли и извиняться. Похоже вас сударь занесло не туда. Обращайтесь к админам или модераторам.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Мне кажется, что "мнение" и "голословные обвинения" это не одно и то же.

Мнение может высказывать каждый, но если оно затрагивает чьи-то интересы, оно должно быть мотивированным.

Клапауций 12345
Offline
Зарегистрирован: 17.05.2020

qwone пишет:

andriano пишет:
В общем-то правила хорошего тона рекомендуют: Сказал "А", говори "Б".

Правило хорошего тона говорит что тон задает сам говорящий.В противном случае, он может A-разболтать лишнее,включая секретные материалы, Б- наболтать себе срок, B-и если правила хорошего тона навязывает тебе тот кто что-то от тебя услышал, то гляди в оба.


таки, да...
японских шпионов полфорума.
у нас на вас по папке на каждого - чуть не в ту сторону пёрнете, за вами быро воронок в четыре утра прилетит.

Клапауций 12345
Offline
Зарегистрирован: 17.05.2020

qwone пишет:

А теперь я должен по вашему мнению устраивать дуэли и извиняться. Похоже вас сударь занесло не туда. Обращайтесь к админам или модераторам.

администгация!

дуэльные пистолеты, секундантов, доктора и священника в тему!

Пух желает стреляться!

Green
Offline
Зарегистрирован: 01.10.2015

Вот так, пух - пух и нет Пуха.(