• Witaj na Forum Arduino Polska! Zapraszamy do rejestracji!
  • Znajdziesz tutaj wiele informacji na temat hardware / software.
Witaj! Logowanie Rejestracja


Ocena wątku:
  • 0 głosów - średnia: 0
  • 1
  • 2
  • 3
  • 4
  • 5
SandBox 5 - MenuBackend - strach ma wielkie oczy?
#1
Witam
Tym razem temat całkiem poważny. Wielu z nas, łącznie ze mną ma lub miało problemy z MenuBackend.
Być może samo zrozumienie podstaw nie jest problemem (odsyłam tutaj do artykułu:
http://majsterkowo.pl/forum/menubackend-...t1549.html
gdzie starałem się wszystko opisać). Jednak samo stosowanie tego "wynalazku" jest już wyższą szkołą jazdy niestety:-)
Myślę, że jednak warto poświęcić trochę czasu na zrozumienie o co w tym wszystkim biega.


Czym jest owo MenuBackend? To biblioteka (zamieszczam w załączniku), która umożliwia w łatwy sposób tworzyć nawet bardzo rozbudowane MENU dla naszej aplikacji.
Często bowiem borykamy się z dylematem: Mamy gotowy program, który coś tam robi, coś tam mierzy, coś tam pokazuje... ale przydałoby się aby dodać do tego jakieś sensowne MENU, tak aby móc w każdej chwili na przykład zmienić temperaturę załączenia grzałki, czas załączenia timera, załączyć alarm i nie wiem co tam jeszcze.
Słowem potrzebna jest jakaś interakcja. Mamy na przykład nowiusieńką klawiaturkę, joystick, manipulator czy po prostu kilka microswitch-ów i chcemy wykorzystać ich "potencjał" :-)
No i właśnie teraz zaczynają się schody. Kurcze jeśli już coś działa, to albo nie działa tak, jak sobie wyobrażaliśmy albo sam program nie robi tego z naszym Menu, co robił poprawnie bez niego, prawda?
MenuBackend od czasu pierwszego wydania tej biblioteki przeżyło kilka poprawek i w chwili obecnej jest już całkiem fajne. Niestety nie mam wiedzy jak zachowuje się z różnymi wersjami IDE Arduino, miejmy jednak nadzieję, że nie ma niespodzianek (czego nie można powiedzieć o LiquidCrystal_I2C).

Dobra, do rzeczy. Nie wiem czy dzisiaj ukończę ten post. W każdym razie będę tu dodawał kolejne fragmenty, jak by co :-)
Jest konkretny projekt do ogarnięcia - sterowanie domowym zbiorem akwarium,( nie wiem czy dopełniacz liczby mnogiej to "akwariów"), w którym nasz kolega ma obsługę:
- kilku akwariów (ups. gramatyka)
- sterowanie temperaturą
- sterowanie oświetleniem
- sterowanie 3 wentylatorami (pompkami chyba... w sumie nie wiem o co biega, nazwijmy to wentylatory)
- sterowanie wyłącznikiem czasowym o nieznanym bliżej przeznaczeniu.
Założenia projektu - za pomocą 4 przycisków i Menu należy mieć możliwość:
1) ustawienia w dowolnym momencie:
   - temperatury załączenia grzałek
   - temperatury wyłączenia grzałek
   - czasu załączenia oświetlenia
   - czasu wyłączenia oświetlenia
   - załączenia/wyłączenia każdego z 3 wentylatorów
   - ustawiania czasu załączenia i wyłączenia Timera
2) To wszystko musi być niezależne od pracy głównego programu (odczyt temperatur, odmierzanie czasu RTC, sterowanie alarmami przekroczeń dopuszczalnych temperatur, sterowanie wentylatorów... i nie wiem co jeszcze.


Jak widać problem nie jest wcale trywialny. Jak zatem metodycznie podejść do tego zagadnienia?
Moim skromnym zdaniem trzeba je podzielić na 2 funkcjonalne bloki - program główny i obsługę menu.
Jeśli w dużym uproszczeniu uda nam się to zrobić w funkcji loop() to mamy pełen sukces :-)

