Projektvorstellung: Library für das Erstellen von Menus für LCD Displays

Guten Tag Arduino-Gemeinde,

ich arbeite seit einigen Wochen an einer Library für das einfache und dynamische Erstellen von Menüs, optermiert für ein LCD Keypadshield (LCD Display mit Button für das navigieren durch ein Menü). Die Library die ich vorstellen werde, ermöglicht es schnell und ohne viele Handgriffe ein komplexes Menü zu erstellen. Im folgenden würde ich nun einfach damit beginnen ein Menü mit einigen Unterpunkten auf zubauen.

Schritt 1.: Planung

Als erstes überlegen wir uns was wir für unsere Menü brauchen. Ich habe als Beispiel folgendes Menü aufgebaut:

Settings -> Sound
Display
Control
LED -> Test LED

Jeder dieser Namen stellt ein MenüItem da. Jedes MenüItem bekommt nun eine nummer zu gewiesen. Dadurch wird es möglich die Items zu verwalten und unter einander bekannt zu machen, sprich wenn man später einen Button drückt ist klar welches Item als nächstes auf dem Display erscheinen soll. Wichtig dabei ist das jedes Item eine einmalige Nummer bekommt.

001 Settings -> 002 Sound
102 Display
202 Control
302 LED -> 033 Test LED

Der Punkt 033: Test Led wird später wenn der Selectbutton gedrückt wurde eine LED zum leuchten bringen.
Nach dem wir nun das Menü geplant haben und jedem Menüpunkt eine Nummer zugewiesen haben können wir Anfangen unser Menü zu programmieren.

Schritt 2.: Anlegen des Menüs und der Items

Als nächtes werden die benötigten Objekte angelegt. Wir beginngen mit dem Menu

// Create a Menu object (The frame you feed with MenuItems)
Menu myMenu;

das Menu objekt verwaltet alle MenuItems und weiß wo hin es geht, wenn ein Button (z.B. Rechts oder Links) gedrückt wurde.

Danach folgen die MenuItems welche die einzelnen Menüpunkte darstellen.

// Create the MenuItems
//                Name     Number For  Back  up  down
MenuItem item001("Settings", 001, 002, 001, 001, 001);
MenuItem item002("Sound", 002, 002, 001, 302, 102);
MenuItem item102("Display", 102, 102, 001, 002, 202);
MenuItem item202("Controll", 202, 202, 001, 102, 302);
MenuItem item302("LEDs", 302, 033, 001, 202, 002);
MenuItem item033("Test LED", 033, 033, 302, 033, 033);

Wie schon in dem Kommentarbeschrieben wird jetzt jedem Item bekannt gemacht, welcher sein Nachbar ist und von hin es geht wenn der jeweilige Button gedrückt wird. Da wir vorher eine ordentliche Planung durchgeführt haben ist es nun einfach die Items anzulegen.

Schritt 3.: Verbinden der Items und dem Display mit dem Menü

Da diese Libary auf ein Menü für LCD Displays ausgelegt ist brauchen wir natürlich auch ein solches Display. Dieses legen wir an und machen es dem Menü bekannt, damit es Ausgaben auf dem Display tätigen kann.

LiquidCrystal lcd(8, 9, 4, 5, 6, 7); 
lcd.begin(16, 2);
  
// Add the display to the menu
myMenu.addLCD(&lcd);

Danach können wir die zuvor erzeugen MenuItems dem Menü bekannt machen.

// Add the items to the menu
myMenu.addItem(item001); 
myMenu.addItem(item002); 
myMenu.addItem(item102);
myMenu.addItem(item202);
myMenu.addItem(item302);
myMenu.addItem(item033);

Ab diesem Punkt ist das Menü erstellt und funktionsfähig. Danach muss natürlich noch festgelegt werden, was passiert wenn die jeweiligen Button gedrückt werden. Dies sollte man aber aus dem folgendem kompletten Beispiel entnehmen.
Zudem werde ich bei Intresse die Libary freigeben. Ich hoffe das meine Erläuterung verständlich genung war und bei einigen Intresse geweckt hat. Außerdem bitte ich um viele Verbesserungsvorschläge bezüglich dieser Vorstellung und natürlich der Libary. Kritik nehme ich eben so gerne an, aber ich bitte um schonende Worte, da dies das erste mal ist das ich solch ein Projekt vorstelle.

In diesem Sinne bin ich auf die Kommentare gespannt und nun das komplette Beispiel

#include <LiquidCrystal.h>
#include <Menu.h>
#include <MenuItem.h>

// Create a Display object
LiquidCrystal lcd(8, 9, 4, 5, 6, 7); 

// Create a Menu object (The frame you feed with MenuItems)
Menu myMenu; 

// Create the MenuItems
//                Name     Number For  Back  up  down
MenuItem item001("Settings", 001, 002, 001, 001, 001);
MenuItem item002("Sound", 002, 002, 001, 302, 102);
MenuItem item102("Display", 102, 102, 001, 002, 202);
MenuItem item202("Controll", 202, 202, 001, 102, 302);
MenuItem item302("LEDs", 302, 033, 001, 202, 002);
MenuItem item033("Test LED", 033, 033, 302, 033, 033);

int readButton; // Variable to store the last pressed Button
int select;  

void ledTest(){
  digitalWrite(13, HIGH);
  delay(3000);
  digitalWrite(13, LOW);
}

void setup(){
  lcd.begin(16, 2);
  
  // Add the display to the menu
  myMenu.addLCD(&lcd); 
  
  // Add the items to the menu
  myMenu.addItem(item001); 
  myMenu.addItem(item002); 
  myMenu.addItem(item102);
  myMenu.addItem(item202);
  myMenu.addItem(item302);
  myMenu.addItem(item033);
  
  myMenu.begin(001); // Start at the Menu with at the MenuItem you want
  
  pinMode(13, OUTPUT); // Set pinMode for Test LED
}

void loop(){
  readButton = analogRead(0); // read the value from a pushed button
  
  // Check if a button was press
  if(readButton < 50){
    myMenu.goForward();
  }
  if(readButton > 50 && readButton < 195){
    myMenu.goUp();
  }
  if(readButton > 195 && readButton < 380){
    myMenu.goDown();
  }
  if(readButton > 380 && readButton < 555){
    myMenu.goBack();
  }
  
  // Check if selectbutton was press
  if(readButton > 555 && readButton < 790){
    select = myMenu.select(); //use select() to check if the item you are at has a runable function
    
    switch(select){
      case 033: //if found a funtion for a item run it
        ledTest();
        break;
        
      default: 
        break;
    }
  }
  readButton = 1000;

}

LcdMenu_v1_0.zip (3.18 KB)

cr0n0s1:
...Zudem werde ich bei Intresse die Libary freigeben. Ich hoffe das meine Erläuterung verständlich genung war und bei einigen Intresse geweckt hat. Außerdem bitte ich um viele Verbesserungsvorschläge bezüglich dieser Vorstellung und natürlich der Libary. Kritik nehme ich eben so gerne an, aber ich bitte um schonende Worte, da dies das erste mal ist das ich solch ein Projekt vorstelle...

Ich hätte Interesse an der Lib und würde sie mir auf jeden Fall mal durchschauen.

Hi,

bei deinem Beispielcode taucht bei mir ein "geistiges Fragezeichen" auf:
Was passiert bei readButton = 50 (oder 195 oder 380) ?

Grüßle Bernd

Ich wünsche mir ein Menü - System, das im Flash liegt und somit schon fertig kompiliert geladen wird.
Wieviel RAM brauchen deine MenuItem-Elemente? Verbrät ein addItem () - Aufruf noch etwas zusätzlich? (Wohl nicht, aber geht nur mit Variablen im RAM)

Ein int im RAM, das den aktuellen Zustand des gesamten MenüSystems kennzeichnet, sollte eigentlich reichen.
Der statische Teil des aktuellen MenuItem sollte natürlich nur jeweils einmal ausgegeben werden, das passt auch noch in die Status-Variable.

