Pull to refresh

Многоуровневое меню для Arduino и не только

Developing for Arduino *
Tutorial


Несколько месяцев назад на хабре появилась статья «Реализация многоуровневого меню для Arduino с дисплеем». «Но, погодите, — подумал я. — Я написал такое меню еще шесть лет назад»!

В далеком 2009 году, я написал первый проект на базе микроконтроллера и дисплея под названием «Автомат управления освещением», для которого потребовалось создать такую оболочку меню, в которую влезет тысяча конфигов, а то и более. Проект был успешно рожден, компилируется и способен работать до сих пор, а оболочка менюОС пошла кочевать из проекта в проект, используя лучшие практики Ущербно-Ориентированного программирования. «Хватит это терпеть» сказал я, и переписал код.

Подкатом вы найдете legacy-код отборного качества, сказ о том, как я его переписал, а также инструкции для тех, кто захочет это использовать.

Требования и возможности менюОС


Для начала определимся с требованиями, которые мы предъявляем к меню:
  1. простота использования, кнопки влево-вправо, вверх-вниз, назад-вперед.;
  2. древовидная структура любой адекватной глубины (до 256);
  3. общее количество пунктов меню, которого хватит всем (10^616);
  4. редактирование настроек;
  5. запуск программ.
  6. простенький встроенный диспетчер задач.

А еще, необходимо чтобы все это как можно меньше весило, было неприхотливо к ресурсам и запускалось на любой платформе(пока есть для AVR, работает с GLCD и текстовым LCD).
Теоретически, с соответствующими драйверами, данное менюОС можно просто взять и подключить к RTOS.

Файловая структура


В качестве примера, будем разбирать следующую структуру меню(слева номер пункта):
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 --  Программа Дата/время

Главным догматом менюОС является «Все есть файл». Да будет так.
У каждого файла есть тип, название, родительская папка, прочие параметры
Опишем структурой:
struct filedata{
	uint8_t type;
	uint8_t parent;
	uint8_t mode1;//параметр 1
	uint8_t mode2;//параметр 2
	char name[20];
};

Для каждого файла определим 4 байта в массиве fileData:
  1. type,
  2. parent, он не очень нужен, так как вся информация есть в хлебных крошках, но остался как legacy
  3. mode1, два параметра, специфичных для каждого типа файла
  4. mode2


type == T_FOLDER

Основным файлом является папка. Она и позволяет создать древовидную структуру всего меню.
Самая главная здесь — корневая папка под номером нуль. Что бы не произошло, в итоге мы вернемся в нее.
Параметрами папки являются
mode1 = стартовый номер дочернего файла,
mode2 = количество файлов в ней.

В корневой папке 0 лежат файлы 1 и 2, всего 2 штуки.
Опишем ее так:
T_FOLDER, 0, 1, 2,


type == T_DFOLDER

В Папке 3 лежит несколько копий одной и той же программы, однако с разными ключами запуска.
Например, в автомате управления освещением имеется возможность установить до 64 суточных программ, с 16 интервалами в каждой. Если описывать каждый пункт, потребуется 1024 файла. На практике достаточно двух. А хлебные крошки скормим программе в виде параметров.
mode1 = номер дочернего файла, копии которого будем плодить
mode2 = количество копий файла.

Нехитрая математика подсказывает нам, что если все 256 файлов будут динамическими папками с максимальным числом копий, общее число пунктов меню в системе составит 256^256 = 3.2 x 10^616. Этого ТОЧНО хватит на любой адекватный и не очень случай.

type == T_APP

Приложение. Его задача — прописаться в диспетчере задач (встроенном или внешнем), перехватить управление кнопками и править.
mode1 = id запускаемого приложения.


type == T_CONF

Конфиг-файл, ради которого и затеян весь сыр-бор. Позволяет устанавливать булево или числовое значение какого-либо параметра. Работает с int16_t.
mode1 = id конфига

У конфига есть свой массив configsLimit, где на каждый конфиг приходится три int16_t числа конфигурации:
  1. Cell ID — Стартовый номер ячейки памяти для хранения данных. Все данные занимают два байта.
  2. Minimum — минимальное значение данных
  3. Maximum — максимальное значение данных.