Zacznijmy od menu:
Kod:
// ---------------------------------------------------------------------------------------------------
/*   Schemat menu:
     root
    Temperatura    Oswietlenie    Wentylatory        Timer            
      T_zal             Czas_zal    Wentylator 1       Timer_zal.
          T_wyl          Czas_wyl       Wentylator 2       Timer_wyl.
                                       Wentylator 3
*/
// ---- definicja wszystkich opcji menu -------------------------------------------------------------
MenuBackend menu = MenuBackend(co_wybrano, co_zmieniono); // tworzymy obiekt klasy MenuBackend o nazwie menu
// co_wybrano i co zmieniono to funkcje menu, ktore służą do jego obsługi
// poniżej definicja wszystkich opcji menu:
MenuItem miTemperatura = MenuItem("Temperatura");   // obiekt klasy MenuItem o nazwie miTemperatura
 MenuItem miT_zal = MenuItem("T zal.");            // itd. dla każdej opcji tworzymy instancję (egzemplarz)
 MenuItem miT_wyl = MenuItem("T wyl.");            // klasy MenuItem
MenuItem miOswietlenie = MenuItem("Oswietlenie");
 MenuItem miCzas_zal = MenuItem("Czas_zal");
 MenuItem miCzas_wyl = MenuItem("Czas_wyl");
MenuItem miWentylatory = MenuItem("Wentylatory");
 MenuItem miW1 = MenuItem("Wentylator 1");
 MenuItem miW2 = MenuItem("Wentylator 2");
 MenuItem miW3 = MenuItem("Wentylator 3");
MenuItem miTimer = MenuItem("Timer");
 MenuItem miTimer_ON = MenuItem("Timer_ON");
 MenuItem miTimer_OFF = MenuItem("Timer_OFF");     // koniec, to już wszystkie przykładowe opcje
Każdą linię opatrzyłem stosownym komentarzem, więc powinno być "z górki" :-)
linia:
Kod:
MenuBackend menu = MenuBackend(co_wybrano, co_zmieniono);
... odpowiada za utworzenie obiektu klasy MenuBackend o nazwie menu.
Argumentami tego konstruktora (bo tak właśnie nazywa się funkcja która tworzy obiekt każdej klasy), są 2 funkcje, które "potrafią" przekazać informację do samego MENU jaką opcję wybrano i odpowiedzieć co było aktywne przed i po tym wyborze. Czyli w prostych, żołnierskich słowach wiemy gdzie w galęzi tego menu aktualnie jesteśmy - mamy tzw. mapkę nawigacji po menu.
  
Biblioteka MenuBackend zasadniczo składa się z 2 klas: MenuBackend i MenuItem. Pierwsza to mama dla drugiej.
Jednak związek mama-córka nie jest tutaj najtrafniejszy, ściśle rzecz ujmując raczej obie te klasy są zaprzyjaźnione, bo de facto tak jest właśnie :-) O dobrodziejstwach tej przyjaźni dowiesz się najwięcej i najwięcej zrozumiesz - jeśli sięgniesz po biblię pana Grębosza - Symfonię C++. Sorry ale musiałem zareklamować tę pozycję, bo jest tego warta :-)
Pierwsza klasa opisuje zasadnicze menu, sposób jego organizacji i dostęp do składników i funkcji obsługi. Druga opisuje poszczególne opcje tego menu ( stąd MenuItem właśnie).
W bibliotece MenuBackend jest opisany sposób dodawania tych opcji oraz sposób tworzenia samego menu. Odpowiada za to funkcja menuSetup:
Kod:
void menuSetup()   // konfiguracja menu (położeń wszystkich opcji)
{
 menu.getRoot().add(miOswietlenie);                     // ustalam rodzica dla opcji Oświetlenie
 menu.getRoot().add(miWentylatory);                     // ustalam rodzica dla opcji Wentylatory
 menu.getRoot().add(miTimer);                           // ustalam rodzica dla opcji Timer
 menu.getRoot().add(miTemperatura);                     // ustalam rodzica dla opcji Temperatura
// bo chcę, aby po wciśnięciu klawisza UP wyjść z MENU (czyli do roota - korzenia)
 miTemperatura.add(miT_zal).add(miT_wyl);               // Temperatura ma 2 opcje, więc je dodaję (funkcja add)
 miTemperatura.addRight(miOswietlenie);                 // na prawo od Temperatura jest Oświetlenie (funkcja addRight)
 miOswietlenie.add(miCzas_zal).add(miCzas_wyl);         // Oświetlenie ma 2 opcje, więc je dodaję (funkcja add)
 miOswietlenie.addRight(miWentylatory);                 // na prawo od Oświetlenie są Wentylatory (funkcja addRight)
 miWentylatory.add(miW1).add(miW2).add(miW3);           // dodaję 3 kolejne opcje dla Wentylatorów (po kolei)
 miWentylatory.addRight(miTimer);                       // na prawo od Wentylatory jest Timer
 miTimer.add(miTimer_ON).add(miTimer_OFF);              // Timer ma 2 opcje, więc je dodaję
 miTimer.addRight(miTemperatura);                       // zamykam pętlę aby przejść do Temperatura :-)
}

 ... i tu także jest sporo objaśnień.
