Помощь с редактированием параметров из меню

a_vel

✩✩✩✩✩✩✩
29 Сен 2021
8
1
Всем добрый день! Пишу скетч для автополива двух горшков, из компонентов у меня энкодер с кнопкой (круглый модуль с пинами S1, S2, KEY), LCD 1602 с подключением по i2c и собсно Arduino Uno 328p.

Написал такой скетч:
C++:
#include <Arduino.h>
//#include <EEPROM.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <LiquidCrystal_I2C.h>
#define ENC_A 2
#define ENC_B 4
#define OK_BUTTON 3
#define DHT_PIN 6
#define DHTTYPE DHT22
DHT dht(DHT_PIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27,16,2);
volatile int encCounter;
volatile boolean flag, resetFlag;
volatile uint8_t curState, prevState;
char date[] = "21/10/05";
char time[] = "16:20";
float t_air = 0;
uint8_t hum_air = 0;
uint8_t hum_left = 0;
uint8_t hum_right = 0;
uint8_t hum_goal = 0;
uint8_t dur_goal = 0;
uint32_t timer;
bool param_edit = 0;
uint8_t page_id = 0;
uint8_t prev_page_id = 0;
uint8_t prev_item_ptr = 0;
uint8_t item_pointer = 0;
char menu_pointer = '>';
char param_pointer = '<';
char menu_blank = ' ';
char menu_items[][12] = {
  "Date: ",
  "Time: ",
  "T. air: ",
  "H. air: ",
  "H. L.pot: ",
  "H. R.pot: ",
  "Hum. Goal: ",
  "Dur. Goal: ",
};