Wie passen Wert-Einstell Funktionen (z.B. über up/down Taster) in dein Menü-System?

Das Ganze aus einfach konfigurierbaren Bausteinen zusammenzustellen, ist ein guter Ansatz!

Guter Ansatz, gut strukturiert. Detailliert kann man aber erst was dazu sagen, wenn wir einen Blick auf die Lib selbst geworfen haben.
Was ich noch für Praktisch halten würde wäre ein dynamischer Aufbau. Also wenn man statt mit den Integer-IDs gleich mit den Zeigern auf die Child- und Parent-Elemente arbeitet.
Toll wäre auch, wenn man die select()-Funktion eines Menuitems gleich beim Anlagen als Funktionspointer "void (*p)(void )" übergeben könnte. Der Parameter "void" kann dann wieder ein Zeiger auf ein beliebiges Objekt sein.

Mario.

Hallo und danke für das Intresse,
ich habe nun den oberen Beitrag bearbeitet und die Lib als .zip angehängt also alle die Intresse haben einfach downloaden. Zudem hab ich vor, ein paar fragen zu beantworten :wink:

BerndJM:
Hi,

bei deinem Beispielcode taucht bei mir ein "geistiges Fragezeichen" auf:
Was passiert bei readButton = 50 (oder 195 oder 380) ?

Grüßle Bernd

Dieser Abschnitt bezieht sich komplett auf das Benutzen eines KeyPadShields (Arduino_LCD_KeyPad_Shield__SKU__DFR0009_-DFRobot) Letztendlich ist dieser Code nur dafür da, um zu ermitteln welcher Button gedrückt wurde und somit durch das Menü zu navigieren.

michael_x:
Wieviel RAM brauchen deine MenuItem-Elemente? Verbrät ein addItem () - Aufruf noch etwas zusätzlich?

Wie viel RAM es genau verbrät kann ich die leider nicht sagen.
Ja addItem() benötigt auch noch Speicher. Allerdings hab ich vor, wie auch schon andere sagten, per Zeiger auf die Items zu zeigen.
Weiter Infos solltest du dir aber aus der Lib holen können.

michael_x:
Das Ganze aus einfach konfigurierbaren Bausteinen zusammenzustellen, ist ein guter Ansatz!

Danke, das war meine Motivation eine solche Lib zubasteln. Es freut mich das ich das anscheinend geschafft habe.

Am Ende möchte ich nochmals sagen das ich mich sehr über Verbesserungsvorschläge freue. Ich hoffe das mein Code nicht ganz so grausam ist und keine hoch heiligen unaufgeschriebenen Programmierregeln bricht.
Also dann viel Spass mit der Lib :wink:

Der Anhang ist der am ersten Post ?

Ich hoffe das mein Code nicht ganz so grausam ist und keine hoch heiligen unaufgeschriebenen Programmierregeln bricht.

Wenn du ohne String arbeitest, ist es "nicht ganz so grausam" :wink:

Wie gesagt, ein Menu im PROGMEM Flash-Speicher aber "konfiguriert statt programmiert", wäre gut.

Hmm doch die String Klasse. Die würde ich auf jeden Fall entsorgen. Da Du mit den Strings selbst nichts machst, außer sie zu speichern, sollte einfach "String" gegen "char*", oder sogar "const char*" ausgetauscht werden.

Als nächstes würde ich auf jeden Fall von den "künstlichen" Indizes auf Zeiger umstellen, dann sparst Du Dir die Funktion getNeighbour(), da jeder Eintrag von forward, back, up und down gleich der passende Zeiger auf das nächste Item ist. Damit man einen definierten Start hat, würde ich ein "root" Element im Menu-Objekt definieren, welches immer den ersten Eintrag liefert. Zusätzlich sollte jedes MenuItem entsprechende Methoden getForward(), getBack(), getUp und getDown() haben, damit man auf die angehängten Elemente zugreifen und damit durch den Baum / Graph navigieren kann.

Mario.