Nasze menu składa się z 4 opcji głównych (parent) i szeregu opcji (children).
Jeśli opcje są równorzędne to dodajemy je w poziome za pomocą addRight, addLeft. 
Jeśli dodajemy "dziecko" to za pomocą zwykłego add ( jakby w pionie).
W bibliotece MenuBackend jest jeszcze kilka innych opcji, ale o tym potem.
W każdym razie załączony przykład ilustruje jak się takie menu tworzy.
Prawdę mówiąc jest jeszcze sporo do omówienia, więc najlepiej chyba będzie jak zamieszczę cały gotowy przykładowy kod, a jutro opiszę dokładnie o co tam biega.
Do testów wykorzystuję oryginalną klawiaturkę z modułu LCD Keypad Shield (5 przycisków na A0). Mam nadzieję, że nie będzie problemu z ogarnięciem o co chodzi.
Poniżej kod:

Kod:
#include <MenuBackend.h>   // niezbędna biblioteka MenuBackend
// --- zmienne programu -----------------------------------------------------------------------------
volatile int start=1;  // zmienna odpowiedzialna za przełączanie: program -> start=1,  menu -> start=0
float Tz=20;           // przykładowa temperatura zalączenia grzałki
float Tw=30;           // przykładowa temperatura wyłączenia grzałki   
int Gz=7;              // przykładowa godzina załączenia oświetlenia
int Gw=10;             // przykładowa godzina wyłączenia oświetlenia
int Mz=20;             // przykładowa minuta załączenia oświetlenia
int Mw=40;             // przykładowa minuta wyłączenia oświetlenia
int GMz=Gz*60+Mz;      // czas załączenia oświetlenia  GMz=Gz*60+Mz
int GMw=Gw*60+Mw;      // czas wyłączenia oświetlenia  GMw=Gw*60+Mw
int TGz=18;            // przykładowa godzina zalączenia timera
int TGw=22;            // przykładowa godzina wyłączenia timera
int TMz=20;            // przykładowa minuta zalączenia timera
int TMw=40;            // przykładowa minuta wyłączenia timera
int TGMz=TGz*60+TMz;   // czas załączenia timera TGMz=TGz*60+TMz
int TGMw=TGw*60+TMw;   // czas wyłączenia timera TGMw=TGw*60+TMw
int stW1=0;            // stan wentylatora 1
int stW2=0;            // stan wentylatora 2
int stW3=0;            // stan wentylatora 3
// ---------------------------------------------------------------------------------------------------
/*   Schemat menu:
      root
   Temperatura    Oswietlenie    Wentylatory        Timer         
     T_zal            Czas_zal   Wentylator 1       Timer_zal.
          T_wyl          Czas_wyl       Wentylator 2       Timer_wyl.
                                        Wentylator 3
*/
// ---- definicja wszystkich opcji menu -------------------------------------------------------------
MenuBackend menu = MenuBackend(co_wybrano, co_zmieniono); // tworzymy obiekt klasy MenuBackend o nazwie menu
// co_wybrano i co zmieniono to funkcje menu, ktore służą do jego obsługi
// poniżej definicja wszystkich opcji menu:
MenuItem miTemperatura = MenuItem("Temperatura");   // obiekt klasy MenuItem o nazwie miTemperatura
  MenuItem miT_zal = MenuItem("T zal.");            // itd. dla każdej opcji tworzymy instancję (egzemplarz)
  MenuItem miT_wyl = MenuItem("T wyl.");            // klasy MenuItem