void encTick() {
  curState = digitalRead(ENC_A) | digitalRead(ENC_B) << 1;
  if (resetFlag && curState == 0b11) {
    if (prevState == 0b10) encCounter++;
    if (prevState == 0b01) encCounter--;
    resetFlag = 0;
    flag = true;
  }
  if (curState == 0b00) resetFlag = 1;
  prevState = curState;
}
void int0() {
  encTick();
}
void readDht() {
  if (millis() - timer >= 2100) {
    hum_air = dht.readHumidity();
    t_air = dht.readTemperature();
    timer = millis();
  }
}
/*
Функция getItemPointer меняет значение переменных item_pointer и prev_item_ptr.
Если флаг param_edit установлен (была нажата кнопка энкодера), значит контроллер
перешёл в режим редактирования параметра. В этом режиме item_pointer всегда равен
prev_item_pointer. Это обеспечивает блокирование скролла меню, если указатель
меню установлен в 15,0 / 15,1 для редактирования параметра. encCounter при переходе
в режим редактирования параметра сбрасывается в prev_item_ptr -- иначе при выходе
из режима редактирования параметра меню может отрисоваться на произвольной странице.
*/
void getItemPointer() {
  if (!param_edit) {
    if (encCounter > 7) {
      item_pointer = 0;
      encCounter = 0;
    }
    else if (encCounter < 0) {
      item_pointer = 7;
      encCounter = 7;
    }
    else {
      item_pointer = encCounter;
    }
  }
  else {
    encCounter = prev_item_ptr;
    item_pointer = prev_item_ptr;
  }
}
void printMenuPage(){                                    // Prints pages of menu. Each page contains 2 items (rows).
  if (item_pointer >= 0 && item_pointer <= 1) {          // Page 1: Date & Time
    lcd.setCursor(1,0);
    lcd.print(menu_items[0]);
    lcd.print(date);
    lcd.setCursor(1,1);
    lcd.print(menu_items[1]);
    lcd.print(time);
    page_id = 0;
  }
  if (item_pointer >= 2 && item_pointer <= 3) {          // Page 2: Humidity Air & Temp Air
    char t_air_char[4];
    dtostrf(t_air, sizeof(t_air_char), 1, t_air_char);
    lcd.setCursor(1,0);
    lcd.print(menu_items[2]);
    lcd.print(t_air_char);
    lcd.print("*C");
    lcd.setCursor(1,1);
    lcd.print(menu_items[3]);
    lcd.print(hum_air);
    lcd.print("%");
    page_id = 1;
  }
  if (item_pointer >= 4 && item_pointer <= 5) {          // Page 3: Humidity Left Pot & Humidity Right Pot
    lcd.setCursor(1,0);
    lcd.print(menu_items[4]);
    lcd.print(hum_left);
    lcd.print("%");
    lcd.setCursor(1,1);
    lcd.print(menu_items[5]);
    lcd.print(hum_right);
    lcd.print("%");
    page_id = 2;
  }
  if (item_pointer >= 6 && item_pointer <= 7) {          // Page 4: Pot Humidity Goal & Daylight Duration Goal
    lcd.setCursor(1,0);
    lcd.print(menu_items[6]);
    lcd.print(hum_goal);
    lcd.print("%");
    lcd.setCursor(1,1);
    lcd.print(menu_items[7]);
    lcd.print(dur_goal);
    lcd.print("h");
    page_id = 3;
  }
}
/*
Функция printPointer отрисовывает один из двух видов указателей меню: ">" или "<".
Вид указателя зависит от булевой переменной param_edit. Если param_edit = false, то
печатается обычный указатель меню слева: ">". Если true, печатается указатель
редактируемого параметра "<".
*/
void printPointer() {
  if (!param_edit) {
    if (item_pointer % 2 == 0) {
        lcd.setCursor(0, 0);
        lcd.print(menu_pointer);
        lcd.setCursor(0, 1);
        lcd.print(menu_blank);
        lcd.setCursor(15, 0);
        lcd.print(menu_blank);
        lcd.setCursor(15, 1);
        lcd.print(menu_blank);
      }
    else {
        lcd.setCursor(0, 0);
        lcd.print(menu_blank);
        lcd.setCursor(0, 1);
        lcd.print(menu_pointer);
        lcd.setCursor(15, 0);
        lcd.print(menu_blank);
        lcd.setCursor(15, 1);
        lcd.print(menu_blank);     
      }
  }
  else {
    if (item_pointer % 2 == 0) {
      lcd.setCursor(0,0);
      lcd.print(menu_blank);
      lcd.setCursor(0,1);
      lcd.print(menu_blank);
      lcd.setCursor(15, 0);
      lcd.print(param_pointer);
      lcd.setCursor(15, 1);
      lcd.print(menu_blank);
    }
    else {
      lcd.setCursor(0,0);
      lcd.print(menu_blank);
      lcd.setCursor(0,1);
      lcd.print(menu_blank);
      lcd.setCursor(15, 0);
      lcd.print(menu_blank);
      lcd.setCursor(15, 1);
      lcd.print(param_pointer);   
    }
  }
}
void editParam() {
  switch (item_pointer)
  {
  case 6:
    encTick();
    break;
  case 7:
    break;
  }
}
void printMenu() {
  if (page_id != prev_page_id) {
    lcd.clear();
  }
  prev_page_id = page_id;
  getItemPointer();
  printPointer();
  printMenuPage();
  if (param_edit) {
    editParam();
  }
  prev_item_ptr = item_pointer;
}
void encButton() {
  uint8_t button_state = digitalRead(OK_BUTTON);
  delay(100);
  if (button_state == LOW) {
    param_edit = !param_edit;
    Serial.println(param_edit);
    Serial.println("Button was pressed\r\n");
  }
}

void setup() {
  Serial.begin(9600);
  attachInterrupt(0, int0, CHANGE);
  attachInterrupt(1, encButton, LOW);
  pinMode(DHT_PIN, INPUT);
  pinMode(OK_BUTTON, INPUT_PULLUP);
  dht.begin();
  lcd.init();
  lcd.clear();
  lcd.backlight();
  lcd.home();
  lcd.print("Welcome to Sol");
  lcd.setCursor(0,1);
  lcd.print("drip controller!");
  delay(1500);
  lcd.clear(); 
}

void loop() {
  printMenu();
  readDht();
  encButton();
  Serial.println(encCounter);
  Serial.println(flag);
  Serial.println(resetFlag);
  Serial.println(prevState);
  Serial.println(curState);
  Serial.println("");
}

Смысл такой:

1. Выводим на дисплей некую страницу меню, в каждой странице может быть 2 строки. Строка состоит из названия параметра (метрики) и значения, которое берётся из переменной (время пока захардкодил, ещё не разбирался с либой time и часами реального времени).
2. Слева от пункта меню, с которым хотим взаимодействовать, выводим указатель ">"
3. Опрашиваем по прерыванию энкодер. Если повернулся и изменился encCounter, то переставляем указатель в зависимости от значения item_pointer, которое берётся из encCounter, но обрезается так, чтобы получить infinity scroll в меню (находясь в п.1 меню, крутим влево и падаем в п.8 и наоборот).
4. По нажатию на кнопку (опрос по прерыванию) мы должны падать в режим редактирования параметра (если параметр подлежит редактированию, например дата, время, целевая влажность, длительность dur_goal -- показания с датчиком не подлежат редактированию).
4.1 Режим редактирования параметра -- указатель печатается не слева, а справа, скролл блокируется (см. getItemPointer()).
4.2 ииии тут мы подошли к проблеме: вращая энкодер, я хочу изменять (пусть даже инкрементом и декрементом 1) значение параметра. И никак, с какой бы стороны не подошёл, не могу понять, как это сделать. Даже меняя функционал принципиально.

Мои догадки:

1. Написать класс для хранения страницы меню, в который добавить методы редактирования параметров и печати.
2. Переписать функцию encTick(), чтоб она учитывала режим работы...? // хз, на самом деле как эта функция работает я не очень понимаю, так как там используется зачем-то побайтовый сдвиг и операции сравнения с байтовыми значениями, которые я не понимаю пока. Если кто-то хочет и готов объяснить -- буду очень благодарен. Вот это я писал сам по урокам и тут мне всё понятно:
C++:
void tick() {
  S1CurrSate = digitalRead(S1);
  delay(50);
  if (S1PrevState != S1CurrSate && S1CurrSate == 1) {
    if (digitalRead(S2) != S1CurrSate) {
      Serial.print("LEFT");
      encCounter++;
    }
    else {
      Serial.print("RIGHT");
      encCounter--;
    }
  }
  S1PrevState = S1CurrSate;
}
3. Переписать логику распечатки меню и указателя. Блокировать скролл другим способом?

1234.jpg
2345.jpg
 
Изменено:

bort707

★★★★★★✩
21 Сен 2020
3,061
912
Зачем тик переписывать? Эта функция работает с энкодером, получает с него смещение энкодера +1 или -1. В режиме скролл вы по этим смещениям переходите по пунктам меню, а в режиме редактирования параметра прибавляйте результат функции тик прямо к значению - вот вам и инкремент- декремент.
 
  • Лойс +1
Реакции: a_vel

a_vel

✩✩✩✩✩✩✩
29 Сен 2021
8
1
bort707, спасибо за ответ -- правда я не вдуплил :D

именно этого я и не понимаю: как реализовать изменение значения переменной, когда у меня указатель справа и стоит bool param_edit = TRUE; без вмешательства в работу функции, которую я не понимаю. Сорри, мб мой вопрос слишком туп.

Кстати, если посмотрите -- я значение encCounter обнуляю и приравниваю к item_pointer, потому что если encCounter ускакивает за пределы моего меню по значению (меньше 0 и больше 7), то для того, чтобы скроллить, приходится какое-то время крутить ручку энкодера, пока он не вылезет в 0...7. Если не обнулять, encCounter может принять значение, например, -100, а по условию если encCounter < 0, то item_pointer = 0, соответственно, я вышел 1-го элемента меню не смогу переместиться, пока encCounter не станет = 0.
 

bort707

★★★★★★✩
21 Сен 2020
3,061
912
Конечно, счетчик меню надо ограничивать только полезными значениями, это называется нормировкой.
Проще всего вставить в код что-то типа
C++:
if ( encCounter < 0) encCounter=0;
if ( encCounter > 7) encCounter = 7;
и счетчик не будет убегать.
 

a_vel

✩✩✩✩✩✩✩
29 Сен 2021
8
1
В итоге сделал такое с функцией tick():
C++:
void encTick() {
  curState = digitalRead(ENC_A) | digitalRead(ENC_B) << 1;
  if (!param_edit) {
    if (resetFlag && curState == 0b11) {
    if (prevState == 0b10) encCounter++;
    if (prevState == 0b01) encCounter--;
    resetFlag = 0;
    flag = true;
    }
    if (curState == 0b00) resetFlag = 1;
  }
  else {
    switch (item_pointer)
    {
    case 0:
      param_edit = !param_edit;
    case 1:
      param_edit = !param_edit;
    case 2:
      param_edit = !param_edit;
    case 3:
      param_edit = !param_edit;
    case 4:
      param_edit = !param_edit;
    case 5:
      param_edit = !param_edit;
    case 6:
      if (resetFlag && curState == 0b11) {
      if (prevState == 0b10) hum_goal++;
      if (prevState == 0b01) hum_goal--;
      resetFlag = 0;
      flag = true;
      }
      if (curState == 0b00) resetFlag = 1;
    case 7:
      if (resetFlag && curState == 0b11) {
      if (prevState == 0b10) dur_goal++;
      if (prevState == 0b01) dur_goal--;
      resetFlag = 0;
      flag = true;
      }
      if (curState == 0b00) resetFlag = 1;
    }
  }
  prevState = curState;
}

И оно работает как я ожидал: позволяет менять и сохранять значение переменных. Единственный нюанс -- отработка поворотов энкодера люто тормозит, срабатывание происходит 1 раз на 2-3 поворота, несмотря на то, что энкодер на прерывании. Попробую переключить прерывание кнопки на второй пин энкодера.

Ну да, с двумя прерываниями энкодер опрашивается корректо и без тормозов. Кнопка тоже. Спасибо за помощь!
 

bort707

★★★★★★✩
21 Сен 2020
3,061
912
Зачем было это все запихивать в функцию тик, вызываемую в прерывании? Это неправильно.
Вам надо в тик только получить данные, повернулся энкодер или нет. Всю остальную обработку делайте в луп.
 

Lumenjer

★★★✩✩✩✩
10 Дек 2020
220
112
Зачем было это все запихивать в функцию тик, вызываемую в прерывании? Это неправильно.
Поддерживаю

@a_vel, У вас есть 8 пунктов, естественно у каждого пункта есть свой "айди", что вам мешает сделать флаг, для "фокусировки" настройки?
Например
Если фокус не активен - перемещаемся по пунктам, если фокус активен - крутим параметр выбранного пункта, все

Пример:
    if (!focus)   // Фокус не активен, крутим позицию
        changePos();
    else {        // Фокус активен, крутим параметры
        switch (position) {
            case 0:            // Первый пункт
                doSomething();
                break;
            case 1:            // Второй
                doSomething2();
                break;
        }
    }
 
  • Лойс +1
Реакции: kostyamat

poty

★★★★★★✩
19 Фев 2020
3,229
940
@a_vel, судя по упоминанию слова "класс" - Вы занимаетесь ООП? Тогда уж будьте последовательны: создавайте иерархию классов. Например, для "выборного" меню (когда не нужно вводить значения вручную цифрами/текстом) практически всегда полезен класс, обеспечивающий хранение данных заданного диапазона (с закольцовыванием или без) и перегруженные методы инкремента/декремента.
Далее стоит создать класс "параметр", в котором будет хранится "диапазонный" класс хранения значения параметра и признак - focus из предыдущего сообщения - активации изменения параметра.
Потом создаём класс "меню", состоящий из массива классов "параметр" и "диапазонного" класса, хранящего номер активного меню.
В общем-то - всё. Разработка закончена.
 
  • Лойс +1
Реакции: a_vel

a_vel

✩✩✩✩✩✩✩
29 Сен 2021
8
1
@poty, но если поделитесь тут реализацией -- буду только рад ;)

я пока в рамках парадигмы ООП только на Python всякие веб штуки для личного и рабочего (автоматизация рутины, снятие метрик) юзал.

Вообще, проблему удалось решить проще (а мне на данном этапе нужно проще, но быстрее) - дописал условных операторов в encTick(), что позволяет мне сообщать функции, в зависимости от булевого флага, что именно менять: указатель или параметр. Работает стабильно.

Добавил сохранение параметров в EEPROM, прикрутил RTC и вывод на экран значений объекта времени. Загляденье (хыхы, посмотрим что я через год скажу об этом "коде") :D