michael_x:
Der Anhang ist der am ersten Post ?

Wie gesagt, ein Menu im PROGMEM Flash-Speicher aber "konfiguriert statt programmiert", wäre gut.

1.) Ja aus dem ersten Post.
2.) Wie soll ich dieses "konfiguriert statt programmiert" verstehen?

mkl0815:
Hmm doch die String Klasse. Die würde ich auf jeden Fall entsorgen. Da Du mit den Strings selbst nichts machst, außer sie zu speichern, sollte einfach "String" gegen "char*", oder sogar "const char*" ausgetauscht werden.

Okay, dazu Fragen. Warum sind Strings schlecht? Sind das nicht letztendlich auch nur Char arrays? Was haben Chars als Vorteil gegenüber Strings?
Und wenn ich jetzt von String auf Char wechsel ist das doch für den User "relativ" aufwendig da er einen Namen z.B. so anlegen muss:

char name[5] = {'H', 'a', 'l', 'l', 'o'};

mkl0815:
Als nächstes würde ich auf jeden Fall von den "künstlichen" Indizes auf Zeiger umstellen, dann sparst Du Dir die Funktion getNeighbour(), da jeder Eintrag von forward, back, up und down gleich der passende Zeiger auf das nächste Item ist. Damit man einen definierten Start hat, würde ich ein "root" Element im Menu-Objekt definieren, welches immer den ersten Eintrag liefert. Zusätzlich sollte jedes MenuItem entsprechende Methoden getForward(), getBack(), getUp und getDown() haben, damit man auf die angehängten Elemente zugreifen und damit durch den Baum / Graph navigieren kann.

Das wäre der nächste Schritt gewesen, allein Schon damit ein Item nicht zwei mal existiert.

Unterm Strich: Würdet ihr diese Lib benutzen wenn ihr gerade ein Projekt amlaufen habt, bei dem ihr deratige Menüs braucht?

cr0n0s1:
Okay, dazu Fragen. Warum sind Strings schlecht? Sind das nicht letztendlich auch nur Char arrays? Was haben Chars als Vorteil gegenüber Strings?
Und wenn ich jetzt von String auf Char wechsel ist das doch für den User "relativ" aufwendig da er einen Namen z.B. so anlegen muss:

char name[5] = {'H', 'a', 'l', 'l', 'o'};

Strings sind "schlecht", die Klasse bei jeder String-Operation den knappen Speicher fragmentiert und am Ende jeder String mehr Platz weg nimmt, als der gleiche char*.

Einen C-String kann man auch folgendermaßen deklarieren:

char* name1 = "Hallo";
char name2[] = "Hallo";

Beide Variablen enthalten auch schon das abschliessende '\0' Zeichen. Was Dein "char name[5]" nicht macht. Dein "Hallo" hat als C-String kein Ende.
Und ein char* name = "Hallo" ist genau so aufwändig wie ein String-Objekt zu erzeugen.
Mario.

mkl0815:

char* name1 = "Hallo";

char name2[] = "Hallo";



Beide Variablen enthalten auch schon das abschliessende '\0' Zeichen. Was Dein "char name[5]" nicht macht. Dein "Hallo" hat als C-String kein Ende. 
Und ein char* name = "Hallo" ist genau so aufwändig wie ein String-Objekt zu erzeugen.
Mario.

Ahh super, das war mir vorher nie so bewusst, dann seh ich auch kein problem dadrin das ganze mit Chars zu machen :wink:

Ahh super, das war mir vorher nie so bewusst, dann seh ich auch kein problem dadrin das ganze mit Chars zu machen

Sorry, hätt' ich vielleicht vorher besser erklären sollen. Vermutlich kannst du dir gar nicht vorstellen, was passiert, wenn du schreibst:

String text = "Hallo";
text = text + "!";

text liefert zwar jetzt "Hallo!", aber der alte String("Hallo") liegt immer noch als Müll rum, weil er zu klein für den neuen text ist.

Ein char array ausreichender Größe ist im Endeffekt einfacher, wenn du erstmal dran gewöhnt bist, wie es zu benutzen ist.

Und wenn du dir dann angewöhnst, feste Texte gar nicht in den RAM zu legen :wink:

Wie soll ich dieses "konfiguriert statt programmiert" verstehen?

So wie du es machst, mit einer Liste von MenuElementen, die quasi in einer Tabelle definiert haben, wie sie zusammenhängen und welche Texte sie anzeigen, statt alles per Programm zu codieren.

Nur hab ich in meinem 328 nur noch 800 byte RAM frei, und ein SD Card Sektor ist halt mal 512 byte groß, da bin ich etwas knickig. Ausserdem geht's auch um's Prinzip, aus sportlichem Ehrgeiz...

Eure Erläuterungen zu Strings sind sehr interessant und gut zu Wissen.

Wie würde denn die richtige Implementierung der PROGMEM in die Lib aussehen?

Lib_X.h

const char **_flashText;

Lib_X.cpp

char ProgMemBuffer[20];
strcpy_P(ProgMemBuffer, (char*)pgm_read_word(_flashText));

*.ino

prog_char strServo1[]               PROGMEM = {"Servos AN"};

PROGMEM const char *MenServo_Strings[] = 
{   
  strServo1
};

KlasseX._flashText = &MenServo_Strings[0];

Funktionieren tuts, ist das auch die optimalste Art ohne Verbesserungsmöglichkeiten??

Könnte man evtl. die neue Funktion F("") irgendwie mit benutzen?
Damit wäre die Einbindung leichter.

Z.B.:

KlasseX.InitText(F("Servos AN"));

Könnte man evtl. die neue Funktion F("") irgendwie mit benutzen?
Damit wäre die Einbindung leichter.

Na klar, F("text") ist zwar keine Funktion sondern ein Makro in WString.h; der einzige Sinn ist, aus "text" den Pseudo-VariablenTyp __FlashStringHelper* zu machen, und als PSTR("text") im Flash abzulegen.

Musst du nur eine Methode InitText(const __FlashStringHelper* ptext) hinzufügen.
Zum Verwenden auf const prog_char* casten, oder einfach mit print() ausgeben.

So habe jetzt meine

Init(const __FlashStringHelper* Text)
{
	_Text3 = Text;
}

jetzt möchte ich den übergebenen Wert in der Klasse abspeichern, komme aber nicht weiter.
Ist das richtig?
const __FlashStringHelper* _Text3;

Ich möchte den Wert später an anderer Stelle wieder verarbeiten:
sprintf(Text, "%s", _Text3);

Brauche nochmals Hilfe, komme nicht weiter...

jetzt möchte ich den übergebenen Wert in der Klasse abspeichern, komme aber nicht weiter.
Ist das richtig?
const __FlashStringHelper* _Text3;

Ja, nur der Name _Text3 ist nicht sehr schön :wink:

Ich möchte den Wert später an anderer Stelle wieder verarbeiten:
sprintf(Text, "%s", _Text3);

sprintf kennt keinen __FlashStringHelper ( und auch kein PROGMEM )
print(_Text3); geht direkt, sonst schau nach den Funktionen, mit denen char aus dem PROGMEM geladen oder kopiert werden

Ja, Text3 auch nur deshalb, weil meine Lib momentan mehrere "Textformate" unterstützt. Wird dann später bereinigt :wink:

Die Fuunktion für PROGMEM ist klar: strcpy_P(Text, (char*)pgm_read_word(_Text2));

Dann werde ich mal suchen, was den __FlashStringHelper in ein char Array klopfen kann.

den __FlashStringHelper

... gibt es so gar nicht "wirklich".
Von einem Pointer auf einen __Flashstringhelper weiss man, dass print() es drucken kann, und dass man diesen Pointer als einen prog_char* ( oder einen PGM_P ) verwenden kann.

also z.B. strcpy_P(rambuffer, (prog_char*)Text3 );

--> arduino-1.0.1\hardware\tools\avr\avr\include\avr\pgmspace.h