MenuItem miOswietlenie = MenuItem("Oswietlenie");
  MenuItem miCzas_zal = MenuItem("Czas_zal");
  MenuItem miCzas_wyl = MenuItem("Czas_wyl");
MenuItem miWentylatory = MenuItem("Wentylatory");
  MenuItem miW1 = MenuItem("Wentylator 1");
  MenuItem miW2 = MenuItem("Wentylator 2");
  MenuItem miW3 = MenuItem("Wentylator 3");
MenuItem miTimer = MenuItem("Timer");
  MenuItem miTimer_ON = MenuItem("Timer_ON");
  MenuItem miTimer_OFF = MenuItem("Timer_OFF");     // koniec, to już wszystkie przykładowe opcje
// ---- funkcje programu -----------------------------------------------------------------------------
void menuSetup()   // konfiguracja menu (położeń wszystkich opcji)
{
  menu.getRoot().add(miOswietlenie);                     // ustalam rodzica dla opcji Oświetlenie 
  menu.getRoot().add(miWentylatory);                     // ustalam rodzica dla opcji Wentylatory 
  menu.getRoot().add(miTimer);                           // ustalam rodzica dla opcji Timer 
  menu.getRoot().add(miTemperatura);                     // ustalam rodzica dla opcji Temperatura
 // bo chcę, aby po wciśnięciu klawisza UP wyjść z MENU (czyli do roota - korzenia) 
  miTemperatura.add(miT_zal).add(miT_wyl);               // Temperatura ma 2 opcje, więc je dodaję (funkcja add)
  miTemperatura.addRight(miOswietlenie);                 // na prawo od Temperatura jest Oświetlenie (funkcja addRight)
  miOswietlenie.add(miCzas_zal).add(miCzas_wyl);         // Oświetlenie ma 2 opcje, więc je dodaję (funkcja add)
  miOswietlenie.addRight(miWentylatory);                 // na prawo od Oświetlenie są Wentylatory (funkcja addRight)
  miWentylatory.add(miW1).add(miW2).add(miW3);           // dodaję 3 kolejne opcje dla Wentylatorów (po kolei)
  miWentylatory.addRight(miTimer);                       // na prawo od Wentylatory jest Timer
  miTimer.add(miTimer_ON).add(miTimer_OFF);              // Timer ma 2 opcje, więc je dodaję
  miTimer.addRight(miTemperatura);                       // zamykam pętlę aby przejść do Temperatura :-)
}
// ---- funkcja pobiera nazwę wybranej opcji ----------------------------------------------------------
void co_wybrano(MenuUseEvent used) 
{
  if(start==0)                            // jeśli obsługujemy menu (start=0)
  {
    Serial.println("-----------------------------------------------------------------------------------");
    Serial.print("Wybrano opcje:  < ");      // komunikat o tym co wybrano
    Serial.print(used.item.getName());    // funkcja getName() zwraca nazwę wybranej opcji
    Serial.println(" >  To tutaj wstawisz akcje do obslugi tej opcji.");
    Serial.println("Na przyklad: if(used.item.getName()==\"W1_ON\") runAkcja_W1();");
    Serial.println("... gdzie runAkcja_W1() to funkcja obslugi tego zdarzenia :-)");
    Serial.println("-----------------------------------------------------------------------------------");
    //  poniżej przykładowa obsługa wszystkich opcji Menu :
    if(used.item.getName()=="T zal.")Tz=ustawTemp("T zal.",Tz,10,40.0,0.2); // obsługa temp. załącznia
    if(used.item.getName()=="T wyl.")Tw=ustawTemp("T wyl.",Tw,10,40.0,0.2); // obsługa temp. wyłączenia
    if(used.item.getName()=="Czas_zal")GMz=ustawCzas("Oswietl","Czas zal.",GMz,10); // obsługa czasu zal.
    if(used.item.getName()=="Czas_wyl")GMw=ustawCzas("Oswietl","Czas wyl.",GMw,10); // obsługa czasu zal.
    if(used.item.getName()=="Wentylator 1")stW1=ustawWent(1); // obsługa wentylatora 1
    if(used.item.getName()=="Wentylator 2")stW2=ustawWent(2); // obsługa wentylatora 2
    if(used.item.getName()=="Wentylator 3")stW3=ustawWent(3); // obsługa wentylatora 3
    if(used.item.getName()=="Timer_ON")TGMz=ustawCzas("Timer","Czas zal.",TGMz,10); // obsługa Timer ON
    if(used.item.getName()=="Timer_ON")TGMw=ustawCzas("Timer","Czas wyl.",TGMw,10); // obsługa Timer OFF
  }
}
// ---- funkcja identyfikuje aktualną wybraną opcję oraz poprzedni stan --------------------------------
void co_zmieniono(MenuChangeEvent changed)
{
  if(changed.to.getName()=="MenuRoot") start=1; // jeśli root to wracamy do obsługi programu głównego
  else                                     // poniżej komunikaty kontrolne o tym co było i co wybrano
    {
    start=0;
    Serial.print("Zmiana z: ");  // 
    Serial.print(changed.from.getName());  // skąd (z jakiej opcji)
    Serial.print(" na: ");
    Serial.println(changed.to.getName());  // dokąd (do jakiej opcji)
    }
}
// ---- odczyt klawiatury (tutaj 4 klawisze: Dalej, Plus ,Minus i OK) -----------------------------------
int czytaj() // wersja z klawiaturą modułu LCD Keypad  Schield
{
   int pinAnalog=0;
   int stan_Analog = analogRead(pinAnalog);delay(30);//Serial.println(stan_Analog); 
   if (stan_Analog > 1000) return -1; // dla wartosci poza zakresem
   if (stan_Analog < 50)   return 0;  // w prawo  
   if (stan_Analog < 150)  return 1;  // do gĂłry 
   if (stan_Analog < 300)  return 2;  // w dół 
   if (stan_Analog < 500)  return 3;  // w lewo  
   if (stan_Analog < 700)  return 4;  // OK 
   return -1;                         // nic nie wcisnieto
   /* 
   Uwaga!!! Te wartości dla stan_Analog mogą się różnić dla różnych wykonań (klonów tego modułu) 
   Należy je dobrać doświadczalnie przez odczyt wartości na A0 (funkcja analogRead(A0)).
   */
}
// --- przykładowa funkcja program główny ----------------------------------------------------------------
void program_glowny() // tutaj Twój program wykonuje swoją pracę niezależnie od MENU
{
  /*
  To tutaj umieszczasz:
  - odczyt danych z czujników
  - reakcje na przekroczenie temperatury
  - wszystkie ważne funkcje wykonywane przez program
  */
  Serial.println("=== WYKONUJE PROGRAM GLOWNY ====");
  obsluga_menu(); //aby w każdej chwili móc przerwać program i przejść do ustawień menu
}
// --- funkcja do obsługi menu ---------------------------------------------------------------------------
void obsluga_menu()
{
 switch(czytaj())
  {
  case 0:menu.moveRight();
    break;
  case 1:menu.moveUp();
    break;
  case 2:menu.moveDown();
    break;
  case 3:menu.moveLeft();
    break;
  case 4:menu.use();
    break;
  }
  delay(100);           // kosmetyka (dobrać doświadczalnie czas dla autopowtarzania)
}
// -------------------------------------------------------------------------------------------------------
void setup()
{
  menuSetup();          // przygotowanie menu do pracy
  Serial.begin(9600);   // konfiguracja dla Seriala
}
// -------------------------------------------------------------------------------------------------------
void loop()  // ma tylko 3 linijki kodu :-)
{
 if(start==1) program_glowny(); // jeśli start=1 wykonuje się nasz program
 else obsluga_menu();           // jeśli naciśniemy klawisz (tutaj Minus lub DOWN - uruchamiamy menu)
 /*
 Jeśli chciałbyś aby inny klawisz był odpowiedzialny za przechodzenie pomiędzy Program a Menu,
 to musisz uzyć innej funkcji klasy MenuBackend w miejscu tworzenia menu, czyli zamiast:
 menu.getRoot().add(miTemperatura); 
 ... użyj na przykład: menu.getRoot().addRight(miTemperatura); lub menu.getRoot().addLeft(miTemperatura);
 */
 delay(20);   // kosmetyka
}
// === koniec ============================================================================================
// --- funkcja nastawy Temperatury -----------------------------------------------------------------------
float ustawTemp(char s[10], float t, float vmin, float vmax, float skok)
{
  Serial.print("Ustawienie: ");Serial.print(s);Serial.print(" = ");Serial.println(t);
  int x=czytaj();
  while(x!=4)
    {
    if(x==1){t+=skok;if(t>vmax)t=vmax;}
    if(x==2){t-=skok;if(t<vmin)t=vmin;}
    x=czytaj();
    Serial.print("Ustawienie: ");Serial.print(s);Serial.print(" = ");Serial.println(t);
    delay(120);
    }
 Serial.println("---------------------------------------------------------");
 Serial.print("< USTAWIONO ");Serial.print(s);Serial.print(" = ");Serial.print(t);Serial.println(" >");delay(100);
 Serial.println("---------------------------------------------------------");
 return t; 
}
// --- funkcja nastawy Czasu ------------------------------------------------------------------------------
int ustawCzas(char op[14],char s[10],int tt, int skok)
{
int g=tt/60;
int m=tt%60;
Serial.print("Ustawienie: ");Serial.print(s);Serial.print(" = ");Serial.print(g);Serial.print(":");Serial.println(m);
int x=czytaj();
  while(x!=4)
    {
    if(x==1){tt+=skok;if(tt>23*60+50)tt=0;}
    if(x==2){tt-=skok;if(tt<0)tt=23*60+50;}
    x=czytaj();
    g=tt/60;
    m=tt%60;
    Serial.print("Ustawienie: ");Serial.print(s);Serial.print(" = ");Serial.print(g);Serial.print(":");Serial.println(m);
    delay(100);
    }
    Serial.println("---------------------------------------------------------");
    Serial.print("< USTAWIONO ");Serial.print(s);Serial.print(" = ");Serial.print(g);Serial.print(":");
    Serial.print(m);Serial.println(" >");
    Serial.println("---------------------------------------------------------");
    delay(1500);
    return tt;
}
// ---- funkcja nastawy Wentylatora -------------------------------------------------------------------------
int ustawWent(int nr)
{
  int s;
  if(nr==1)s=stW1;if(nr==2)s=stW2;if(nr==3)s=stW3;
  Serial.print("Ustawienie: Wentylator ");Serial.print(nr);Serial.print(": stan = ");Serial.println(s);
  int x=czytaj();
  while(x!=4)
    {
     if(x==1){s=!s;}
     if(x==2){s=!s;}
    x=czytaj();
    Serial.print("Ustawienie: Wentylator ");Serial.print(nr);Serial.print(": stan = ");Serial.println(s);
    delay(120);
    }
    Serial.println("---------------------------------------------------------");
    Serial.print("< USTAWIONO ");Serial.print("Wentylator ");Serial.print(nr);Serial.print(": stan = ");Serial.print(s);
    Serial.println(" >");
    Serial.println("---------------------------------------------------------");
    delay(100);
    return s;
}  
//-----------------------------------------------------------------------------------------------------------
... a biblioteka w załączniku 
Na końcu, za loop-em umieściłem funkcje do obsługi ustawień temperatury i czasu.
Z założenia każda z nich czyta stan klawiatury i dopóki  nie wciśnięto OK (tutaj wartość =4), dopóty reagujemy na zmiany klawiszy UP-DOWN (prawo-lewo, czy Plus-Minus... jak kto woli).
Do pełni szczęścia brakuje jeszcze zapisu wartości do EEPROM-a no i całego programu głównego, ale to już zadanie dla autora projektu :-)
Trzeba jeszcze zamienić Seriale na obsługę wyświetlacza LCD - a tu może być pomocny mój poprzedni post:
Sandbox 4 :-)
Pozdrawiam


Załączone pliki
.zip   MenuBackend_1-1.zip (Rozmiar: 13 KB / Pobrań: 86)
 
Odpowiedź
  


Wiadomości w tym wątku
SandBox 5 - MenuBackend - strach ma wielkie oczy? - przez wojtekizk - 06-02-2016, 22:10

Skocz do:


Przeglądający: 2 gości