Spirik / GEM

Good Enough Menu for Arduino
GNU Lesser General Public License v3.0
245 stars 36 forks source link
arduino arduino-library glcd lcd lcd-menu menu

GEM

GEM (a.k.a. Good Enough Menu) - Arduino library for creation of graphic multi-level menu with editable menu items, such as variables (supports int, byte, float, double, bool, char[17] data types) and option selects. User-defined callback function can be specified to invoke when menu item is saved.

Supports buttons that can invoke user-defined actions and create action-specific context, which can have its own enter (setup) and exit callbacks as well as loop function.

Party hard!

Supports AltSerialGraphicLCD (since GEM ver. 1.0), U8g2 (since GEM ver. 1.1) and Adafruit GFX (since GEM ver. 1.3) graphics libraries.

Note that U8g2 and Adafruit GFX libraries are required by default, regardless of which one of them is actually used to drive display (although the ones that are not used shouldn't affect compiled sketch size much). However, it is possible (since GEM ver. 1.2.2) to exclude support for not used ones. Support for AltSerialGraphicLCD is disabled by default since GEM ver. 1.5 (it is still possible to enable it). See Configuration section for details.

For use with AltSerialGraphicLCD library (by Jon Green) LCD screen must be equipped with SparkFun Graphic LCD Serial Backpack and properly set up to operate using firmware provided with aforementioned library.

Cyrillic is partially supported in U8g2 version of GEM (since 1.1). Can be used in menu title, menu item labels (including variables, buttons, and menu page links), and select options. Editable strings with Cyrillic characters are not supported.

Optional support for editable variables of float and double data types was added since version 1.2 of GEM. It is enabled by default, but can be disabled by editing config.h file that ships with the library or by defining GEM_DISABLE_FLOAT_EDIT flag before build. Disabling this feature may save considerable amount of program storage space. See Floating-point variables section for details.

User-defined arguments can be passed to callback function as a part of GEMCallbackData struct (specified for menu items that represent editable variables and buttons) since version 1.4 of GEM.

When to use

If you want to equip your project with graphic LCD display and let user choose different options and settings to configure its operation. Whether it is control panel of smart home or simple configurable LED strip, GEM will provide all necessary controls for editing variables and navigating through submenus, as well as running user-defined functions.

Structure

Menu created with GEM library comprises of three base elements:

GEM structure

Installation

Library format is compatible with Arduino IDE 1.5.x+. There are number of ways to install the library:

Whichever option you choose you may need to reload IDE afterwards.

U8g2 and Adafruit GFX libraries are required to be installed by default as well. Support for AltSerialGraphicLCD is disabled by default since GEM ver. 1.5 (so it is not required to be installed). However, it is possible to exclude support for not used libraries (since GEM ver. 1.2.2), and/or enable support for AltSerialGraphicLCD (since GEM ver 1.5). See Configuration section for details.

How to use with AltSerialGraphicLCD

Click here to view ### Requirements GEM supports [AltSerialGraphicLCD](http://www.jasspa.com/serialGLCD.html) library if enabled (see [Configuration](#configuration) section). LCD screen must be equipped with [SparkFun Graphic LCD Serial Backpack](https://www.sparkfun.com/products/9352) and properly set up to operate using firmware provided with AltSerialGraphicLCD. Installation and configuration of it is covered in great detail in AltSerialGraphicLCD manual. In theory GEM is compatible with any display, that is supported by SparkFun Graphic LCD Serial Backpack. Guaranteed to work with [128x64](https://www.sparkfun.com/products/710) pixel displays. [160x128](https://www.sparkfun.com/products/8799) pixel ones should work fine as well, although it wasn't tested. ### Import To include AltSerialGraphicLCD-compatible version of GEM library add the following line at the top of your sketch: ```cpp #include ``` AltSerialGraphicLCD library will be included automatically through GEM library, so no need to include it explicitly in your sketch (although it still needs to be installed in your system, of course). In order to communicate with your SparkFun Graphic LCD Serial Backpack, AltSerialGraphicLCD library uses [SoftwareSerial](https://www.arduino.cc/en/Reference/SoftwareSerial) (that ships with Arduino IDE). There is no need to explicitly include it in your sketch as well, because it is already included through AltSerialGraphicLCD library. So the following line is completely optional (although note that second inclusion won't affect the size of you sketch) - you may omit it and use SoftwareSerial as though it was included. ```cpp #include ``` > Note that it is possible to use hardware serial instead (e.g. if you're planning to use it with Arduino Leonardo's `Serial1` class), however some modifications of AltSerialGraphicLCD library would be required in that case. One more additional library that may come in handy (although is not necessary) is [KeyDetector](https://github.com/Spirik/KeyDetector) - it is small and lightweight library for key press events detection. It is used in some of the supplied examples (as well as the following one) to detect button presses for navigation through menu. To include KeyDetector library, install it first and then add the following line: ```cpp #include ``` ### Use Assume you have a simple setup as follows: - 128x64 LCD screen equipped with SparkFun Graphic LCD Serial Backpack, which is properly connected to the power source and to digital pins 8 and 9 of your Arduino for serial communication via SoftwareSerial library; - also you have 6 push-buttons (momentary switches) connected to the digital pins 2 to 7, wired with 10kOhm pulldown resistors (so the HIGH means that the button is pressed). ![Basic example breadboard](https://github.com/Spirik/GEM/wiki/images/ex_GEM_01_basic_bb_edited_1776_o.png) Let's create a simple one page menu with one editable menu item associated with `int` variable, one with `bool` variable, and a button, pressing of which will result in `int` variable value being printed to Serial monitor if `bool` variable is set to `true`. To navigate through menu we will use 6 push-buttons connected to the Arduino (for four directional controls, one Cancel, and one Ok). For the sake of simplicity we will use KeyDetector library to detect single button presses (as we need a way to prevent continuously pressed button from triggering press event multiple times in a row). > For more detailed examples and tutorials please visit GEM [wiki](https://github.com/Spirik/GEM/wiki). #### Navigation buttons initial setup (via KeyDetector library) Create constants for the pins you want to detect signals on (these are the pins the push-buttons are connected to): ```cpp const byte downPin = 2; const byte leftPin = 3; const byte rightPin = 4; const byte upPin = 5; const byte cancelPin = 6; const byte okPin = 7; ``` Create an array of `Key` objects. It will hold information about which button press event should be detected on which input pin: ```cpp Key keys[] = {{GEM_KEY_UP, upPin}, {GEM_KEY_RIGHT, rightPin}, {GEM_KEY_DOWN, downPin}, {GEM_KEY_LEFT, leftPin}, {GEM_KEY_CANCEL, cancelPin}, {GEM_KEY_OK, okPin}}; ``` > **Note:** aliases `GEM_KEY_UP`, `GEM_KEY_RIGHT`, `GEM_KEY_DOWN`, `GEM_KEY_LEFT`, `GEM_KEY_CANCEL`, and `GEM_KEY_OK` are predefined and come with the GEM library. They represent identifiers of buttons that menu listens and responds to. E.g. sending to menu `GEM_KEY_DOWN` will trigger it to move cursor down and highlight the next menu item, etc. Create `KeyDetector` object called `myKeyDetector` and supply its constructor with `keys` array created at the previous step and explicitly pass the size of the array: ```cpp KeyDetector myKeyDetector(keys, sizeof(keys)/sizeof(Key)); ``` Navigation buttons initial setup is now complete. #### LCD initial setup (via SoftwareSerial and AltSerialGraphicLCD libraries) Create constants for the pins SparkFun Graphic LCD Serial Backpack is connected to: ```cpp const byte rxPin = 8; const byte txPin = 9; ``` Initialize an instance of the SoftwareSerial library called `serialLCD`: ```cpp SoftwareSerial serialLCD(rxPin, txPin); ``` Create an instance of the `GLCD` class named `glcd`. This instance is used to call all of the subsequent GLCD functions (internally from GEM library, or manually in your sketch if it is required). Instance is created with a reference to the software serial object: ```cpp GLCD glcd(serialLCD); ``` LCD initial setup is now complete. #### Menu initial setup Create variables that you would like to be editable through the menu. Assign them initial values: ```cpp int number = -512; bool enablePrint = false; ``` Create two menu item objects of class `GEMItem`, linked to `number` and `enablePrint` variables. Let's name them simply "Number" and "Enable print" respectively - these names will be printed on screen: ```cpp GEMItem menuItemInt("Number:", number); GEMItem menuItemBool("Enable print:", enablePrint); ``` Create menu button that will trigger `printData()` function. It will print value of our `number` variable to Serial monitor if `enablePrint` is `true`. We will write (define) this function later. However we should forward-declare it in order to pass its reference to `GEMItem` constructor. Let's name our button "Print": ```cpp void printData(); // Forward declaration GEMItem menuItemButton("Print", printData); ``` Create menu page object of class `GEMPage`. Menu page holds menu items (`GEMItem`) and, in fact, represents menu level. Menu can have multiple menu pages (linked to each other) with multiple menu items each. Let's call our only menu page "Main Menu": ```cpp GEMPage menuPageMain("Main Menu"); ``` And finally, create menu object of class `GEM`. Supply its constructor with a reference to `glcd` object we created earlier: ```cpp GEM menu(glcd); ``` > **Note:** `GEM` constructor supports additional optional parameters that can customize look of the menu. See [Reference](#reference) and [wiki](https://github.com/Spirik/GEM/wiki) for details. We will link menu items to menu pages to menu in `setup()` function. For now, menu initial setup is complete. #### setup() function In `setup()` function of the sketch define modes of the pins push-buttons are connected to: ```cpp pinMode(downPin, INPUT); pinMode(leftPin, INPUT); pinMode(rightPin, INPUT); pinMode(upPin, INPUT); pinMode(cancelPin, INPUT); pinMode(okPin, INPUT); ``` Init serial communications (both native `Serial` and software `serialLCD`): ```cpp Serial.begin(115200); serialLCD.begin(115200); ``` Reset LCD to its initial state. Using delays here is optional, but I've found that it actually may help certain LCDs to regain their initial state more effectively after being previously unexpectedly shut down. Adjust the delays to what best works for your display. ```cpp delay(500); glcd.reset(); delay(1000); ``` If you still encounter spontaneous freezing on load (especially right after sketch uploads), you may want to add the second instance of reset instructions. This (almost) bulletproof solution will ensure that screen boots and properly resets no matter what. ```cpp // Uncomment the following lines in dire situations // (e.g. when screen becomes unresponsive after shutdown) glcd.reset(); delay(1000); ``` Init menu. That will run some initialization routines (e.g. load sprites into LCD Serial Backpack's internal memory), then show splash screen (which can be customized). ```cpp menu.init(); ``` The next step is to gather all of the previously declared menu items and pages together and assign them to our menu. It is convenient to do that in a separate function. Let's call it `setupMenu()`. We will define it later. ```cpp setupMenu(); ``` And finally, draw menu to the screen: ```cpp menu.drawMenu(); ``` `setup()` function of the sketch is now complete: ```cpp void setup() { // Push-buttons pin modes pinMode(downPin, INPUT); pinMode(leftPin, INPUT); pinMode(rightPin, INPUT); pinMode(upPin, INPUT); pinMode(cancelPin, INPUT); pinMode(okPin, INPUT); // Serial communications setup Serial.begin(115200); SerialLCD.begin(115200); // LCD reset delay(500); glcd.reset(); delay(1000); // Uncomment the following lines in dire situations // (e.g. when screen becomes unresponsive after shutdown) glcd.reset(); delay(1000); // Menu init, setup and draw menu.init(); setupMenu(); menu.drawMenu(); } ``` #### setupMenu() function Let's assemble our menu in `setupMenu()` function. First, add menu items to menu page: ```cpp menuPageMain.addMenuItem(menuItemInt); menuPageMain.addMenuItem(menuItemBool); menuPageMain.addMenuItem(menuItemButton); ``` Because we don't have multiple menu levels, all we left to do now is to add our only menu page to menu and set it as initial menu page (loaded when menu first drawn): ```cpp menu.setMenuPageCurrent(menuPageMain); ``` `setupMenu()` function is now complete: ```cpp void setupMenu() { // Add menu items to menu page menuPageMain.addMenuItem(menuItemInt); menuPageMain.addMenuItem(menuItemBool); menuPageMain.addMenuItem(menuItemButton); // Add menu page to menu and set it as current menu.setMenuPageCurrent(menuPageMain); } ``` #### loop() function In the `loop()` function of the sketch we'll be listening to push-buttons presses (using `KeyDetector`) and delegate pressed button to menu: ```cpp void loop() { // If menu is ready to accept button press... if (menu.readyForKey()) { // ...detect key press using KeyDetector library myKeyDetector.detect(); // Pass pressed button to menu // (pressed button ID is stored in trigger property of KeyDetector object) menu.registerKeyPress(myKeyDetector.trigger); } } ``` #### Button action Let's define `printData()` function that we declared earlier. It will be invoked each time the "Print" button in our menu is pressed. It should print value of our `number` variable to Serial monitor if `enablePrint` is `true`. ```cpp void printData() { // If enablePrint flag is set to true (checkbox on screen is checked)... if (enablePrint) { // ...print the number to Serial Serial.print("Number is: "); Serial.println(number); } else { Serial.println("Printing is disabled, sorry:("); } } ``` > This is the simplest action that menu item button can have. More elaborate versions make use of custom "[context](#gemcontext)" that can be created when button is pressed. In that case, button action can have its own setup and loop functions (named `context.enter()` and `context.loop()`) that run similarly to how sketch operates. It allows you to initialize variables and e.g. prepare screen (if needed for the task that function performs), and then run through loop function, waiting for user input, or sensor reading, or command to terminate and exit back to the menu eventually. In the latter case additional `context.exit()` function will be called, that can be used to clean up your context and e.g. to free some memory and draw menu back to screen. #### Sketch Full version of this basic example is shipped with the library and can be found at "examples/AltSerialGraphicLCD/Example-01_Basic/[Example-01_Basic.ino](https://github.com/Spirik/GEM/blob/master/examples/AltSerialGraphicLCD/Example-01_Basic/Example-01_Basic.ino)". #### Run After compiling and uploading sketch to Arduino, wait while LCD screen boots and menu is being initialized and drawn to the screen. Then start pressing the push-buttons and navigate through the menu. Pressing "Ok" button (attached to pin 7) will trigger edit mode of the "Number" variable, or change state of "Enable print" option, or invoke action associated with "Print" menu button (depending on which menu item is currently selected). If "Enable print" option is checked, then pressing "Print" button will result in `number` variable printed to the Serial Monitor. ![Basic example](https://github.com/Spirik/GEM/wiki/images/gem-ex-01-basic-run.gif) To learn more about GEM library, see the [Reference](#reference) section and visit [wiki](https://github.com/Spirik/GEM/wiki).

How to use with U8g2

Click here to view ### Requirements GEM supports [U8g2](https://github.com/olikraus/U8g2_Arduino) library. In theory GEM is compatible with any display, that is supported by U8g2 library (given that it is properly set up and configured using correct [constructor](https://github.com/olikraus/u8g2/wiki/u8g2setupcpp)). Guaranteed to work with [128x64](https://www.sparkfun.com/products/710) pixel displays, based on KS0108 controller. [160x128](https://www.sparkfun.com/products/8799) pixel ones should also work, as well as any other display that is supported by U8g2, although it is yet to be tested. ### Import To include U8g2-compatible version of GEM library add the following line at the top of your sketch: ```cpp #include ``` U8g2 library will be included automatically through GEM library, so no need to include it explicitly in your sketch (although it still needs to be installed in your system, of course). ### Use Assume you have a simple setup as follows: - 128x64 LCD screen based on (or compatible with) KS0108 controller, connected as shown below, with 10kOhm potentiometer to adjust screen contrast; - also you have 6 push-buttons (momentary switches) connected to the digital pins 2 to 7, wired with 10kOhm pullup resistors (so the LOW means that the button is pressed). ![Basic example breadboard](https://github.com/Spirik/GEM/wiki/images/ex_GEM_01_basic_u8g2_breadboard_bb_edited_1974_o.png) Let's create a simple one page menu with one editable menu item associated with `int` variable, one with `bool` variable, and a button, pressing of which will result in `int` variable value being printed to Serial monitor if `bool` variable is set to `true`. To navigate through menu we will use 6 push-buttons connected to the Arduino (for four directional controls, one Cancel, and one Ok). We will use U8g2 library to detect single button presses. > For more detailed examples and tutorials please visit GEM [wiki](https://github.com/Spirik/GEM/wiki). #### LCD initial setup (via U8g2 library) U8g2 library supports numerous popular display controllers. Choose a matching constructor for the correct initialization of the display. See available constructors and supported controllers in the [documentation](https://github.com/olikraus/u8g2/wiki/u8g2setupcpp) for U8g2 library. In our case create an instance of the `U8G2_KS0108_128X64_1` class named `u8g2`. This instance is used to call all of the subsequent U8g2 functions (internally from GEM library, or manually in your sketch if it is required). ```cpp U8G2_KS0108_128X64_1 u8g2(U8G2_R0, 8, 9, 10, 11, 12, 13, 18, 19, /*enable=*/ A0, /*dc=*/ A1, /*cs0=*/ A3, /*cs1=*/ A2, /*cs2=*/ U8X8_PIN_NONE, /* reset=*/ U8X8_PIN_NONE); ``` > **Note:** GEM library is compatible with all [buffer size](https://github.com/olikraus/u8g2/wiki/u8g2setupcpp#buffer-size) options (namely `_1`, `_2`, `_F`) and screen [rotation](https://github.com/olikraus/u8g2/wiki/u8g2setupcpp#rotation) options supported by U8g2. LCD initial setup is now complete. #### Menu initial setup Create variables that you would like to be editable through the menu. Assign them initial values: ```cpp int number = -512; bool enablePrint = false; ``` Create two menu item objects of class `GEMItem`, linked to `number` and `enablePrint` variables. Let's name them simply "Number" and "Enable print" respectively - these names will be printed on screen: ```cpp GEMItem menuItemInt("Number:", number); GEMItem menuItemBool("Enable print:", enablePrint); ``` Create menu button that will trigger `printData()` function. It will print value of our `number` variable to Serial monitor if `enablePrint` is `true`. We will write (define) this function later. However we should forward-declare it in order to pass its reference to `GEMItem` constructor. Let's name our button "Print": ```cpp void printData(); // Forward declaration GEMItem menuItemButton("Print", printData); ``` Create menu page object of class `GEMPage`. Menu page holds menu items (`GEMItem`) and, in fact, represents menu level. Menu can have multiple menu pages (linked to each other) with multiple menu items each. Let's call our only menu page "Main Menu": ```cpp GEMPage menuPageMain("Main Menu"); ``` And finally, create menu object of class `GEM_u8g2`. Supply its constructor with a reference to `u8g2` object we created earlier: ```cpp GEM_u8g2 menu(u8g2); ``` > **Note:** `GEM_u8g2` constructor supports additional optional parameters that can customize look of the menu. See [Reference](#reference) and [wiki](https://github.com/Spirik/GEM/wiki) for details. We will link menu items to menu pages to menu in `setup()` function. For now, menu initial setup is complete. #### setup() function In `setup()` function of the sketch init serial communication: ```cpp Serial.begin(115200); ``` Init `u8g2` instance of U8g2 library by calling `begin()` method and supplying it with pin numbers push-buttons are attached to: ```cpp u8g2.begin(/*Select/OK=*/ 7, /*Right/Next=*/ 4, /*Left/Prev=*/ 3, /*Up=*/ 5, /*Down=*/ 2, /*Home/Cancel=*/ 6); ``` Init menu. That will run some initialization routines (e.g. clear display and apply some GEM specific settings), then show splash screen (which can be customized). ```cpp menu.init(); ``` The next step is to gather all of the previously declared menu items and pages together and assign them to our menu. It is convenient to do that in a separate function. Let's call it `setupMenu()`. We will define it later. ```cpp setupMenu(); ``` And finally, draw menu to the screen: ```cpp menu.drawMenu(); ``` `setup()` function of the sketch is now complete: ```cpp void setup() { // Serial communication setup Serial.begin(115200); // U8g2 library init. Pass pin numbers the buttons are connected to. // The push-buttons should be wired with pullup resistors (so the LOW means that the button is pressed) u8g2.begin(/*Select/OK=*/ 7, /*Right/Next=*/ 4, /*Left/Prev=*/ 3, /*Up=*/ 5, /*Down=*/ 2, /*Home/Cancel=*/ 6); // Menu init, setup and draw menu.init(); setupMenu(); menu.drawMenu(); } ``` #### setupMenu() function Let's assemble our menu in `setupMenu()` function. First, add menu items to menu page: ```cpp menuPageMain.addMenuItem(menuItemInt); menuPageMain.addMenuItem(menuItemBool); menuPageMain.addMenuItem(menuItemButton); ``` Because we don't have multiple menu levels, all we left to do now is to add our only menu page to menu and set it as initial menu page (loaded when menu first drawn): ```cpp menu.setMenuPageCurrent(menuPageMain); ``` `setupMenu()` function is now complete: ```cpp void setupMenu() { // Add menu items to menu page menuPageMain.addMenuItem(menuItemInt); menuPageMain.addMenuItem(menuItemBool); menuPageMain.addMenuItem(menuItemButton); // Add menu page to menu and set it as current menu.setMenuPageCurrent(menuPageMain); } ``` #### loop() function In the `loop()` function of the sketch we'll be listening to push-buttons presses (using `U8g2`) and delegate pressed button to menu: ```cpp void loop() { // If menu is ready to accept button press... if (menu.readyForKey()) { // ...detect key press using U8g2 library // and pass pressed button to menu menu.registerKeyPress(u8g2.getMenuEvent()); } } ``` #### Button action Let's define `printData()` function that we declared earlier. It will be invoked each time the "Print" button in our menu is pressed. It should print value of our `number` variable to Serial monitor if `enablePrint` is `true`. ```cpp void printData() { // If enablePrint flag is set to true (checkbox on screen is checked)... if (enablePrint) { // ...print the number to Serial Serial.print("Number is: "); Serial.println(number); } else { Serial.println("Printing is disabled, sorry:("); } } ``` > This is the simplest action that menu item button can have. More elaborate versions make use of custom "[context](#gemcontext)" that can be created when button is pressed. In that case, button action can have its own setup and loop functions (named `context.enter()` and `context.loop()`) that run similarly to how sketch operates. It allows you to initialize variables and e.g. prepare screen (if needed for the task that function performs), and then run through loop function, waiting for user input, or sensor reading, or command to terminate and exit back to the menu eventually. In the latter case additional `context.exit()` function will be called, that can be used to clean up your context and e.g. to free some memory and draw menu back to screen. #### Sketch Full version of this basic example is shipped with the library and can be found at "examples/U8g2/Example-01_Basic/[Example-01_Basic.ino](https://github.com/Spirik/GEM/blob/master/examples/U8g2/Example-01_Basic/Example-01_Basic.ino)". #### Run After compiling and uploading sketch to Arduino, wait while LCD screen boots and menu is being initialized and drawn to the screen. Then start pressing the push-buttons and navigate through the menu. Pressing "Ok" button (attached to pin 7) will trigger edit mode of the "Number" variable, or change state of "Enable print" option, or invoke action associated with "Print" menu button (depending on which menu item is currently selected). If "Enable print" option is checked, then pressing "Print" button will result in `number` variable printed to the Serial Monitor. ![Basic example](https://github.com/Spirik/GEM/wiki/images/gem-ex-01-basic-run.gif) To learn more about GEM library, see the [Reference](#reference) section and visit [wiki](https://github.com/Spirik/GEM/wiki).

How to use with Adafruit GFX

Click here to view ### Requirements GEM supports [Adafruit GFX](https://learn.adafruit.com/adafruit-gfx-graphics-library) library. In theory GEM is compatible with any display, that is supported by Adafruit GFX library (given that it is properly set up and configured as required by the library). Guaranteed to work with [Adafruit 1.8" 128x160](https://www.adafruit.com/products/358) TFT LCD display, based on ST7735 controller. Other ST77** based ones should also work, theoretically as well as any other display that is supported by Adafruit GFX, although it is yet to be tested. ### Import To include Adafruit GFX-compatible version of GEM library add the following line at the top of your sketch: ```cpp #include ``` Adafruit GFX library will be included automatically through GEM library, so no need to include it explicitly in your sketch (although it still needs to be installed in your system, of course). However, in order to communicate with your display it is required to install and explicitly include library specific to controller of your display, e.g. ST7735: ```cpp #include ``` One more additional library that may come in handy (although is not necessary) is [KeyDetector](https://github.com/Spirik/KeyDetector) - it is small and lightweight library for key press events detection. It is used in some of the supplied examples (as well as the following one) to detect button presses for navigation through menu. To include KeyDetector library, install it first and then add the following line: ```cpp #include ``` ### Use Assume you have a simple setup as follows: - 128x160 TFT LCD display based on (or compatible with) ST7735 controller, e.g. [Adafruit 1.8" TFT Display with microSD](http://www.adafruit.com/products/358), connected as shown below; - also you have 6 push-buttons (momentary switches) connected to the digital pins 2 to 7, wired with 10kOhm pulldown resistors (so the HIGH means that the button is pressed). ![Basic example breadboard](https://raw.githubusercontent.com/wiki/Spirik/GEM/images/ex_GEM_01_basic_agfx_breadboard_bb_edited_1590_o.png) Let's create a simple one page menu with one editable menu item associated with `int` variable, one with `bool` variable, and a button, pressing of which will result in `int` variable value being printed to Serial monitor if `bool` variable is set to `true`. To navigate through menu we will use 6 push-buttons connected to the Arduino (for four directional controls, one Cancel, and one Ok). For the sake of simplicity we will use KeyDetector library to detect single button presses (as we need a way to prevent continuously pressed button from triggering press event multiple times in a row). > For more detailed examples and tutorials please visit GEM [wiki](https://github.com/Spirik/GEM/wiki). #### Navigation buttons initial setup (via KeyDetector library) Create constants for the pins you want to detect signals on (these are the pins the push-buttons are connected to): ```cpp const byte downPin = 2; const byte leftPin = 3; const byte rightPin = 4; const byte upPin = 5; const byte cancelPin = 6; const byte okPin = 7; ``` Create an array of `Key` objects. It will hold information about which button press event should be detected on which input pin: ```cpp Key keys[] = {{GEM_KEY_UP, upPin}, {GEM_KEY_RIGHT, rightPin}, {GEM_KEY_DOWN, downPin}, {GEM_KEY_LEFT, leftPin}, {GEM_KEY_CANCEL, cancelPin}, {GEM_KEY_OK, okPin}}; ``` > **Note:** aliases `GEM_KEY_UP`, `GEM_KEY_RIGHT`, `GEM_KEY_DOWN`, `GEM_KEY_LEFT`, `GEM_KEY_CANCEL`, and `GEM_KEY_OK` are predefined and come with the GEM library. They represent identifiers of buttons that menu listens and responds to. E.g. sending to menu `GEM_KEY_DOWN` will trigger it to move cursor down and highlight the next menu item, etc. Create `KeyDetector` object called `myKeyDetector` and supply its constructor with `keys` array created at the previous step and explicitly pass the size of the array: ```cpp KeyDetector myKeyDetector(keys, sizeof(keys)/sizeof(Key)); ``` Navigation buttons initial setup is now complete. #### LCD initial setup (via Adafruit GFX library) Adafruit GFX library supports several different display controllers (through separately installed and included libraries). Choose a matching library for the correct initialization of the display. See available libraries and supported controllers in the [documentation](https://learn.adafruit.com/adafruit-gfx-graphics-library) for Adafruit GFX library. In our case we included Adafruit_ST7735 library, and now we need to create an instance of the `Adafruit_ST7735` class named `tft`. This instance is used to call all of the subsequent Adafruit GFX functions (internally from GEM library, or manually in your sketch if it is required). Before that, we define aliases for the pins display is connected to. ```cpp #define TFT_CS A2 #define TFT_RST -1 #define TFT_DC A3 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); ``` LCD initial setup is now complete. #### Menu initial setup Create variables that you would like to be editable through the menu. Assign them initial values: ```cpp int number = -512; bool enablePrint = false; ``` Create two menu item objects of class `GEMItem`, linked to `number` and `enablePrint` variables. Let's name them simply "Number" and "Enable print" respectively - these names will be printed on screen: ```cpp GEMItem menuItemInt("Number:", number); GEMItem menuItemBool("Enable print:", enablePrint); ``` Create menu button that will trigger `printData()` function. It will print value of our `number` variable to Serial monitor if `enablePrint` is `true`. We will write (define) this function later. However we should forward-declare it in order to pass its reference to `GEMItem` constructor. Let's name our button "Print": ```cpp void printData(); // Forward declaration GEMItem menuItemButton("Print", printData); ``` Create menu page object of class `GEMPage`. Menu page holds menu items (`GEMItem`) and, in fact, represents menu level. Menu can have multiple menu pages (linked to each other) with multiple menu items each. Let's call our only menu page "Main Menu": ```cpp GEMPage menuPageMain("Main Menu"); ``` And finally, create menu object of class `GEM_adafruit_gfx`. Supply its constructor with a reference to `tft` object we created earlier: ```cpp GEM_adafruit_gfx menu(tft, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO); ``` > **Note:** `GEM_POINTER_ROW` option defines the look of the menu item pointer, `GEM_ITEMS_COUNT_AUTO` turns on automatic calculation of number of items that will fit on the screen based on screen's height. `GEM_adafruit_gfx` constructor supports additional optional parameters that can customize look of the menu. See [Reference](#reference) and [wiki](https://github.com/Spirik/GEM/wiki) for details. We will link menu items to menu pages to menu in `setup()` function. For now, menu initial setup is complete. #### setup() function In `setup()` function of the sketch define modes of the pins push-buttons are connected to: ```cpp pinMode(downPin, INPUT); pinMode(leftPin, INPUT); pinMode(rightPin, INPUT); pinMode(upPin, INPUT); pinMode(cancelPin, INPUT); pinMode(okPin, INPUT); ``` Init serial communications: ```cpp Serial.begin(115200); ``` Init `tft` instance of Adafruit_ST7735/Adafruit GFX library by calling `initR()` method and supplying it with tab initialization parameter, suitable for your display (in case of Adafruit 1.8" TFT Display set it to `INITR_BLACKTAB`): ```cpp tft.initR(INITR_BLACKTAB); ``` You can optionally [rotate](https://learn.adafruit.com/adafruit-gfx-graphics-library/rotating-the-display) the display by calling `setRotation()` method, e.g.: ```cpp tft.setRotation(3); ``` Init menu. That will run some initialization routines, then show splash screen (which can be customized). ```cpp menu.init(); ``` The next step is to gather all of the previously declared menu items and pages together and assign them to our menu. It is convenient to do that in a separate function. Let's call it `setupMenu()`. We will define it later. ```cpp setupMenu(); ``` And finally, draw menu to the screen: ```cpp menu.drawMenu(); ``` `setup()` function of the sketch is now complete: ```cpp void setup() { // Push-buttons pin modes pinMode(downPin, INPUT); pinMode(leftPin, INPUT); pinMode(rightPin, INPUT); pinMode(upPin, INPUT); pinMode(cancelPin, INPUT); pinMode(okPin, INPUT); // Serial communications setup Serial.begin(115200); // Use this initializer if using a 1.8" TFT screen: tft.initR(INITR_BLACKTAB); // Init ST7735S chip, black tab // OR use this initializer if using a 1.8" TFT screen with offset such as WaveShare: // tft.initR(INITR_GREENTAB); // Init ST7735S chip, green tab // See more options in Adafruit GFX library documentation // Optionally, rotate display // tft.setRotation(3); // See Adafruit GFX library documentation for details // Menu init, setup and draw menu.init(); setupMenu(); menu.drawMenu(); } ``` #### setupMenu() function Let's assemble our menu in `setupMenu()` function. First, add menu items to menu page: ```cpp menuPageMain.addMenuItem(menuItemInt); menuPageMain.addMenuItem(menuItemBool); menuPageMain.addMenuItem(menuItemButton); ``` Because we don't have multiple menu levels, all we left to do now is to add our only menu page to menu and set it as initial menu page (loaded when menu first drawn): ```cpp menu.setMenuPageCurrent(menuPageMain); ``` `setupMenu()` function is now complete: ```cpp void setupMenu() { // Add menu items to menu page menuPageMain.addMenuItem(menuItemInt); menuPageMain.addMenuItem(menuItemBool); menuPageMain.addMenuItem(menuItemButton); // Add menu page to menu and set it as current menu.setMenuPageCurrent(menuPageMain); } ``` #### loop() function In the `loop()` function of the sketch we'll be listening to push-buttons presses (using `KeyDetector`) and delegate pressed button to menu: ```cpp void loop() { // If menu is ready to accept button press... if (menu.readyForKey()) { // ...detect key press using KeyDetector library myKeyDetector.detect(); // Pass pressed button to menu // (pressed button ID is stored in trigger property of KeyDetector object) menu.registerKeyPress(myKeyDetector.trigger); } } ``` #### Button action Let's define `printData()` function that we declared earlier. It will be invoked each time the "Print" button in our menu is pressed. It should print value of our `number` variable to Serial monitor if `enablePrint` is `true`. ```cpp void printData() { // If enablePrint flag is set to true (checkbox on screen is checked)... if (enablePrint) { // ...print the number to Serial Serial.print("Number is: "); Serial.println(number); } else { Serial.println("Printing is disabled, sorry:("); } } ``` > This is the simplest action that menu item button can have. More elaborate versions make use of custom "[context](#gemcontext)" that can be created when button is pressed. In that case, button action can have its own setup and loop functions (named `context.enter()` and `context.loop()`) that run similarly to how sketch operates. It allows you to initialize variables and e.g. prepare screen (if needed for the task that function performs), and then run through loop function, waiting for user input, or sensor reading, or command to terminate and exit back to the menu eventually. In the latter case additional `context.exit()` function will be called, that can be used to clean up your context and e.g. to free some memory and draw menu back to screen. #### Sketch Full version of this basic example is shipped with the library and can be found at "examples/AdafruitGFX/Example-01_Basic/[Example-01_Basic.ino](https://github.com/Spirik/GEM/blob/master/examples/AdafruitGFX/Example-01_Basic/Example-01_Basic.ino)". #### Run After compiling and uploading sketch to Arduino, wait while LCD screen boots and menu is being initialized and drawn to the screen. Then start pressing the push-buttons and navigate through the menu. Pressing "Ok" button (attached to pin 7) will trigger edit mode of the "Number" variable, or change state of "Enable print" option, or invoke action associated with "Print" menu button (depending on which menu item is currently selected). If "Enable print" option is checked, then pressing "Print" button will result in `number` variable printed to the Serial Monitor. ![Basic example](https://github.com/Spirik/GEM/wiki/images/gem-agfx-ex-01-basic-run.gif) To learn more about GEM library, see the [Reference](#reference) section and visit [wiki](https://github.com/Spirik/GEM/wiki).

Reference

GEM, GEM_u8g2, GEM_adafruit_gfx

Primary class of the library. Responsible for general appearance of the menu, communication with display (via supplied GLCD, U8G2 or Adafruit_GFX object), integration of all menu items GEMItem and pages GEMPage into one menu. Object of corresponding GEM class variation defines as follows.

AltSerialGraphicLCD version:

GEM menu(glcd[, menuPointerType[, menuItemsPerScreen[, menuItemHeight[, menuPageScreenTopOffset[, menuValuesLeftOffset]]]]]);
// or
GEM menu(glcd[, appearance]);

U8g2 version:

GEM_u8g2 menu(u8g2[, menuPointerType[, menuItemsPerScreen[, menuItemHeight[, menuPageScreenTopOffset[, menuValuesLeftOffset]]]]]);
// or
GEM_u8g2 menu(u8g2[, appearance]);

Adafruit GFX version:

GEM_adafruit_gfx menu(tft[, menuPointerType[, menuItemsPerScreen[, menuItemHeight[, menuPageScreenTopOffset[, menuValuesLeftOffset]]]]]);
// or
GEM_adafruit_gfx menu(tft[, appearance]);

GEM customization

Calls to GEM, GEM_u8g2 or GEM_adafruit_gfx constructors GEM(glcd), GEM_u8g2(u8g2), GEM_adafruit_gfx(tft) without specifying additional custom parameters are equivalent to the following calls:

GEM menu(glcd, /* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ 5, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86);
GEM_u8g2 menu(u8g2, /* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ 5, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86);
GEM_adafruit_gfx menu(tft, /* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ 5, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86);

Note: carefully choose values of menuItemsPerScreen, menuItemHeight, menuPageScreenTopOffset, menuValuesLeftOffset in accordance to the actual size of your display. Default values of these options are suitable for 128x64 screens. But that is not the only possible option: the other combination of values you set may also be suitable - just calculate them correctly and see what works best for you.

Note: long title of the menu page GEMPage won't overflow to the new line in U8g2 version and will be truncated at the edge of the screen.

Note: it is possible to customize appearance of each menu page individually (since GEM ver. 1.5) by using GEMAppearance object.

For more details on customization see corresponding section of the wiki and description of GEMAppearance struct.

Constants

Methods

Note: calls to methods that return a reference to the owning GEM, or GEM_u8g2, or GEM_adafruit_gfx object can be chained, e.g. menu.hideVersion().invertKeysDuringEdit().init(); (since GEM ver. 1.4.6).

Properties


GEMPage

Menu page holds menu items GEMItem and represents menu level. Menu can have multiple menu pages (linked to each other) with multiple menu items each. Object of class GEMPage defines as follows:

GEMPage menuPage(title[, exitAction]);

or

GEMPage menuPage(title[, parentMenuPage]);

Constants

Methods

Note: calls to methods that return a reference to the owning GEMPage object can be chained, e.g. menuPageSettings.addMenuItem(menuItemInterval).addMenuItem(menuItemTempo).setParentMenuPage(menuPageMain); (since GEM ver. 1.4.6).


GEMItem

Menu item of the menu. Can represent editable or read-only variable of type int, byte, float, double, bool, char[17] (or char[GEM_STR_LEN], to be exact); option select of type int, byte, float, double, char[n]; incremental spinner of type int, byte, float, double; link to another menu page; or button that can invoke user-defined actions and create action-specific context, which can have its own enter (setup) and exit callbacks as well as loop function. User-defined callback function can be specified to invoke when editable menu item is saved or option is selected. Exact definition of GEMItem object depends on its type.

Note: support for editable variables (and spinner) of types float and double is optional. It is enabled by default, but can be disabled by editing config.h file that ships with the library. Disabling this feature may save considerable amount of program storage space (up to 10% on Arduino UNO R3). See Floating-point variables for more details.

Variable

GEMItem menuItemVar(title, linkedVariable[, readonly]);

or

GEMItem menuItemVar(title, linkedVariable[, saveCallback[, callbackVal]]);

Note: you cannot specify both readonly mode and callback in the same constructor. However, you can set readonly mode for menu item with callback explicitly later using GEMItem::setReadonly() method.

Option select

GEMItem menuItemSelect(title, linkedVariable, select[, readonly]);

or

GEMItem menuItemSelect(title, linkedVariable, select[, saveCallback[, callbackVal]]);

Note: you cannot specify both readonly mode and callback in the same constructor. However, you can set readonly mode for menu item with callback explicitly later using GEMItem::setReadonly() method.

Spinner

GEMItem menuItemSpinner(title, linkedVariable, spinner[, readonly]);

or

GEMItem menuItemSpinner(title, linkedVariable, spinner[, saveCallback[, callbackVal]]);

Note: you cannot specify both readonly mode and callback in the same constructor. However, you can set readonly mode for menu item with callback explicitly later using GEMItem::setReadonly() method.

Link to menu page

GEMItem menuItemLink(title, linkedPage[, readonly]);

Button

GEMItem menuItemButton(title, buttonAction[, readonly]);

or

GEMItem menuItemButton(title, buttonAction[, callbackVal[, readonly]]);

Note: you cannot specify both readonly mode and callback with uninitialized GEMCallbackData argument in the same constructor. However, you can set readonly mode later using GEMItem::setReadonly() method.

Constants

Methods

Note: calls to methods that return a reference to the owning GEMItem object can be chained, e.g. menuItemInterval.setReadonly().show(); (since GEM ver. 1.4.6).


GEMSelect

List of values available for option select. Supplied to GEMItem constructor. Object of class GEMSelect defines as follows:

GEMSelect mySelect(length, optionsArray);

Example of use:

// Integer option select
// 1) Create array of options:
SelectOptionInt optionsArray[] = {{"Opt 1", 10}, {"Opt 2", -12}, {"Opt 3", 13}};
// 2) Supply array of options to GEMSelect constructor:
GEMSelect mySelect(sizeof(optionsArray)/sizeof(SelectOptionInt), optionsArray);

or

// Integer option select
// GEMSelect constructor with anonymous options array (length of array (3) can't be calculated in this case and should be explicitly supplied):
GEMSelect mySelect(3, (SelectOptionInt[]){{"Opt 1", 10}, {"Opt 2", -12}, {"Opt 3", 13}});

SelectOptionInt

Data structure that represents option of the select of type int. Object of type SelectOptionInt defines as follows:

SelectOptionInt selectOption = {name, val_int};

SelectOptionByte

Data structure that represents option of the select of type byte. Object of type SelectOptionByte defines as follows:

SelectOptionByte selectOption = {name, val_byte};

SelectOptionFloat

Data structure that represents option of the select of type float. Object of type SelectOptionFloat defines as follows:

SelectOptionFloat selectOption = {name, val_float};

SelectOptionDouble

Data structure that represents option of the select of type double. Object of type SelectOptionDouble defines as follows:

SelectOptionDouble selectOption = {name, val_double};

SelectOptionChar

Data structure that represents option of the select of type char*. Object of type SelectOptionChar defines as follows:

SelectOptionChar selectOption = {name, val_char};

GEMSpinner

Spinner is similar to option select, but instead of specifying available options as an array, requires setting of minimum and maximum values of available range and a step with which increment/decrement of the linked variable is performed. Available since GEM ver. 1.6.

GEMSpinner represents range of values available for incremental spinner. Supplied to GEMItem constructor. Object of class GEMSpinner defines as follows:

GEMSpinner mySpinner(boundaries);

Example of use:

// Integer spinner
// 1) Define settings (boundaries) of the spinner:
GEMSpinnerBoundariesInt mySpinnerBoundaries = { .step = 50, .min = -150, .max = 150 };
// 2) Supply settings to GEMSpinner constructor:
GEMSpinner mySpinner(mySpinnerBoundaries);

It is possible to exclude support for spinner menu items to save some space on your chip. For that, locate file config.h that comes with the library, open it and comment out corresponding inclusion, i.e. change this line:

#include "config/support-spinner.h"

to

// #include "config/support-spinner.h"

Keep in mind that contents of the config.h file most likely will be reset to its default state after installing library update.

Or, alternatively, define GEM_DISABLE_SPINNER flag before build. E.g. in PlatformIO environment via platformio.ini:

build_flags =
    ; Disable support for increment/decrement spinner menu items
    -D GEM_DISABLE_SPINNER

GEMSpinnerBoundariesInt

Data structure that represents settings of the spinner of type int. Object of type GEMSpinnerBoundariesInt defines as follows:

GEMSpinnerBoundariesInt boundaries = {step, min, max};

GEMSpinnerBoundariesByte

Data structure that represents settings of the spinner of type byte. Object of type GEMSpinnerBoundariesByte defines as follows:

GEMSpinnerBoundariesByte boundaries = {step, min, max};

GEMSpinnerBoundariesFloat

Data structure that represents settings of the spinner of type float. Object of type GEMSpinnerBoundariesFloat defines as follows:

GEMSpinnerBoundariesFloat boundaries = {step, min, max};

GEMSpinnerBoundariesDouble

Data structure that represents settings of the spinner of type double. Object of type GEMSpinnerBoundariesDouble defines as follows:

GEMSpinnerBoundariesDouble boundaries = {step, min, max};

Note: It is up to author of the sketch to make sure that initial value of the associated variable is within allowable range of spinner, and that type of variable and types of step and min/max boundaries match (and their values don't exceed capacity of their data type). Increment/decrement of variable will stop closest to the corresponding min/max boundary of allowable range, and result variable value may not always reach said boundaries exactly, if initial value and step combination won't allow it. If initial value is not within min/max boundaries interaction with spinner won't affect it (it will be possible to enter edit mode but won't be possible to increment/decrement value). If step is supplied as a negative value, the absolute value will be taken instead. If supplied value of min is greater than max, min/max values will be swapped. So { .step = -50, .min = 150, .max = 49 } is equivalent to { .step = 50, .min = 49, .max = 150 }.


GEMCallbackData

Data structure that represents an argument that optionally can be passed to callback function associated with menu item. It contains pointer to menu item itself and a user-defined value, which can be one of the following types: int, byte, float, double, bool, const char*, void*. The value is stored as an anonymous union, so choose carefully which property to use to access it (as it is will access the same portion of memory).

Declaration of GEMCallbackData type:

struct GEMCallbackData {
  GEMItem* pMenuItem;     // Pointer to current menu item
  union {                 // User-defined value for callback argument (as one of the following types listed in a union)
    byte valByte;
    int valInt;
    float valFloat;
    double valDouble;
    bool valBoolean;
    bool valBool;
    const char* valChar;
    void* valPointer;
  };
};

Object of type GEMCallbackData contains the following properties:

Basic example of use:

...

void buttonAction(GEMCallbackData callbackData); // Forward declaration
const char* buttonId = "button_first"; // User-defined string that will be passed as a part of callback argument
GEMItem menuItemButton("Press me!", buttonAction, buttonId);

...

void buttonAction(GEMCallbackData callbackData) {
  Serial.print("Pressed menu item title: ");
  Serial.println(callbackData.pMenuItem->getTitle()); // Get title of the menu item via pointer to the menu item
  Serial.print("Button ID: ");
  Serial.println(callbackData.valChar); // Get user-defined variable as a part of callbackData argument
}

For more details and examples of using user-defined callback arguments see corresponding sections of the wiki.


GEMAppearance

Data structure that holds values of appearance settings that define how menu is rendered on screen. Can be submitted to GEM (GEM_u8g2, GEM_adafruit_gfx) constructor instead of specifying each option as a separate argument, or passed as an argument to GEM::setAppearance() (GEM_u8g2::setAppearance(), GEM_adafruit_gfx::setAppearance()) method to set general appearance of the menu (that will be used for every menu page if not overridden). Pointer to object of type GEMAppearance can be passed to GEMPage::setAppearance() method to customize appearance of corresponding menu page individually.

Object of type GEMAppearance defines as follows:

GEMAppearance appearanceGeneral = {menuPointerType, menuItemsPerScreen, menuItemHeight, menuPageScreenTopOffset, menuValuesLeftOffset}

Basic example of use:

// Create GEMAppearance object with general values (that will be used for every menu page if not overridden)
GEMAppearance appearanceGeneral = {/* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ GEM_ITEMS_COUNT_AUTO, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86}
// Create GEMAppearance object as a copy of appearanceGeneral (its values can be customized later in sketch).
// Note that it should be created in a global scope of the sketch (in order to be passed as a pointer to menu page)
GEMAppearance appearanceSettings = appearanceGeneral;

// Create menu object (and pass appearanceGeneral as an argument to constructor)
GEM menu(glcd, appearanceGeneral);

...

// Later in sketch, e.g. in setupMenu()
void setupMenu() {
  ...
  appearanceSettings.menuValuesLeftOffset = 70;
  menuPageSettings.setAppearance(&appearanceSettings); // Note `&` operator
  ...
}

Alternatively:

// Create empty GEMAppearance object (its values can be populated later in sketch).
// Note that it should be created in a global scope of the sketch (in order to be passed as a pointer to menu page)
GEMAppearance appearanceSettings;

// Create menu object (its appearance settings will be populated with default values)
GEM menu(glcd);

...

// Later in sketch, e.g. in setupMenu()
void setupMenu() {
  ...
  // Create GEMAppearance object with general values (that will be used for every menu page if not overridden)
  GEMAppearance appearanceGeneral;
  appearanceGeneral.menuPointerType = GEM_POINTER_ROW;
  appearanceGeneral.menuItemsPerScreen = GEM_ITEMS_COUNT_AUTO;
  appearanceGeneral.menuItemHeight = 10;
  appearanceGeneral.menuPageScreenTopOffset = 10;
  appearanceGeneral.menuValuesLeftOffset = 86;

  // Set appearanceGeneral as a general appearance of the menu
  menu.setAppearance(appearanceGeneral); // Note there is no `&` operator when setting general (or global) appearance of the menu

  // Copy values from appearanceGeneral object to appearanceSettings for further customization
  appearanceSettings = appearanceGeneral;
  appearanceSettings.menuValuesLeftOffset = 70;
  menuPageSettings.setAppearance(&appearanceSettings); // Note `&` operator
  ...
}

Passing GEMAppearance object to GEMPage::setAppearance() method as a pointer allows to change appearance of the individual menu page dynamically by changing values stored in object (and making sure that menu.drawMenu(); is called afterwards) without need for additional call to GEMPage::setAppearance(). In contrast, to change general appearance of the menu (and not individual page) menu.setAppearance(appearanceGeneral); method should be called with new or updated GEMAppearance object supplied as an argument (and menu.drawMenu(); should be called afterwards as well).

For more details about appearance customization see corresponding section of the wiki.


GEMContext

Data structure that represents "context" of currently executing user action, toggled by pressing menu item button. Property context of the GEM (GEM_u8g2, GEM_adafruit_gfx) object is of type GEMContext.

Consists of pointers to user-supplied functions that represent setup and loop functions (named context.enter() and context.loop() respectively) of the context. It allows you to initialize variables and e.g. prepare screen (if needed for the task that function performs), and then run through loop function, waiting for user input, or sensor reading, or command to terminate and exit back to the menu eventually. In the latter case additional context.exit() function will be called, that can be used to clean up your context and e.g. to free some memory and draw menu back to screen.

Object of type GEMContext (also aliased as AppContext) defines as follows:

GEMContext myContext = {loop, enter, exit, allowExit};

Basic example of use:

...

void buttonAction(); // Forward declaration
GEMItem menuItemButton("Blink!", buttonAction);

...

// Create menu object
GEM menu(glcd); // AltSerialGraphicLCD version used for example

...

void buttonAction() {
  // Declaration of context functions
  menu.context.loop = buttonContextLoop;
  menu.context.enter = buttonContextEnter; // Optional
  menu.context.exit = buttonContextExit; // Optional
  // Call of context functions
  menu.context.enter();
  //menu.context.allowExit = false; // Set to false if required to manually exit the context loop from within the loop itself (default is true)
}

void buttonContextEnter() {
  // Running some user-defined routines that usually belongs to setup()
  // Clear LCD screen
  glcd.clearScreen();
  // Initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

void buttonContextLoop() {
  // Do something in the loop() function you would normally do in the sketch, e.g. blink built in LED
  digitalWrite(LED_BUILTIN, HIGH);
  delay(500);
  digitalWrite(LED_BUILTIN, LOW);
  delay(500);
}

void buttonContextExit() {
  // Set GEM specific settings to their values
  menu.reInit();
  // Draw menu back to screen
  menu.drawMenu();
  // Clear context (assigns `nullptr` values to function pointers of the `context` property of the `GEM` object and resets `allowExit` flag to its default state)
  menu.clearContext();
}

To exit currently running context and return to menu, press button associated with GEM_KEY_CANCEL key (only if context.allowExit flag is set to its default value of true, otherwise you should handle exit from the loop manually and call context.exit() explicitly) - context.exit() callback will be called.

For more details see supplied example on context usage and read corresponding section of the wiki.

Floating-point variables

The float data type has only 6-7 decimal digits of precision ("mantissa"). For AVR based Arduino boards (like UNO R3) double data type has basically the same precision, being only 32 bit wide (the same as float). On some other boards (like SAMD boards, e.g. with M0 chips) double is actually a 64 bit number, so it has more precision (up to 15 digits).

Internally in GEM, dtostrf() and atof() are used to convert floating-point number to and from a string. Support for dtostrf() comes with stdlib.h for AVR, and hence available out of the box for AVR-based boards. While it is possible to use sprintf() for some other boards (like SAMD), dtostrf() is used for them instead as well, for consistency through explicit inclusion of avr/dtostrf.h. See this thread for some more details on dtostrf() support across different boards.

Default precision (the number of digits after the decimal sign, in terms of dtostrf()) is set to 6, but can be individually set for each editable menu item using GEMItem::setPrecision() method.

Note that maximum length of the number should not exceed GEM_STR_LEN (i.e. 17) - otherwise overflows and undetermined behavior may occur (that includes the value of precision specified through GEMItem::setPrecision() method or default one, which will increase length of the number with trailing zeros if necessary). This is result of using char[GEM_STR_LEN] buffer during dtostrf() conversion. It is not possible to enter number with the length exceeding this limit during edit of the variable, however, additional caution should be taken to verify that initial value of the variable (or externally changed value) in combination with specified precision does not exceed this limit.

It is possible to exclude support for editable float and double variables to save some space on your chip (up to 10% of program storage space on UNO R3). For that, locate file config.h that comes with the library, open it and comment out corresponding inclusion, i.e. change this line:

#include "config/support-float-edit.h"

to

// #include "config/support-float-edit.h"

Keep in mind that contents of the config.h file most likely will be reset to its default state after installing library update.

Or, alternatively, define GEM_DISABLE_FLOAT_EDIT flag before build. E.g. in PlatformIO environment via platformio.ini:

build_flags =
    ; Disable support for editable floats
    -D GEM_DISABLE_FLOAT_EDIT

Note that option selects support float and double variables regardless of this setting.

Advanced Mode

Advanced Mode provides additional means to modify, customize and extend functionality of GEM.

When Advanced Mode is enabled some of the internal methods of the library is made virtual (marked with GEM_VIRTUAL macro in source code). That (alongside with public and protected access specifiers) makes it possible to override those methods in your own sketch. However keep in mind that inner workings of GEM is more prone to change than its public interface (e.g. during code refactoring), so be cautious to upgrade GEM version if your code is relying on derivative classes and overrides.

Additional features of Advanced Mode may be added in the future.

To enable Advanced Mode, locate file config.h that comes with the library, open it and comment out the following line:

#define GEM_DISABLE_ADVANCED_MODE

to

// #define GEM_DISABLE_ADVANCED_MODE

Keep in mind that contents of the config.h file most likely will be reset to its default state after installing library update.

Or, alternatively, define GEM_ENABLE_ADVANCED_MODE flag before build. E.g. in PlatformIO environment via platformio.ini:

build_flags =
    ; Enable Advanced Mode
    -D GEM_ENABLE_ADVANCED_MODE

Note that GEM in Advanced Mode requires more memory to run, so plan accordingly.

Configuration

It is possible to configure GEM library by excluding some features not needed in your project. That may help to save some additional program storage space. E.g., you can disable support for editable floating-point variables (see previous section).

You can also choose which version of GEM library (AltSerialGraphicLCD, U8g2 or Adafruit GFX based) should be compiled. That way, there won't be requirement to have all of the supported graphics libraries installed in the system at the same time (regardless of which one is actually used).

Currently there are two ways of achieving that.

Manual config.h edition

For that, locate file config.h that comes with the library, open it and comment out corresponding inclusion.

Support for AltSerialGraphicLCD is disabled by default since GEM ver. 1.5. To enable AltSerialGraphicLCD support comment out the following line:

#define GEM_DISABLE_GLCD

To disable U8g2 support comment out the following line:

#include "config/enable-u8g2.h"

To disable Adafruit GFX support comment out the following line:

#include "config/enable-adafruit-gfx.h"

More configuration options may be added in the future.

Keep in mind that contents of the config.h file most likely will be reset to its default state after installing library update.

Defining build flags

Alternatively, define corresponding flag before build. E.g. in PlatformIO environment via platformio.ini:

build_flags =
    ; Enable AltSerialGraphicLCD support
    -D GEM_ENABLE_GLCD
    ; Disable U8g2 support
    -D GEM_DISABLE_U8G2
    ; Disable Adafruit GFX support
    -D GEM_DISABLE_ADAFRUIT_GFX

Compatibility

Some boards (e.g. ESP32, ESP8266, RP2040, nRF52840, etc. based boards) are not supported in AltSerialGraphicLCD version of GEM: this library should not be enabled in Configuration before compiling for these boards.

When support for Floating-point variables is enabled, GEM relies on dtostrf() function to handle conversion to a string, which may not be available for all of the architectures supported by Arduino by default. You may have to manually include support for it, e.g., via explicit inclusion of suitable version of dtostrf.h header file in GEM.cpp, GEM_u8g2.cpp or GEM_adafruit_gfx.cpp source files. It is available for AVR-based boards by default and currently it is explicitly included for SAMD boards (e.g. with M0 chips), RP2040 and nRF52840 based boards. ESP32 based boards should be fine as well.

Note: there are reports of possible compatibility issues with some ESP32 based boards resulting in flash read err, 1000 message after flashing compiled sketch (some users find it is possible to reflash the board afterwards to restore its functionality, some don't). Check this thread for more details.

Examples

GEM library comes with several annotated examples that will help you get familiar with it. More detailed info on the examples (including schematic and breadboard view where necessary) available in wiki.

License

This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with this library. If not, see http://www.gnu.org/licenses/.