Например, в ячейку 2 можно записать число от -100 до 150, тогда строка примет вид:
2, -100, 150, 


type == S_CONF

Интересный(но оставшийся пока только в старом коде) конфиг, работает в связке с T_SFOLDER
mode1 = id конфига


type == T_SFOLDER

Особый вид папки вынесен ближе к конфигу, так как является одной из его разновидностей.
Представьте себе, у вас в системе зашита возможность работы по RS-485 по протоколам A,B или C. Помещаем в папку кучку файлов вида S_CONF и выбираем из них необходимый. Более того, когда мы зайдем в папку вновь, курсор подсветит активный вариант.
mode1, mode2 аналогичны для T_FOLDER. Дочерними файлами являются только T_SCONF

Результаты рефакторинга


Я не ставил перед собой задачу пересмотра архитектуры, во многих местах я даже оставил логику работы как есть. Есть весьма забавные костыли.
Основная задача — перебрать систему так, чтобы ее использование в новых проектах было простым. В итоге:
  1. Выделил работу с аппаратной частью как минимум в отдельные функции в отдельном файле. В HWI вошли:
  2. Переписаны модули под классы. Спрятано в private все что только можно, унифицирован внешний вид, Фишка с классами и более-менее унифицированным интерфейсом потом пригодится.
  3. «Добавлен» интерфейс для работы с RTOS. Вернее, штатный диспетчер задач довольно просто заменить на любой другой.
  4. Банально прибрался в коде, сделал его более понятнее, убрал магические числа, улучшил интерфейс. Теперь его не стыдно показать.

Модуль настройки часов мне было лень переписывать под hwi. Все равно его нужно полностью переделывать. Он ужасен.
Как проходил рефакторинг, можно наглядно увидеть в репозитории.

Создание своего проекта


Настройка проекта включает в себя следующие пункты:

Создание файлов

Создадим массивы по ранее рассмотренной структуре
//массив структуры
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
};

Создадим массив для конфигов:
//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
};


Настройка кнопок

Я предпочитаю подключать кнопки с замыканием на землю и подтягивающим резистором к питанию, который всегда в наличии в МК.

В файле hw/hwdef.h укажем названия регистров и расположение кнопок:
 #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
 };


Настройка дисплея

Сейчас проект тащит за собой библиотеку GLCDv3, что не есть хорошо. Исторически так сложилось.
Ссылка на google-code — https://code.google.com/p/glcd-arduino

Создание приложения

Рассмотрим пример приложения, использующий базовые функции меню.
menuos/app/sampleapp.cpp

Создадим класс со следующей структурой:
#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;

//Сишные <s>костыли</s>обертки для обработчика кнопок и диспетчера
void SampleAppButtonsHandler(uint8_t button);
void SampleAppTaskHandler();

#endif //__SAMPLEAPP_H__

И набросаем основные функции:
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;
}


Обертки:
void SampleAppButtonsHandler(uint8_t button){
	SampleApp.ButtonsLogic(button);
}

void SampleAppTaskHandler(){
	SampleApp.TaskLogic();
}


Обработчик кнопок:
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;
}

И функция, которая будет вызываться каждую секунду:
uint8_t sampleapp::TaskLogic(void){
	GLCD.CursorTo((HwDispGetStringsLength()-11)/2, HwDispGetStringsNumb()/2+1);
	GLCD.PrintNumber(tick++);	
}

Теперь в menu.cpp пропишем, что по номеру 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;
	}
}

Соберем проект и посмотрим, что у нас получилось:


То же самое для визуалов


Подробная и слегка занудная инструкция по файловой структуре и архитектуре, а также пример работы в видеоматериале.


Ссылки и репозитории


Проект собран в среде программирования Atmel Studio, но настанет тот день и он будет форкнут и под Eclipse. Актуальная версия проекта доступна в любом репозитории(Резервирование).
  1. Репозиторий на GitHub: https://github.com/radiolok/menuosv1
  2. Репозиторий на Bitbucket: https://bitbucket.org/radiolok/menuosv1
  3. GLCDv3: https://code.google.com/p/glcd-arduino/
  4. openLCD:https://bitbucket.org/bperrybap/openglcd/
Tags:
Hubs:
Total votes 38: ↑35 and ↓3 +32
Views 62K
Comments Comments 21