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

от автора


Несколько месяцев назад на хабре появилась статья «Реализация многоуровневого меню для 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 	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: http://habrahabr.ru/post/203646/
  4. openLCD:https://bitbucket.org/bperrybap/openglcd/

ссылка на оригинал статьи http://habrahabr.ru/post/257607/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *