Снова меню для LCD1602 и 2004 - прошу проверить опытных товарищей

Dinosaur
Dinosaur аватар
Offline
Зарегистрирован: 01.01.2018

Добрый день, корифеи

Попросил родственник сваять ему контроллер нагревателя, нахожусь в начальное стадии реализации проекта. Пришлось "изобрести" меню с навигацией для LCD, который будет применяться в проекте. Код можно условно считать работающим (по крайней мере симуляция в протеусе работает стабильно, ошибок не выдает, и я сам пристрастно тыкал во все что можно на экране). Но поскольку интерес для меня не столько сваять железку, сколько научиться новому, выношу свой "индусский" скетч (пока нарисовал только меню, навигацию и редактирование параметров) на суд опытных товарищей. Прошу посмотреть и подсказать: что сделано не оптимально, что ректально, а что вообще недопустимо. Ну и соответственно прошу задать направления для размышлений (если это сделано плохо, то как это делать правильно). 

Что конкретно смущает меня: пытаюсь разобраться с указателями, не уверен что применяю их правильно; переменные настроек типа bool хочу упаковать в один байт (их как раз 8), но не соображу как это сделать при идеологии использования одной (универсальной) функции editParam ()  (сейчас на мой взгляд красиво, но не оптимально исходя из занимаемой переменными памяти). В общем буду рад любой конструктивной критике.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#define BUZZER_OUT 3                                                                 // buzzer
#define BUTTON_PLUS A0                                                               // +
#define BUTTON_MINUS A1                                                              // - 
#define BUTTON_OK A2                                                                 // OK
#define BUTTON_CANCEL A3                                                             // CANCEL
#define BUTTON_QTY 4                                                                 // количество кнопок

const byte BUTTON_PIN[] PROGMEM = {BUTTON_PLUS, BUTTON_MINUS, BUTTON_OK, BUTTON_CANCEL};     // выводы, к которым подключены кнопки
byte buttonState[BUTTON_QTY];                                                        // состояние кнопок, доступные другим функциям (0 - событие отработано, 1 - короткое нажатие, 2 - длинное нажатие, 3 - удержание);

#define TEMP_DEFAULT 600                                                             // температура по умолчанию 60 градусов (/10)
#define TIME_DEFAULT 60                                                              // время по умолчанию 1 час (в минутах)
volatile int setTime = TIME_DEFAULT;                                                 // таймер приготовления
unsigned int setTemp = TEMP_DEFAULT;                                                 // температура готовки

int EEMEM threePhase_addr;
int EEMEM waterValve_addr;
int EEMEM triacControl_addr;
int EEMEM triacCooling_addr;
int EEMEM pumpControl_addr;
int EEMEM param_6_addr;
int EEMEM param_7_addr;
int EEMEM param_8_addr;

int threePhase;
int waterValve;
int triacControl;
int triacCooling;
int pumpControl;
int param_6;
int param_7;
int param_8;

//Экран
#define DISPLAY_LINES 4                                                              // количество строк дисплея
#define DISPLAY_CHARS 20                                                             // количество символов в строке дисплея
#define DISPLAY_ADDRESS 0x27                                                         // адрес дисплея на шине I2C
#define BACKLIGHT_TIMER_DELAY 30                                                     // задержка гашения подстветки (сек)
LiquidCrystal_I2C lcd(DISPLAY_ADDRESS, DISPLAY_CHARS, DISPLAY_LINES);                // инициализируем библиотеку
byte editChar[8] = {0x4, 0xE, 0x15, 0x4, 0x4, 0x15, 0xE, 0x4};                       // создаем символ редактирования параметра
volatile byte backightTimer;                                                         // переменная таймера задержки отключения дисплея

//Меню
#define MENU_MAX_POSITION 14                                                         // всего пунктов в меню

#define MENU_TYPE_FOLDER 0                                                           // Тип элемента меню - меню
#define MENU_TYPE_INT 1                                                              // Тип элемента меню - целое число
#define MENU_TYPE_TIME 2                                                             // Тип элемента меню - время (HH:MM, целое число, но отображается как время)
#define MENU_TYPE_TEMP 3                                                             // Тип элемента меню - температура (целое число разделенное на 10)
#define MENU_TYPE_BOOL 4                                                             // Тип элемента меню - bool
#define MENU_TYPE_APP 5                                                              // Тип элемента меню - функция

#define MENU_ACTION_UPDATE 0
#define MENU_ACTION_NEXT 1
#define MENU_ACTION_PREVIOUS 2
#define MENU_ACTION_CANCEL 3
#define MENU_ACTION_CONFIRM 4

#define MODE_MENU_NAVIGATION 1
#define MODE_EDIT_PARAM 2

struct menuStruct {
  const byte type;                 // тип элемента
  const byte parent;               // код родителя
  const byte param1;               // child first для MENU_TYPE_FOLDER, increment/decrement slow для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP, MENU_TYPE_BOOL
  const byte param2;               // child last для MENU_TYPE_FOLDER  increment/decrement fast для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP, MENU_TYPE_BOOL
  const int param3;                // min_value для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP, MENU_TYPE_BOOL
  const int param4;                // max_value для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP, MENU_TYPE_BOOL
};

const menuStruct menu[MENU_MAX_POSITION] PROGMEM = {
  {MENU_TYPE_FOLDER, 0, 1,  5,  0,   0},                                                        // 0 - Main menu
  {MENU_TYPE_TIME,   0, 10, 30, 0,   1440},                                                     // 1 - Set time
  {MENU_TYPE_TEMP,   0, 10, 50, 400, 1000},                                                     // 2 - Set temp
  {MENU_TYPE_APP,    0, 0,  0,  0,   0},                                                        // 3 - Start
  {MENU_TYPE_APP,    0, 0,  0,  0,   0},                                                        // 4 - Cancel
  {MENU_TYPE_FOLDER, 0, 6,  13, 0,   0},                                                        // 5 - Settings
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 6 - Param 1
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 7 - Param 2
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 8 - Param 3
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 9 - Param 4
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 10 - Param 5
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 11 - Param 6
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 12 - Param 7
  {MENU_TYPE_BOOL,   5, 1,  1,  0,   1},                                                        // 13 - Param 8
};

const char name_0[] PROGMEM =  "Main menu";
const char name_1[] PROGMEM =  "Set time";
const char name_2[] PROGMEM =  "Set temp";
const char name_3[] PROGMEM =  "Start";
const char name_4[] PROGMEM =  "Cancel";
const char name_5[] PROGMEM =  "Settings";
const char name_6[] PROGMEM =  "3 phase";
const char name_7[] PROGMEM =  "Water valve";
const char name_8[] PROGMEM =  "Triac control";
const char name_9[] PROGMEM =  "Triac cooling";
const char name_10[] PROGMEM = "Pump control";
const char name_11[] PROGMEM = "Param 6";
const char name_12[] PROGMEM = "Param 7";
const char name_13[] PROGMEM = "Param 8";

int* menuInts[][MENU_MAX_POSITION] = {
  {0, &setTime, &setTemp, 0, 0, 0, &threePhase,      &waterValve,      &triacControl,      &triacCooling,      &pumpControl,      &param_6,      &param_7,      &param_8},         // указатель на переменные, которые изменяют функции
  {0, 0,        0,        0, 0, 0, &threePhase_addr, &waterValve_addr, &triacControl_addr, &triacCooling_addr, &pumpControl_addr, &param_6_addr, &param_7_addr, &param_8_addr},    // указатель на адреса переменных в EEPROM
};

const char* const menuNames[] PROGMEM = {
  name_0,  name_1,  name_2,  name_3,  name_4,  name_5,  name_6,  name_7,  name_8, name_9, name_10, name_11, name_12, name_13    // указатель на имена переменных
};

byte menuActualPosition = 1;                                                         // текущее положение в меню
byte menuMode = MODE_MENU_NAVIGATION;                                                // состояние меню

ISR (TIMER1_COMPA_vect)                                                              // функция вызываемая таймером-счетчиком T1
{
  if (backightTimer > 0) {                                                           // задержка отключения подстветки
    backightTimer--;
  }
}

void initTimer1() {                                                                  //  настраиваем таймер 1
  TCCR1B = 0;
  TCCR1A = 0;
  TCCR1B |= (1 << WGM12);                                                            // Режим CTC (сброс по совпадению)
  TCCR1B |= (1 << CS10) | (1 << CS12);                                               // CLK/1024
  OCR1A = 15624;                                                                     // Частота прерываний A будет 1 раз в сек
  TIMSK1 |= (1 << OCIE1A);                                                           // Разрешить прерывание по совпадению A
}

void setup() {

  Serial.begin(9600);
  Serial.println(F("System started"));

  for (byte button = 0; button < BUTTON_QTY; button++) {
    pinMode(pgm_read_byte(&BUTTON_PIN[button]), INPUT_PULLUP);
  }
  pinMode(BUZZER_OUT, OUTPUT);

  for (byte i = 6; i <= 13; i++) {
    int readval = eeprom_read_word(&*menuInts[1][i]);                        // читаем настройки из EEPROM
    if (readval != 0 && readval != 1) {                                      // обрабатываем первый запуск
      readval = 0;
    }
    *menuInts[0][i] = readval;
  }

  initTimer1();

  lcd.init();
  lcd.createChar(0, editChar);
  //  lcd.backlight();
  //  lcd.clear();
  //  lcd.print(F("SousVide ver.1.0"));
  //  lcd.setCursor(1, 1);
  //  lcd.print(F("System started"));
  //  backightTimer = BACKLIGHT_TIMER_DELAY;
  //  delay(1000);
  //  menuDraw(0);

}

void beep(int lenght) {                                                              // ФУНКЦИЯ УПРАВЛЕНИЯ ДИНАМИКОМ
  tone(BUZZER_OUT, 3000, lenght);                                                    // воспроизводим сигнал частотой 3 кгц заданной длительностью
}

void menuDraw(byte value) {

  byte menuFirstLine = pgm_read_byte(&menu[pgm_read_byte(&menu[menuActualPosition].parent)].param1);                                                  // отбираем пунты меню для отображения на текущем уровне
  byte menuLastLine = pgm_read_byte(&menu[pgm_read_byte(&menu[menuActualPosition].parent)].param2);
  static byte displayFirstLine = menuFirstLine;                                                                                                       // первый отображающийся на экране пукт меню

  if (value == MENU_ACTION_NEXT) {                                                                                                                    // обрабатываем +

    if (menuMode == MODE_MENU_NAVIGATION) {                                                                                                           // если мы в режиме навигации
      menuActualPosition++;                                                                                                                           // переходим к следующему пункту меню

      if (menuActualPosition >= displayFirstLine + DISPLAY_LINES) {                                                                                   // прокрутка вперед
        displayFirstLine++;
      }

      if (menuActualPosition > menuLastLine) {                                                                                                        // обрабатываем переход выше максимального значения
        menuActualPosition = menuFirstLine;
        displayFirstLine = menuFirstLine;
      }
    }
  }

  if (value == MENU_ACTION_PREVIOUS) {                                                                     // обрабатываем декремент

    if (menuMode == MODE_MENU_NAVIGATION) {                                                                // если мы в режиме навигации
      menuActualPosition--;                                                                                // возвращаемся к предыдущему пункту меню
      if (menuActualPosition < displayFirstLine) {                                                         // прокрутка назад
        displayFirstLine--;
      }

      if (menuActualPosition < menuFirstLine) {                                                            // обрабатываем переход ниже минимального значения
        menuActualPosition = menuLastLine;
        displayFirstLine = menuLastLine - DISPLAY_LINES + 1;
      }
    }

  }

  if (value == MENU_ACTION_CONFIRM) {                                                                      // обрабатываем OK
    if (pgm_read_byte(&menu[menuActualPosition].type) == MENU_TYPE_FOLDER) {                               // если пункт меню = папка
      menuActualPosition = pgm_read_byte(&menu[menuActualPosition].param1);                                // переходим к первому дочернему пункту меню
      displayFirstLine = menuActualPosition;
    } else if (pgm_read_byte(&menu[menuActualPosition].type) != MENU_TYPE_FOLDER && pgm_read_byte(&menu[menuActualPosition].type) != MENU_TYPE_APP) { // если пукт меню не папка и не приложение
      editParam(101);                                                                                      // запускаем редактирование параметра
    }
  }

  if (value == MENU_ACTION_CANCEL) {                                                                       // обрабатываем CANCEL
    if (menuMode == MODE_MENU_NAVIGATION) {                                                                // если мы в режиме навигации
      if (pgm_read_byte(&menu[menuActualPosition].parent) > 0) {                                           // если у пункта меню есть родитель
        menuActualPosition = pgm_read_byte(&menu[menuActualPosition].parent);                              // возврат к родителю
        displayFirstLine = menuActualPosition - DISPLAY_LINES + 1;
      }
    }
  }

  for (byte i = 0; i < DISPLAY_LINES; i++) {

    String displayString;                                                                                  // тут формируем строку для вывода на экран

    if (i + displayFirstLine == menuActualPosition && menuMode == MODE_MENU_NAVIGATION) {                  // рисуем указатель для активного пункта меню в режиме навигации
      displayString = String("\x7E");
    } else {
      displayString = String(" ");
    }

    char arrayBuf[DISPLAY_CHARS];                                                                          // создаём для считывания наименования из Progmem
    strcpy_P(arrayBuf, pgm_read_byte(&(menuNames[i + displayFirstLine])));                                 // считываем наименование из Progmem

    displayString += String(arrayBuf);                                                                     // добавляем название пункта меню

    if (pgm_read_byte(&menu[i + displayFirstLine].type) == MENU_TYPE_TIME) {                               // Тип элемента меню - время (HH:MM, целое число, но отображается как время)
      byte displayHours = *menuInts[0][i + displayFirstLine]  / 60;                                        // раскладываем время на часы
      byte displayMins = *menuInts[0][i + displayFirstLine] % 60;                                          // и минуты
      byte timeLenght;                                                                                     // считаем количество символов во времени
      if (displayHours < 10) {
        timeLenght = 4;
      } else {
        timeLenght = 5;
      }

      byte totalSpaces = DISPLAY_CHARS - displayString.length() - timeLenght;                              // считаем требуемое кол-во пробелов для выравнимания строки по ширине экрана
      for (byte i = 0; i < totalSpaces; i++) {                                                             // добавляем требуемое количество пробелов после названия
        displayString += String(" ");
      }

      displayString += (String(displayHours) + String(":"));                                               // добавляем количество часов и разделитель

      if (displayMins == 0) {                                                                              // добавляем количество минут

        displayString += (String(displayMins) + String("0"));
      } else if
      (displayMins > 0 && displayMins < 10 ) {
        displayString += (String("0") + String(displayMins));
      } else {
        displayString += String(displayMins);
      }
    }

    if (pgm_read_byte(&menu[i + displayFirstLine].type) == MENU_TYPE_TEMP) {                               // Тип элемента меню - темература (XXX°С, целое число)
      byte displayTemp = *menuInts[0][i + displayFirstLine]  / 10;                                         // считаем температуру для отображения на экране
      byte tempLenght;                                                                                     // считаем количество символов в температуре для отображения на экране
      if (displayTemp < 10) {
        tempLenght = 3;
      } else if (displayTemp >= 10 && displayTemp < 100) {
        tempLenght = 4;
      } else {
        tempLenght = 5;
      }

      byte totalSpaces = DISPLAY_CHARS - displayString.length() - tempLenght;                              // считаем требуемое кол-во пробелов для выравнимания строки по ширине экрана

      for (byte i = 0; i < totalSpaces; i++) {                                                             // добавляем требуемое количество пробелов после названия
        displayString += String(" ");
      }
      displayString += (String(displayTemp) + String("\xDF") + String("C"));                               // добавляем температуру, знак градуса и символ температуры
    }

    if (pgm_read_byte(&menu[i + displayFirstLine].type) == MENU_TYPE_BOOL) {                               // Тип элемента меню - bool

      byte boollenght;
      String boolstring;

      if (*menuInts[0][i + displayFirstLine])  {
        boollenght = 3;
        boolstring = "YES";
      } else {
        boollenght = 2;
        boolstring = "NO";
      }

      byte totalSpaces = DISPLAY_CHARS - displayString.length() - boollenght;                              // считаем требуемое кол-во пробелов для выравнимания строки по ширине экрана

      for (byte i = 0; i < totalSpaces; i++) {                                                             // добавляем требуемое количество пробелов после названия
        displayString += String(" ");
      }
      displayString += String(boolstring);                                                                 // добавляем YES или NO
    }

    if  (i == 0 ) {                                                                                        // если выводим первую строку
      lcd.clear();                                                                                         // очищаем экран
    }
    lcd.setCursor(0, i);                                                                                   // ставим курсор в начало строки
    lcd.print(displayString);

    if (i + displayFirstLine == menuActualPosition && menuMode != MODE_MENU_NAVIGATION) {                  // рисуем указатель для редактируемого пункта меню
      lcd.setCursor(0, i);
      lcd.write((byte)0);
    }
  }

  if (backightTimer <= 1) {                                                                        //если подсветка не включена
    lcd.backlight();                                                                               // включаем подсветку
  }
  backightTimer = BACKLIGHT_TIMER_DELAY;                                                           // перезапускаем тамймер отключения подсветки
}

void editParam (int value) {
  //  Serial.print(F("editParam - "));
  //  Serial.println(value);
  /*   101 - вход в режим редактирования параметра и обновление экрана
       201 - выход из режима редактирования параметра с сохранением изменения
       301 - выход из режима редактирования параметра с отменой сделанных изменений
  */
  static int previousParamValue = 0;                              // тут будем запоминать состояние переменной для отмены сделанных изменений

  if (value == 101) {                                             // Переход в режим настройки
    menuMode = MODE_EDIT_PARAM;                                   // ставим флаг редактирования параметров
  }
  else if (value == 201) {                                        // OK - выход из режима настройки
    previousParamValue = 0;                                       // сбрасываем переменную

    Serial.print("address - ");
    Serial.print(*menuInts[1][menuActualPosition]);
    Serial.print(", data - ");
    Serial.println(*menuInts[0][menuActualPosition]);

    eeprom_update_word(&*menuInts[1][menuActualPosition], *menuInts[0][menuActualPosition]);

    menuMode = MODE_MENU_NAVIGATION;                              // сбрасываем флаг редактирования параметров
  }
  else if (value == 301) {                                        // CANCEL - выход с отменой сделанных изменений
    *menuInts[0][menuActualPosition] = previousParamValue;           // отменяем сделанные изменения
    previousParamValue = 0;                                       // сбрасываем переменную
    menuMode = MODE_MENU_NAVIGATION;                              // возвращаемся в меню
  } else {
    if (!previousParamValue) {                                    // если переменная пустая
      previousParamValue = *menuInts[0][menuActualPosition];         // запомнили значение переменной до редактирования параметра
    }
    *menuInts[0][menuActualPosition] += value;                       // увеличили/уменьшили состояние переменной

    if (*menuInts[0][menuActualPosition] > (int)pgm_read_word(&menu[menuActualPosition].param4)) {
      *menuInts[0][menuActualPosition] = pgm_read_word(&menu[menuActualPosition].param3);
    }
    if (*menuInts[0][menuActualPosition] < (int)pgm_read_word(&menu[menuActualPosition].param3)) {
      *menuInts[0][menuActualPosition] = pgm_read_word(&menu[menuActualPosition].param4);
    }
  }
  menuDraw(0);                                                    // обновляем картинку
}


bool buttonRead () {                                                                                                                 // функция чтения кнопок
#define BUTTON_DEBOUNCE 100                                                                                                          // антидребезг
#define BUTTON_LONG_PRESS 2000                                                                                                       // время длинного нажатия кнопки
  byte returnvalue = 0;                                                                                                              // флаг нажатия кнопки
  static bool _buttonINTstate[BUTTON_QTY];                                                                                           // состояние кнопки (нажата, не нажата)
  static bool _buttonTMPstate[BUTTON_QTY];                                                                                           // временная переменная (нажата, не нажата - пока не определись дребезг или нажатие)
  static bool _buttonLongPress[BUTTON_QTY];                                                                                          // флаг длинного нажатия кнопки
  static unsigned long _buttonChangeStateTime[BUTTON_QTY];                                                                           // время изменения состояния кнопки
  unsigned long actualTime = millis();                                                                                               // запоминаем время вызова функции

  for (byte buttonNumber = 0; buttonNumber < BUTTON_QTY; buttonNumber++) {

    bool buttonNewState = !digitalRead(pgm_read_byte(&BUTTON_PIN[buttonNumber]));                                                                    // читаем состояние кнопки

    if  (_buttonTMPstate[buttonNumber] != buttonNewState) {                                                                          // если состояние кнопки изменилось
      _buttonChangeStateTime[buttonNumber] = actualTime;                                                                             // запомнили время изменения состояния
      _buttonTMPstate[buttonNumber] = buttonNewState;                                                                                // и запомнили новое состояние кнопки во временной переменной (вдруг дребезг)
    }

    if  (!_buttonINTstate[buttonNumber] && buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_DEBOUNCE)) {// если кнопка не была нажата ранее, но осталась нажата в течение времени больше чем дребезг
      _buttonINTstate[buttonNumber] = buttonNewState;                                                                                // ставим флаг нажатия кнопки
      buttonState[buttonNumber] = 1;                                                                                                 // ставим флаг короткого нажатия кнопки для внешней обработки
      returnvalue = 1;                                                                                                               // событие
    }

    if  (!_buttonLongPress[buttonNumber] && _buttonINTstate[buttonNumber] && buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_LONG_PRESS)) {
      _buttonLongPress[buttonNumber] = buttonNewState;                                                                               // ставим флаг нажатия кнопки
      buttonState[buttonNumber] = 2;                                                                                                 // ставим флаг длинного нажатия кнопки для внешней обработки
      returnvalue = 1;                                                                                                               // событие
    }

    if  (buttonLongPress[buttonNumber] && _buttonINTstate[buttonNumber] && buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_LONG_PRESS + 100)) {
      //      _buttonLongPress[buttonNumber] = buttonNewState;                                                                               // ставим флаг нажатия кнопки
      buttonState[buttonNumber] = 3;                                                                                                 // ставим флаг длинного нажатия кнопки для внешней обработки
      returnvalue = 1;                                                                                                               // событие
    }

    if  (_buttonINTstate[buttonNumber] && !buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_DEBOUNCE)) {
      _buttonINTstate[buttonNumber] = buttonNewState;
      _buttonLongPress[buttonNumber] = buttonNewState;
      if (buttonState[buttonNumber] == 3) {
        buttonState[buttonNumber] = 0;
        returnvalue = 1;                                                                                                               // событие
      }
    }
  }
  return returnvalue;
}

void buttonShortPress(byte buttonNumber) {
  beep(50);                                                 // пикаем коротко

  switch (buttonNumber) {

    case 0:                                                 // кнопка "+"
      if (menuMode == MODE_MENU_NAVIGATION) {               // если бродим по меню
        menuDraw(MENU_ACTION_PREVIOUS);                         // переход к следуюшему пункту
      } else if (menuMode == MODE_EDIT_PARAM) {             // если настраиваем параметр
        editParam(pgm_read_byte(&menu[menuActualPosition].param1));         // увеличиваем значение на один шаг
      }
      break;

    case 1:                                                 // кнопка "-"
      if (menuMode == MODE_MENU_NAVIGATION) {               // если бродим по меню
        menuDraw(MENU_ACTION_NEXT);                         // переход к предыдущему пункту
      } else if (menuMode == MODE_EDIT_PARAM) {             // если настраиваем параметр
        editParam(-pgm_read_byte(&menu[menuActualPosition].param1));        // уменьшаем значение на один шаг
      }
      break;

    case 2:                                                 // кнопка "OK"
      if (menuMode == MODE_MENU_NAVIGATION) {               // если бродим по меню
        menuDraw(MENU_ACTION_CONFIRM);                      // входим в пункт меню
      } else if (menuMode == MODE_EDIT_PARAM) {             // если настраиваем параметр
        editParam(201);                                     // сохраняем сделанные изменения
      }
      break;

    case 3:                                                 // кнопка "CANCEL"
      if (menuMode == MODE_MENU_NAVIGATION) {               // если бродим по меню
        menuDraw(MENU_ACTION_CANCEL);                       // возвращаемся к предыдущему пункту
      } else if (menuMode == MODE_EDIT_PARAM) {             // если настраиваем параметр
        editParam(301);                                     // отменяем сделанные изменения
      }
      break;
  }
  buttonState[buttonNumber] = 0;
}

void buttonLongPress(byte buttonNumber) {
  //  beep(300);                                              // пикаем длинно
  buttonState[buttonNumber] = 0;
}

void buttonHold(byte buttonNumber) {
#define COUNT_DELAY 1000                                      // задержка между счетом
  static unsigned long changeTime;                            // время изменения меременной
  unsigned long actualTime = millis();                        // запоминаем время вызова функции

  if  (actualTime - changeTime > COUNT_DELAY) {               // если задержка вышла
    beep(10);                                                 // пикаем коротко
    switch (buttonNumber) {
      case 0:                                                 // кнопка "+"
        if (menuMode == MODE_EDIT_PARAM) {                    // если настраиваем параметр
          editParam(pgm_read_byte(&menu[menuActualPosition].param2));         // увеличиваем значение на один быстрый шаг
        }
        break;

      case 1:                                                 // кнопка "-"
        if (menuMode == MODE_EDIT_PARAM) {                    // если настраиваем параметр
          editParam(-pgm_read_byte(&menu[menuActualPosition].param1));        // уменьшаем значение на быстрыйодин шаг
        }
        break;
    }
    changeTime = actualTime;
  }
}

void loop() {

  if (buttonRead()) {                                                                                                                // если было событие нажатия кнопки
    for (byte buttonNumber = 0; buttonNumber < BUTTON_QTY; buttonNumber++) {
      if (buttonState[buttonNumber] == 1) {                                                                                          // если была коротко нажата кнопка
        buttonShortPress(buttonNumber);                                                                                              // запускаем обработчик нажатия кнопок
      }
      if (buttonState[buttonNumber] == 2) {
        buttonLongPress(buttonNumber);                                                                                              // запускаем обработчик нажатия кнопок
      }
      if (buttonState[buttonNumber] == 3) {
        buttonHold(buttonNumber);                                                                                              // запускаем обработчик нажатия кнопок
      }
    }
  }

  if (backightTimer == 1) {
    lcd.noBacklight();
  }


}

 

-NMi-
Offline
Зарегистрирован: 20.08.2018

Классно.    Ачо "кнопки" в одну функцию не свёл?      И, скока занимает памяти скетч?

Dinosaur
Dinosaur аватар
Offline
Зарегистрирован: 01.01.2018

-NMi- пишет:
Классно.    Ачо "кнопки" в одну функцию не свёл?

Иногда использую аналоговые кнопки, тогда просто меняю функцию считывания на другую, а обработка событий (вынесенные в отдельную), остаются неизменными

-NMi- пишет:
И, скока занимает памяти скетч?

Скетч использует 10506 байт (32%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 629 байт (30%) динамической памяти, оставляя 1419 байт для локальных переменных. Максимум: 2048 байт.
ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Dinosaur пишет:

переменные настроек типа bool хочу упаковать в один байт (их как раз 8)

Я тут когда-то уже выкладывал маленькую библиотечку для массива из восьми флагов в одном байте (можно и для большего количества флагов в соответствующем количестве байтов если надо - у меня есть).

Библиотечка вот такая (файл называется "BoolArray.h")

#ifndef	BOOLARRAY_H
#define BOOLARRAY_H

struct CBoolArray {
	struct IntBool {
		byte & bt, mask;
		inline IntBool(byte & _bt, const byte _mask) : bt(_bt), mask(_mask) {}
		inline operator bool(void) { return 0 != (bt & mask); }
		inline bool operator = (const bool b) { return (bt = b ? bt | mask : bt & ~mask); return b; }
		inline bool operator = (IntBool & other) { return static_cast<bool>(*this = static_cast<bool>(other)); }
	};
	byte m_val;
	inline CBoolArray(void) { m_val = 0; }
	inline IntBool operator [] (const int n) { return IntBool (m_val, 1 << n); }
};

#endif	//	BOOLARRAY_H

Вот пример/тест использования (в примере для упрощения печати используется библиотека Printing)

#include <Printing.h>
#include "BoolArray.h"

static const int8_t totalBools = 8;

static CBoolArray booleanArray;

//
// Печать массива.
// Обратите внимание, что для печати "TRUE" и "FALSE" мы используем 
//	элемент массива в качестве bool в операторе "?"
//
void printArray(const char * title) {
	Serial << "*** " << title << "\r\n";
	for (int8_t i = 0; i < totalBools; i++) {
		Serial << "booleanArray[" << i << "]=" 
			<< (booleanArray[i] ? "TRUE" : "FALSE") << "\r\n";
	}
	Serial << "-------------------------------\r\n";
}

void setup(void) {
	Serial.begin(57600);
	// 
	// Убедимся, что размер правильный
	printVar(sizeof(booleanArray));
	//
	//	Печатаем массив. Там сначала все false
	printArray("All elements are FALSE");
	//
	//	Делаем все true и опять печатаем
	for (int8_t i = 0; i < totalBools; booleanArray[i] = true, ++i);
	printArray("All elements are TRUE");
	//
	//	Делаем чётные элементы false, а нечётные - true
	for (int8_t i = 0; i < totalBools; booleanArray[i] = (i % 2), ++i);
	printArray("Even elements are FALSE while odd elements are TRUE");
	//
	//	Присваивание друг другу
	booleanArray[0] = booleanArray[1];
	printArray("The zero element is equal to the first");
}

void loop(void) {}
Dinosaur
Dinosaur аватар
Offline
Зарегистрирован: 01.01.2018

Евгений, добрый день. Рад что Вы присоединились. Посмотрите пожалуйста на этот кусок:

int* menuInts[][MENU_MAX_POSITION] = {
  {0, &setTime, &setTemp, 0, 0, 0, &threePhase,      &waterValve,      &triacControl,      &triacCooling,      &pumpControl,      &param_6,      &param_7,      &param_8},         // указатель на переменные, которые изменяют функции
  {0, 0,        0,        0, 0, 0, &threePhase_addr, &waterValve_addr, &triacControl_addr, &triacCooling_addr, &pumpControl_addr, &param_6_addr, &param_7_addr, &param_8_addr},    // указатель на адреса переменных в EEPROM
};

У меня ощущение что я с EEMEMом и указателями сильно перемудрил (хотя и работает)?

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

Ну, двухмерный массив указателей - похоже на перемудрение, но я же не знаю, что Вы хотели сделать.

Dinosaur
Dinosaur аватар
Offline
Зарегистрирован: 01.01.2018

Первая строка - указатель на переменную, а вторая стока - указатель на адрес этой же переменной в eeprom (как я писал выше, с указателями я пока на "вы", да и с eemem/progmem тоже). Вот тоже чувствую что это масло масляное, как и вот это:

int EEMEM threePhase_addr;
int EEMEM waterValve_addr;
int EEMEM triacControl_addr;
int EEMEM triacCooling_addr;
int EEMEM pumpControl_addr;
int EEMEM param_6_addr;
int EEMEM param_7_addr;
int EEMEM param_8_addr;

int threePhase;
int waterValve;
int triacControl;
int triacCooling;
int pumpControl;
int param_6;
int param_7;
int param_8;

 

Dinosaur
Dinosaur аватар
Offline
Зарегистрирован: 01.01.2018

Убрал вторую строку из массива указателей (действительно она там нафиг не нужна), убрал кучу int переменных, использовавшихся ранее для хранения bool значений, упаковал bool настройки в один байт:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#define BUZZER_OUT 3                                                                 // buzzer

#define BUTTON_PLUS A0                                                               // +
#define BUTTON_MINUS A1                                                              // - 
#define BUTTON_OK A2                                                                 // OK
#define BUTTON_CANCEL A3                                                             // CANCEL
#define BUTTON_QTY 4                                                                 // количество кнопок

const byte BUTTON_PIN[] PROGMEM = {BUTTON_PLUS, BUTTON_MINUS, BUTTON_OK, BUTTON_CANCEL};     // выводы, к которым подключены кнопки
byte buttonState[BUTTON_QTY];                                                        // состояние кнопок, доступные другим функциям (0 - событие отработано, 1 - короткое нажатие, 2 - длинное нажатие, 3 - удержание);

byte EEMEM settings_addr;                                                            // адрес для хранения настроек bool упакованных в byte
byte settings;                                                                       // настройки устройства bool упакованные в byte

#define SET_3_PHASE 0                                                                // распределяем переменные по битам
#define SET_WATER_VALVE 1
#define SET_TRIAC_CONTROL 2
#define SET_TRIAC_COOLING 3
#define SET_PUMP_CONTROL 4
#define SET_PARAM_6 5
#define SET_PARAM_7 6
#define SET_PARAM_8 7

#define TEMP_DEFAULT 600                                                             // температура по умолчанию 60 градусов (/10)
#define TIME_DEFAULT 60                                                              // время по умолчанию 1 час (в минутах)
volatile int setTime = TIME_DEFAULT;                                                 // таймер приготовления
unsigned int setTemp = TEMP_DEFAULT;                                                 // температура готовки

//Экран
#define DISPLAY_LINES 4                                                              // количество строк дисплея
#define DISPLAY_CHARS 20                                                             // количество символов в строке дисплея
#define DISPLAY_ADDRESS 0x27                                                         // адрес дисплея на шине I2C
#define BACKLIGHT_TIMER_DELAY 30                                                     // задержка гашения подстветки (сек)
LiquidCrystal_I2C lcd(DISPLAY_ADDRESS, DISPLAY_CHARS, DISPLAY_LINES);                // инициализируем библиотеку
byte editChar[8] = {0x4, 0xE, 0x15, 0x4, 0x4, 0x15, 0xE, 0x4};                       // создаем символ редактирования параметра
volatile byte backightTimer;                                                         // переменная таймера задержки отключения дисплея

//Меню
#define MENU_MAX_POSITION 14                                                         // всего пунктов в меню

#define MENU_TYPE_FOLDER 0                                                           // Тип элемента меню - меню
#define MENU_TYPE_INT 1                                                              // Тип элемента меню - целое число
#define MENU_TYPE_TIME 2                                                             // Тип элемента меню - время (HH:MM, целое число, но отображается как время)
#define MENU_TYPE_TEMP 3                                                             // Тип элемента меню - температура (целое число разделенное на 10)
#define MENU_TYPE_BOOL 4                                                             // Тип элемента меню - bool
#define MENU_TYPE_APP 5                                                              // Тип элемента меню - функция

#define MENU_ACTION_UPDATE 0
#define MENU_ACTION_NEXT 1
#define MENU_ACTION_PREVIOUS 2
#define MENU_ACTION_CANCEL 3
#define MENU_ACTION_CONFIRM 4

#define MODE_MENU_NAVIGATION 1
#define MODE_EDIT_PARAM 2

struct menuStruct {
  const byte type;                 // тип элемента
  const byte parent;               // код родителя
  const byte param1;               // child first для MENU_TYPE_FOLDER, increment/decrement slow для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP, номер бита в байте для bool параметров
  const byte param2;               // child last для MENU_TYPE_FOLDER  increment/decrement fast для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP
  const int param3;                // min_value для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP
  const int param4;                // max_value для MENU_TYPE_INT, MENU_TYPE_TIME, MENU_TYPE_TEMP
};

const menuStruct menu[MENU_MAX_POSITION] PROGMEM = {
  {MENU_TYPE_FOLDER, 0, 1,                 5,  0,   0   },                           // 0 - Main menu
  {MENU_TYPE_TIME,   0, 10,                30, 0,   1440},                           // 1 - Set time
  {MENU_TYPE_TEMP,   0, 10,                50, 400, 1000},                           // 2 - Set temp
  {MENU_TYPE_APP,    0, 0,                 0,  0,   0   },                           // 3 - Start
  {MENU_TYPE_APP,    0, 0,                 0,  0,   0   },                           // 4 - Cancel
  {MENU_TYPE_FOLDER, 0, 6,                 13, 0,   0   },                           // 5 - Settings
  {MENU_TYPE_BOOL,   5, SET_3_PHASE,       0,  0,   0   },                           // 6 - 3 phase
  {MENU_TYPE_BOOL,   5, SET_WATER_VALVE,   0,  0,   0   },                           // 7 - Water valve
  {MENU_TYPE_BOOL,   5, SET_TRIAC_CONTROL, 0,  0,   0   },                           // 8 - Triac control
  {MENU_TYPE_BOOL,   5, SET_TRIAC_COOLING, 0,  0,   0   },                           // 9 - Triac cooling
  {MENU_TYPE_BOOL,   5, SET_PUMP_CONTROL,  0,  0,   0   },                           // 10 - Pump control
  {MENU_TYPE_BOOL,   5, SET_PARAM_6,       0,  0,   0   },                           // 11 - Param 6
  {MENU_TYPE_BOOL,   5, SET_PARAM_7,       0,  0,   0   },                           // 12 - Param 7
  {MENU_TYPE_BOOL,   5, SET_PARAM_8,       0,  0,   0   },                           // 13 - Param 8
};

const char name_0[] PROGMEM =  "Main menu";
const char name_1[] PROGMEM =  "Set time";
const char name_2[] PROGMEM =  "Set temp";
const char name_3[] PROGMEM =  "Start";
const char name_4[] PROGMEM =  "Cancel";
const char name_5[] PROGMEM =  "Settings";
const char name_6[] PROGMEM =  "3 phase";
const char name_7[] PROGMEM =  "Water valve";
const char name_8[] PROGMEM =  "Triac control";
const char name_9[] PROGMEM =  "Triac cooling";
const char name_10[] PROGMEM = "Pump control";
const char name_11[] PROGMEM = "Param 6";
const char name_12[] PROGMEM = "Param 7";
const char name_13[] PROGMEM = "Param 8";

const char* const menuNames[] PROGMEM = {
  name_0,  name_1,  name_2,  name_3,  name_4,  name_5,  name_6,  name_7,  name_8, name_9, name_10, name_11, name_12, name_13    // указатель на имена переменных
};

int* menuInts[MENU_MAX_POSITION] = {0, &setTime, &setTemp, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};   // !!! указатель на переменные int, которые изменяют функции

byte menuActualPosition = 1;                                                         // текущее положение в меню
byte menuMode = MODE_MENU_NAVIGATION;                                                // состояние меню


ISR (TIMER1_COMPA_vect)                                                              // функция вызываемая таймером-счетчиком T1
{
  if (backightTimer > 0) {                                                           // задержка отключения подстветки
    backightTimer--;
  }
}

void setup() {
  settings = eeprom_read_byte(&settings_addr);                                       // !!! считываем настройки устройства

  Serial.begin(9600);
  Serial.println(F("System started"));

  for (byte button = 0; button < BUTTON_QTY; button++) {
    pinMode(pgm_read_byte(&BUTTON_PIN[button]), INPUT_PULLUP);
  }

  pinMode(BUZZER_OUT, OUTPUT);

  initTimer1();

  lcd.init();
  lcd.createChar(0, editChar);
  lcd.backlight();
  lcd.clear();
  lcd.print(F("SousVide ver.1.0"));
  lcd.setCursor(1, 1);
  lcd.print(F("System started"));
  backightTimer = BACKLIGHT_TIMER_DELAY;
  delay(1000);
  menuDraw(0);
}

void initTimer1() {                                                                  //  настраиваем таймер 1
  TCCR1B = 0;
  TCCR1A = 0;
  TCCR1B |= (1 << WGM12);                                                            // Режим CTC (сброс по совпадению)
  TCCR1B |= (1 << CS10) | (1 << CS12);                                               // CLK/1024
  OCR1A = 15624;                                                                     // Частота прерываний A будет 1 раз в сек
  TIMSK1 |= (1 << OCIE1A);                                                           // Разрешить прерывание по совпадению A
}

bool readSettingBit (byte numbit) {                                                  // ФУНКЦИЯ ЧТЕНИЯ НАСТРОЕК ИЗ БАЙТА
  bool returnvalue = bitRead(settings, numbit);
  return returnvalue;
}

void beep(int lenght) {                                                              // ФУНКЦИЯ УПРАВЛЕНИЯ ДИНАМИКОМ
  tone(BUZZER_OUT, 3000, lenght);                                                    // воспроизводим сигнал частотой 3 кгц заданной длительностью
}

void menuDraw(byte value) {

  byte menuFirstLine = pgm_read_byte(&menu[pgm_read_byte(&menu[menuActualPosition].parent)].param1);       // отбираем пунты меню для отображения на текущем уровне
  byte menuLastLine = pgm_read_byte(&menu[pgm_read_byte(&menu[menuActualPosition].parent)].param2);
  static byte displayFirstLine = menuFirstLine;                                                            // первый отображающийся на экране пукт меню

  if (value == MENU_ACTION_NEXT) {                                                                         // обрабатываем +

    if (menuMode == MODE_MENU_NAVIGATION) {                                                                // если мы в режиме навигации
      menuActualPosition++;                                                                                // переходим к следующему пункту меню

      if (menuActualPosition >= displayFirstLine + DISPLAY_LINES) {                                        // прокрутка вперед
        displayFirstLine++;
      }

      if (menuActualPosition > menuLastLine) {                                                             // обрабатываем переход выше максимального значения
        menuActualPosition = menuFirstLine;
        displayFirstLine = menuFirstLine;
      }
    }
  }

  if (value == MENU_ACTION_PREVIOUS) {                                                                     // обрабатываем декремент

    if (menuMode == MODE_MENU_NAVIGATION) {                                                                // если мы в режиме навигации
      menuActualPosition--;                                                                                // возвращаемся к предыдущему пункту меню
      if (menuActualPosition < displayFirstLine) {                                                         // прокрутка назад
        displayFirstLine--;
      }

      if (menuActualPosition < menuFirstLine) {                                                            // обрабатываем переход ниже минимального значения
        menuActualPosition = menuLastLine;
        displayFirstLine = menuLastLine - DISPLAY_LINES + 1;
      }
    }

  }

  if (value == MENU_ACTION_CONFIRM) {                                                                      // обрабатываем OK
    if (pgm_read_byte(&menu[menuActualPosition].type) == MENU_TYPE_FOLDER) {                               // если пункт меню = папка
      menuActualPosition = pgm_read_byte(&menu[menuActualPosition].param1);                                // переходим к первому дочернему пункту меню
      displayFirstLine = menuActualPosition;
    } else if (pgm_read_byte(&menu[menuActualPosition].type) != MENU_TYPE_FOLDER && pgm_read_byte(&menu[menuActualPosition].type) != MENU_TYPE_APP) { // если пукт меню не папка и не приложение
      editParam(101);                                                                                      // запускаем редактирование параметра
    }
  }

  if (value == MENU_ACTION_CANCEL) {                                                                       // обрабатываем CANCEL
    if (menuMode == MODE_MENU_NAVIGATION) {                                                                // если мы в режиме навигации
      if (pgm_read_byte(&menu[menuActualPosition].parent) > 0) {                                           // если у пункта меню есть родитель
        menuActualPosition = pgm_read_byte(&menu[menuActualPosition].parent);                              // возврат к родителю
        displayFirstLine = menuActualPosition - DISPLAY_LINES + 1;
      }
    }
  }

  for (byte i = 0; i < DISPLAY_LINES; i++) {

    String displayString;                                                                                  // тут формируем строку для вывода на экран

    if (i + displayFirstLine == menuActualPosition && menuMode == MODE_MENU_NAVIGATION) {                  // рисуем указатель для активного пункта меню в режиме навигации
      displayString = String(F("\x7E"));
    } else {
      displayString = String(" ");
    }

    char arrayBuf[DISPLAY_CHARS];                                                                          // создаём для считывания наименования из Progmem
    strcpy_P(arrayBuf, pgm_read_byte(&(menuNames[i + displayFirstLine])));                                 // считываем наименование из Progmem

    displayString += String(arrayBuf);                                                                     // добавляем название пункта меню

    if (pgm_read_byte(&menu[i + displayFirstLine].type) == MENU_TYPE_TIME) {                               // Тип элемента меню - время (HH:MM, целое число, но отображается как время)
      byte displayHours = *menuInts[i + displayFirstLine]  / 60;                                        // раскладываем время на часы
      byte displayMins = *menuInts[i + displayFirstLine] % 60;                                          // и минуты
      byte timeLenght;                                                                                     // считаем количество символов во времени
      if (displayHours < 10) {
        timeLenght = 4;
      } else {
        timeLenght = 5;
      }

      byte totalSpaces = DISPLAY_CHARS - displayString.length() - timeLenght;                              // считаем требуемое кол-во пробелов для выравнимания строки по ширине экрана
      for (byte i = 0; i < totalSpaces; i++) {                                                             // добавляем требуемое количество пробелов после названия
        displayString += String(" ");
      }

      displayString += (String(displayHours) + String(F(":")));                                               // добавляем количество часов и разделитель

      if (displayMins == 0) {                                                                              // добавляем количество минут

        displayString += (String(displayMins) + String(F("0")));
      } else if
      (displayMins > 0 && displayMins < 10 ) {
        displayString += (String(F("0")) + String(displayMins));
      } else {
        displayString += String(displayMins);
      }
    }

    if (pgm_read_byte(&menu[i + displayFirstLine].type) == MENU_TYPE_TEMP) {                               // Тип элемента меню - темература (XXX°С, целое число)
      byte displayTemp = *menuInts[i + displayFirstLine]  / 10;                                         // считаем температуру для отображения на экране
      byte tempLenght;                                                                                     // считаем количество символов в температуре для отображения на экране
      if (displayTemp < 10) {
        tempLenght = 3;
      } else if (displayTemp >= 10 && displayTemp < 100) {
        tempLenght = 4;
      } else {
        tempLenght = 5;
      }

      byte totalSpaces = DISPLAY_CHARS - displayString.length() - tempLenght;                              // считаем требуемое кол-во пробелов для выравнимания строки по ширине экрана

      for (byte i = 0; i < totalSpaces; i++) {                                                             // добавляем требуемое количество пробелов после названия
        displayString += String(" ");
      }
      displayString += (String(displayTemp) + String(F("\xDF")) + String(F("C")));                         // добавляем температуру, знак градуса и символ температуры
    }

    if (pgm_read_byte(&menu[i + displayFirstLine].type) == MENU_TYPE_BOOL) {                               // Тип элемента меню - bool

      byte boollenght;
      String boolstring;

      if bit_is_set(settings, pgm_read_byte(&menu[i + displayFirstLine].param1)) {                         // если бит включен
        boollenght = 3;
        boolstring = F("YES");
      } else {
        boollenght = 2;
        boolstring = F("NO");
      }

      byte totalSpaces = DISPLAY_CHARS - displayString.length() - boollenght;                              // считаем требуемое кол-во пробелов для выравнимания строки по ширине экрана

      for (byte i = 0; i < totalSpaces; i++) {                                                             // добавляем требуемое количество пробелов после названия
        displayString += String(" ");
      }
      displayString += String(boolstring);                                                                 // добавляем YES или NO
    }

    if  (i == 0 ) {                                                                                        // если выводим первую строку
      lcd.clear();                                                                                         // очищаем экран
    }
    lcd.setCursor(0, i);                                                                                   // ставим курсор в начало строки
    lcd.print(displayString);

    if (i + displayFirstLine == menuActualPosition && menuMode != MODE_MENU_NAVIGATION) {                  // рисуем указатель для редактируемого пункта меню
      lcd.setCursor(0, i);
      lcd.write((byte)0);
    }
  }

  if (backightTimer <= 1) {                                                                                //если подсветка не включена
    lcd.backlight();                                                                                       // включаем подсветку
  }
  backightTimer = BACKLIGHT_TIMER_DELAY;                                                                   // перезапускаем тамймер отключения подсветки
}

void editParam (int value) {
  /*   101 - вход в режим редактирования параметра и обновление экрана
       201 - выход из режима редактирования параметра с сохранением изменения
       301 - выход из режима редактирования параметра с отменой сделанных изменений
  */
  static int previousParamValue = 0;                                                                       // тут будем запоминать состояние переменной для отмены сделанных изменений

  switch (value) {

    case 101:                                                                                              // Переход в режим настройки
      menuMode = MODE_EDIT_PARAM;                                                                          // ставим флаг редактирования параметров
      break;

    case 201:                                                                                              // OK - выход из режима настройки
      previousParamValue = 0;                                                                              // сбрасываем переменную
      if (pgm_read_byte(&menu[menuActualPosition].type) == MENU_TYPE_BOOL) {                               // если редактируемый элемент bool
        eeprom_update_byte(&settings_addr, settings);                                                      // обновляем байт настроек
      }
      menuMode = MODE_MENU_NAVIGATION;                                                                     // сбрасываем флаг редактирования параметров
      break;

    case 301:                                                                                              // CANCEL - выход с отменой сделанных изменений
      if (pgm_read_byte(&menu[menuActualPosition].type) != MENU_TYPE_BOOL) {                               // если редактируемый элемент не bool
        *menuInts[menuActualPosition] = previousParamValue;                                                // отменяем сделанные изменения
      } else {
        bitWrite(settings, pgm_read_byte(&menu[menuActualPosition].param1), previousParamValue);           // отменяем сделанные изменения
      }

      previousParamValue = 0;                                                                              // сбрасываем переменную
      menuMode = MODE_MENU_NAVIGATION;                                                                     // возвращаемся в меню
      break;

    default:                                                                                               // изменение переменной
      if (!previousParamValue) {                                                                           // если переменная пустая
        if (pgm_read_byte(&menu[menuActualPosition].type) != MENU_TYPE_BOOL) {                             // если редактируемый элемент не bool
          previousParamValue = *menuInts[menuActualPosition];                                              // запомнили значение переменной до редактирования параметра
        } else {                                                                                           // если редактируемый элемент bool
          previousParamValue = readSettingBit(pgm_read_byte(&menu[menuActualPosition].param1));            // запомнили состояние бита до редактирования параметра
        }
      }

      if (pgm_read_byte(&menu[menuActualPosition].type) != MENU_TYPE_BOOL) {                               // если тип элемента не bool
        *menuInts[menuActualPosition] += value;                                                            // увеличили/уменьшили состояние переменной
        if (*menuInts[menuActualPosition] > (int)pgm_read_word(&menu[menuActualPosition].param4)) {        // уход выше максимально допустимого значения
          *menuInts[menuActualPosition] = pgm_read_word(&menu[menuActualPosition].param3);
        }
        if (*menuInts[menuActualPosition] < (int)pgm_read_word(&menu[menuActualPosition].param3)) {        // уход ниже минимально допустимого значения
          *menuInts[menuActualPosition] = pgm_read_word(&menu[menuActualPosition].param4);
        }
      } else {                                                                                             // если тип элемента bool
        byte bitNumber = pgm_read_byte(&menu[menuActualPosition].param1);                                  // считываем номер бита в конфигурационном байте
        bitWrite(settings, bitNumber, !readSettingBit(bitNumber));                                         // инвертируем соответствующий бит в байте
      }
  }

  menuDraw(0);                                                                                             // обновляем картинку
}

bool buttonRead () {                                                                                       // функция чтения кнопок
#define BUTTON_DEBOUNCE 100                                                                                // антидребезг
#define BUTTON_LONG_PRESS 2000                                                                             // время длинного нажатия кнопки
  byte returnvalue = 0;                                                                                    // флаг нажатия кнопки
  static bool _buttonINTstate[BUTTON_QTY];                                                                 // состояние кнопки (нажата, не нажата)
  static bool _buttonTMPstate[BUTTON_QTY];                                                                 // временная переменная (нажата, не нажата - пока не определись дребезг или нажатие)
  static bool _buttonLongPress[BUTTON_QTY];                                                                // флаг длинного нажатия кнопки
  static unsigned long _buttonChangeStateTime[BUTTON_QTY];                                                 // время изменения состояния кнопки
  unsigned long actualTime = millis();                                                                     // запоминаем время вызова функции

  for (byte buttonNumber = 0; buttonNumber < BUTTON_QTY; buttonNumber++) {

    bool buttonNewState = !digitalRead(pgm_read_byte(&BUTTON_PIN[buttonNumber]));                          // читаем состояние кнопки

    if  (_buttonTMPstate[buttonNumber] != buttonNewState) {                                                // если состояние кнопки изменилось
      _buttonChangeStateTime[buttonNumber] = actualTime;                                                   // запомнили время изменения состояния
      _buttonTMPstate[buttonNumber] = buttonNewState;                                                      // и запомнили новое состояние кнопки во временной переменной (вдруг дребезг)
    }

    if  (!_buttonINTstate[buttonNumber] && buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_DEBOUNCE)) {// если кнопка не была нажата ранее, но осталась нажата в течение времени больше чем дребезг
      _buttonINTstate[buttonNumber] = buttonNewState;                                                      // ставим флаг нажатия кнопки
      buttonState[buttonNumber] = 1;                                                                       // ставим флаг короткого нажатия кнопки для внешней обработки
      returnvalue = 1;                                                                                     // событие
    }

    if  (!_buttonLongPress[buttonNumber] && _buttonINTstate[buttonNumber] && buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_LONG_PRESS)) {
      _buttonLongPress[buttonNumber] = buttonNewState;                                                     // ставим флаг нажатия кнопки
      buttonState[buttonNumber] = 2;                                                                       // ставим флаг длинного нажатия кнопки для внешней обработки
      returnvalue = 1;                                                                                     // событие
    }

    if  (buttonLongPress[buttonNumber] && _buttonINTstate[buttonNumber] && buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_LONG_PRESS + 100)) {
      //      _buttonLongPress[buttonNumber] = buttonNewState;                                             // ставим флаг нажатия кнопки
      buttonState[buttonNumber] = 3;                                                                       // ставим флаг длинного нажатия кнопки для внешней обработки
      returnvalue = 1;                                                                                     // событие
    }

    if  (_buttonINTstate[buttonNumber] && !buttonNewState && (actualTime - _buttonChangeStateTime[buttonNumber] > BUTTON_DEBOUNCE)) {
      _buttonINTstate[buttonNumber] = buttonNewState;
      _buttonLongPress[buttonNumber] = buttonNewState;
      if (buttonState[buttonNumber] == 3) {
        buttonState[buttonNumber] = 0;
        returnvalue = 1;                                                                                   // событие
      }
    }
  }
  return returnvalue;
}

void buttonShortPress(byte buttonNumber) {
  beep(50);                                                                                                // пикаем коротко

  switch (buttonNumber) {

    case 0:                                                                                                // кнопка "+"
      if (menuMode == MODE_MENU_NAVIGATION) {                                                              // если бродим по меню
        menuDraw(MENU_ACTION_PREVIOUS);                                                                    // переход к следуюшему пункту
      } else if (menuMode == MODE_EDIT_PARAM) {                                                            // если настраиваем параметр
        editParam(pgm_read_byte(&menu[menuActualPosition].param1));                                        // увеличиваем значение на один шаг
      }
      break;

    case 1:                                                                                                // кнопка "-"
      if (menuMode == MODE_MENU_NAVIGATION) {                                                              // если бродим по меню
        menuDraw(MENU_ACTION_NEXT);                                                                        // переход к предыдущему пункту
      } else if (menuMode == MODE_EDIT_PARAM) {                                                            // если настраиваем параметр
        editParam(-pgm_read_byte(&menu[menuActualPosition].param1));                                       // уменьшаем значение на один шаг
      }
      break;

    case 2:                                                                                                // кнопка "OK"
      if (menuMode == MODE_MENU_NAVIGATION) {                                                              // если бродим по меню
        menuDraw(MENU_ACTION_CONFIRM);                                                                     // входим в пункт меню
      } else if (menuMode == MODE_EDIT_PARAM) {                                                            // если настраиваем параметр
        editParam(201);                                                                                    // сохраняем сделанные изменения
      }
      break;

    case 3:                                                                                                // кнопка "CANCEL"
      if (menuMode == MODE_MENU_NAVIGATION) {                                                              // если бродим по меню
        menuDraw(MENU_ACTION_CANCEL);                                                                      // возвращаемся к предыдущему пункту
      } else if (menuMode == MODE_EDIT_PARAM) {                                                            // если настраиваем параметр
        editParam(301);                                                                                    // отменяем сделанные изменения
      }
      break;
  }
  buttonState[buttonNumber] = 0;
}

void buttonLongPress(byte buttonNumber) {
  //  beep(300);                                                                                           // пикаем длинно
  buttonState[buttonNumber] = 0;
}

void buttonHold(byte buttonNumber) {
#define COUNT_DELAY 1000                                                                                   // задержка между счетом
  static unsigned long changeTime;                                                                         // время изменения меременной
  unsigned long actualTime = millis();                                                                     // запоминаем время вызова функции

  if  (actualTime - changeTime > COUNT_DELAY) {                                                            // если задержка вышла
    beep(10);                                                                                              // пикаем коротко
    switch (buttonNumber) {
      case 0:                                                                                              // кнопка "+"
        if (menuMode == MODE_EDIT_PARAM) {                                                                 // если настраиваем параметр
          editParam(pgm_read_byte(&menu[menuActualPosition].param2));                                      // увеличиваем значение на один быстрый шаг
        }
        break;

      case 1:                                                                                              // кнопка "-"
        if (menuMode == MODE_EDIT_PARAM) {                                                                 // если настраиваем параметр
          editParam(-pgm_read_byte(&menu[menuActualPosition].param1));                                     // уменьшаем значение на быстрыйодин шаг
        }
        break;
    }
    changeTime = actualTime;
  }
}

void loop() {


  if (buttonRead()) {                                                                                      // если было событие нажатия кнопки
    for (byte buttonNumber = 0; buttonNumber < BUTTON_QTY; buttonNumber++) {
      if (buttonState[buttonNumber] == 1) {                                                                                          // если была коротко нажата кнопка
        buttonShortPress(buttonNumber);                                                                                              // запускаем обработчик нажатия кнопок
      }
      if (buttonState[buttonNumber] == 2) {
        buttonLongPress(buttonNumber);                                                                                              // запускаем обработчик нажатия кнопок
      }
      if (buttonState[buttonNumber] == 3) {
        buttonHold(buttonNumber);                                                                                              // запускаем обработчик нажатия кнопок
      }
    }
  }

  if (backightTimer == 1) {
    lcd.noBacklight();
  }


}

Результат - Скетч использует 10872 байт (35%) памяти устройства. Всего доступно 30720 байт. Глобальные переменные используют 550 байт (26%) динамической памяти, оставляя 1498 байт для локальных переменных. Максимум: 2048 байт. Вроде и не космическая экономия, но на душе тепло :)