Multilevel menu for Arduino and not only
- Tutorial
A few months ago, an article appeared on the hub: "Implementing a multi-level menu for Arduino with a display . " But hey, I thought. “I wrote this menu six years ago!”
Back in 2009, I wrote the first project based on a microcontroller and display called “Lighting Control Automation”, for which it was necessary to create a menu shell that would fit a thousand configs, or even more. The project was successfully born, compiles and is able to work so far, and the OS menu went to wander from project to project, using the best practices of Damage-Oriented programming . “Stop it,” I said, and rewrote the code.
You will find a legacy code of selected quality, a tale about how I rewrote it, as well as instructions for those who want to use it.
OS menu requirements and features
To begin with, we will determine the requirements that we set for the menu:
- ease of use, buttons left-right, up-down, back-forward .;
- tree structure of any adequate depth (up to 256);
- the total number of menu items that is enough for everyone (10 ^ 616);
- editing settings;
- launching programs.
- A simple built-in task manager.
And yet, it is necessary that all this weigh as little as possible, be unpretentious to resources and run on any platform (as long as it is for AVR, it works with GLCD and text LCD).
Theoretically, with the appropriate drivers, this OS menu can simply be taken and connected to RTOS.
File structure
As an example, we will analyze the following menu structure (item number on the left):
0 Корень/
1 - Папка 1/ - папка с файлами
3 -- Программа 1
4 -- Программа 2
5 -- Папка 3/ - папка с множеством копий программы. Положение курсора будет являться параметром запуска
6 --- Программа 3.1
6 --- Программа 3.2
6 --- Программа 3.3
6 --- хххххх
6 --- Программа 3.64
2 - Папка 2/ - папка с конфигами
7 -- Булев конфиг 1
8 -- Числовой конфиг 2
9 -- Числовой конфиг 3
10 -- Программа Дата/время
The main tenet of the OS menu is "Everything is a file." May it be so.
Each file has a type, name, parent folder, other parameters.
We describe it with the structure:
struct filedata{
uint8_t type;
uint8_t parent;
uint8_t mode1;//параметр 1
uint8_t mode2;//параметр 2
char name[20];
};
For each file, define 4 bytes in the fileData array:
- type,
- parent, it is not really needed, since all the information is in breadcrumbs, but remains as legacy
- mode1, two parameters specific to each file type
- mode2
type == T_FOLDER
The main file is a folder. It allows you to create a tree structure of the entire menu.
The most important thing here is the root folder number null. Whatever happens, in the end we will return to it.
Folder Options are
mode1 = стартовый номер дочернего файла,
mode2 = количество файлов в ней.
In the root folder 0 are files 1 and 2, a total of 2 pieces.
We describe it like this:
T_FOLDER, 0, 1, 2,
type == T_DFOLDER
Folder 3 contains several copies of the same program, but with different launch keys.
For example, in the lighting control unit, it is possible to set up to 64 daily programs, with 16 intervals in each. If you describe each item, you need 1024 files. In practice, two are enough. And feed bread crumbs to the program in the form of parameters.
mode1 = номер дочернего файла, копии которого будем плодить
mode2 = количество копий файла.
Simple mathematics tells us that if all 256 files are dynamic folders with the maximum number of copies, the total number of menu items in the system will be 256 ^ 256 = 3.2 x 10 ^ 616. This is EXACTLY enough for any adequate and not very case.
type == T_APP
Application. Its task is to register in the task manager (built-in or external), take control of the buttons and edit.
mode1 = id запускаемого приложения.
type == T_CONF
A config file for the sake of which all fuss is started. Allows you to set the boolean or numeric value of a parameter. Works with int16_t.
mode1 = id конфига
The config has its own configsLimit array, where for each config there are three int16_t configuration numbers:
- Cell ID - The starting number of the memory cell for storing data. All data takes up two bytes.
- Minimum - minimum data value
- Maximum - the maximum value of the data.
For example, in cell 2 you can write a number from -100 to 150, then the line will take the form:
2, -100, 150,
type == S_CONF
An interesting (but still only in the old code) config, works in conjunction with T_SFOLDER
mode1 = id конфига
type == T_SFOLDER
A special kind of folder is rendered closer to the config, as it is one of its varieties.
Imagine that you have the ability to work on RS-485 protocols A, B or C in your system. We put a bunch of files of the S_CONF type in the folder and select the necessary one from them. Moreover, when we go into the folder again, the cursor will highlight the active option.
mode1, mode2 are similar for T_FOLDER. The child files are only T_SCONF
Refactoring Results
I did not set myself the task of revising the architecture, in many places I even left the logic of work as is. There are quite funny crutches.
The main task is to sort out the system so that its use in new projects is simple. Eventually:
- Allocated work with the hardware at least to separate functions in a separate file. The HWI includes:
- Rewritten modules for classes. Everything that is possible is hidden in private, the appearance is unified, a chip with classes and a more or less unified interface will come in handy later.
- "Added" interface for working with RTOS. Rather, the full-time task manager is quite simple to replace with any other.
- Tidy up the code, made it more understandable, removed magic numbers, improved the interface. Now he is not ashamed to show.
Clock settings module I was too lazy to rewrite under hwi. All the same, it needs to be completely redone. He is terrible.
How the refactoring took place can be clearly seen in the repository.
Create your own project
The project setup includes the following items:
File creation
Let's create arrays according to the previously considered structure.
//массив структуры
static const uint8_t fileStruct[FILENUMB*FILEREW] PROGMEM =
{
T_FOLDER, 0, 1, 2, //0
T_FOLDER, 0, 3, 3, //1
T_FOLDER, 0, 7, 4, //2
T_APP, 1, 1, 0, //3
T_APP, 1, 2, 0, //4
T_DFOLDER, 1, 6, 66, //5
T_APP, 5, 2, 0, //6
T_CONF, 2, 0, 0, //7
T_CONF, 2, 1, 0, //8
T_CONF, 2, 2, 0, //9
T_APP, 2, 3, 0 //10
};
//Массив названий
static PROGMEM const char file_0[] = "Root";
static PROGMEM const char file_1[] = "Folder 1";
static PROGMEM const char file_2[] = "Folder 2";
static PROGMEM const char file_3[] = "App 1";
static PROGMEM const char file_4[] = "App 2";
static PROGMEM const char file_5[] = "Dyn Folder";
static PROGMEM const char file_6[] = "App";
static PROGMEM const char file_7[] = "config 0";
static PROGMEM const char file_8[] = "config 1";
static PROGMEM const char file_9[] = "config 2";
static PROGMEM const char file_10[] = "Date and Time";
PROGMEM static const char *fileNames[] = {
file_0, file_1, file_2, file_3, file_4, file_5, file_6, file_7, file_8,
file_9, file_10
};
Let's create an array for configs:
//number of cell(step by 2), minimal value, maximum value
static const PROGMEM int16_t configsLimit[] = {
0,0,0,// config 0: 0 + 0 дадут булев конфиг
2,-8099,8096,//config 1
4,1,48,//config 2
};
Button Settings
I prefer to connect the buttons with a ground fault and a pull-up resistor to the power supply, which is always available in the MK.
In the file hw / hwdef.h we indicate the names of the registers and the location of the buttons:
#define BUTTONSDDR DDRB
#define BUTTONSPORT PORTB
#define BUTTONSPIN PINB
#define BUTTONSMASK 0x1F
#define BSLOTS 5
/**Button mask*/
enum{
BUTTONRETURN = 0x01,
BUTTONLEFT = 0x02,
BUTTONRIGHT = 0x10,
BUTTONUP = 0x08,
BUTTONDOWN = 0x04
};
Display setting
Now the project is dragging the GLCDv3 library, which is not good. Historically so.
Link to google-code - https://code.google.com/p/glcd-arduino
Application creation
Consider an example application that uses basic menu functions.
menuos / app / sampleapp.cpp
Create a class with the following structure:
#ifndef __SAMPLEAPP_H__
#define __SAMPLEAPP_H__
#include "hw/hwi.h"
#include "menuos/MTask.h"
#include "menuos/buttons.h"
class sampleapp
{
//variables
public:
uint8_t Setup(uint8_t argc, uint8_t *argv);//запуск приложения. В качестве параметров - текущий уровень и массив хлебных крошек
uint8_t ButtonsLogic(uint8_t button);//обработчик кнопок
uint8_t TaskLogic(void);//обработчик таймера
protected:
private:
uint8_t tick;
void Return();//возврат в главное меню
//functions
public:
sampleapp();
~sampleapp();
protected:
private:
}; //sampleapp
extern sampleapp SampleApp;
//Сишные костылиобертки для обработчика кнопок и диспетчера
void SampleAppButtonsHandler(uint8_t button);
void SampleAppTaskHandler();
#endif //__SAMPLEAPP_H__
And we outline the main functions:
uint8_t sampleapp::Setup(uint8_t argc, uint8_t *argv)
{
tick = 0;
//пропишем себя в системных модулях
Buttons.Add(SampleAppButtonsHandler);//add button handler
Task.Add(1, SampleAppTaskHandler, 1000);//add task ha
GLCD.ClearScreen();//очистим экран
//и на самом видном месте напишем
GLCD.CursorTo((HwDispGetStringsLength()-11)/2, HwDispGetStringsNumb()/2);
GLCD.Puts("Hello Habr");
return 0;
}
Wrappers:
void SampleAppButtonsHandler(uint8_t button){
SampleApp.ButtonsLogic(button);
}
void SampleAppTaskHandler(){
SampleApp.TaskLogic();
}
Button handler:
uint8_t sampleapp::ButtonsLogic(uint8_t button){
switch (button){
case BUTTONLEFT:
break;
case BUTTONRIGHT:
break;
case BUTTONRETURN:
Return();
break;
case BUTTONUP:
break;
case BUTTONDOWN:
break;
default:
break;
}
return 0;
}
And a function that will be called every second:
uint8_t sampleapp::TaskLogic(void){
GLCD.CursorTo((HwDispGetStringsLength()-11)/2, HwDispGetStringsNumb()/2+1);
GLCD.PrintNumber(tick++);
}
Now in menu.cpp we will write that our program will be called by number 2:
void MMenu::AppStart(void){
if (file.mode2 != BACKGROUND){
Task.Add(MENUSLOT, MenuAppStop, 10);//100 ms update
Task.ActiveApp = 1;//app should release AtiveApp to zero itself
}
switch (file.mode1){//AppNumber
case 2:
SampleApp.Setup(level, brCrumbs);
break;
case 3:
Clock.Setup(level, brCrumbs);
break;
default:
Task.ActiveApp = 0;
break;
}
}
Let's assemble the project and see what we got:
The same for visuals
Detailed and slightly boring instructions on file structure and architecture, as well as an example of working in video material.
Links and repositories
The project was built in the Atmel Studio programming environment, but that day will come and it will be forked under Eclipse. The current version of the project is available in any repository (Reservation).
- GitHub repository: https://github.com/radiolok/menuosv1
- Bitbucket repository: https://bitbucket.org/radiolok/menuosv1
- GLCDv3: https://code.google.com/p/glcd-arduino/
- openLCD: https://bitbucket.org/bperrybap/openglcd/