Как правильно создать многоуровневое меню на arduino. Информационный портал по безопасности

06.05.2019

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

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

Для начала определимся с требованиями, которые мы предъявляем к меню:
  • простота использования, кнопки влево-вправо, вверх-вниз, назад-вперед.;

  • древовидная структура любой адекватной глубины (до 256);

  • общее количество пунктов меню, которого хватит всем (10^616);

  • редактирование настроек;

  • запуск программ.

  • простенький встроенный диспетчер задач.
  • А еще, необходимо чтобы все это как можно меньше весило, было неприхотливо к ресурсам и запускалось на любой платформе(пока есть для 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; };
    Для каждого файла определим 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 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 -
    Создание приложения
    Рассмотрим пример приложения, использующий базовые функции меню.
    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]костылиобертки для обработчика кнопок и диспетчера 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. Актуальная версия проекта доступна в любом репозитории(Резервирование).
  • Репозиторий на GitHub:
    • 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; };
    Для каждого файла определим 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 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; //Сишные костылиобертки для обработчика кнопок и диспетчера 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; } }

    Возле ректора 22 января 2011 в 21:51

    Меню для Arduino

    • Чулан *

    Мне всегда нравились микроконтроллеры и компьютеры. С компьютером проще, были бы деньги, а вот с микроконтроллерами немного сложнее. Моделей много глаза разбегаются, да и в свободной продаже у нас долгое время не встречалось. Раньше были попытки научится с ними работать, но как то не сложилось. И ATtiny12L отправилась в дальний яшик. Встречались мне новости по конструкторам на микроконтроллерах но на тот момент заказать было его невозможно, да и дорого. Гдето на глаза попалась Arduino и он ли она меня заинтересовала. Немного подумав решил заказать сие чудо и шилд для него в виде ЖК экрана с кнопочками, потому как моргать светодиодами было скучно, а делать что то свое лень.

    И вот Arduno у меня в руках, софт весь стоит, одним словом твори и твори. Мысль была сделать что то похожее на бортовой компьютер, пробег, напряжение, температура за бортом. Первоначально общий и суточный пробег, но куда девать кнопки? Значит добавим напряжение и температуру.
    Итого у нас получается:

    • Arduino + LCD Keypad Shield (5 кнопок, 2 строки по 16 символов);
    • отображение общего и суточного пробега;
    • напряжение в бортовой сети;
    • температура за бортом.
    Опыта в написании скетчей у меня не было, поэтому начал разбираться как выводить текст на экран и как работают кнопки. Для экрана есть своя стандартная библиотека с которой нашёлся общий язык. Для кнопок используется одна аналоговая линия и в зависимости от напряжения на соответствующем выводе можно определить какая кнопка нажата.
    На этом вроде все сложности должны были закончится, пиши себе да пиши. Но не все так просто как кажется. При отображении определённого параметра кнопки выполняют не одинаковые функции. Вроде что тут сложного? Если то то так так или так. Да, если параметра два и кнопок тоже две, но получается каша, про поиск ошибок и дополнении я вообще молчу. Вообщем это не тот вариант который мне нужен был.
    Помню в университете мне понравилась одна лекция про конечные автоматы, в частности речь идет об автомате Мили. Не буду вдаваться в математические подробности его работы, в Википедии про него прочитал, но ничего не понял. Не смотря на это продолжу. У нас есть N состояний, M кнопок и K выполняемых функций.
    Входные сигналы
    Кнопок у нас 5 штук (Right, Up, Down, Left, Select), они являются входными сигналами. Каждую кнопку можно представить одним событием или тремя (нажата, удерживается в нажатом положении, отжата), также можно добавить событие удерживается более X секунд, ничего сложного в этом нет. Фактически я использую только отпускание кнопок, чтобы исключить многократный повтор функций при удержании клавиши. Я буду использовать 5 различных входных сигналов.
    Состояния автомата
    Их получается не много больше чем мне показалось на первый взгляд. По мимо отображения основных значений (пробег, температура, напряжение, время, дата) добавляется ещё сброс суточного пробега, установка времени и даты. Всего получилось 12 состояний.
    Функции. Выходные сигналы
    Каждый выходной сигнал соответствует вызову той или иной функции, если требуется изменить только состояние без выполнения какой либо функции то предусмотрена для этих случаев пустая функция. Все остальные функции выполняют необходимые действия.
    Таблица автомата Мили
    Первая и вторая строка состояние автомата: отображаемый параметр и код состояния соответственно. Первая и вторая колонка клавиша и её код (входной сигнал).
    На пересечении входного сигнала и текущего состояния записаны новое состояние и выполняемая функция через запятую.

    Как этим пользоваться. MenuState хранит текущее состояние, допустим оно равно 1, на экране отображается суточный пробег. Нажимаем Select для сброса показаний, входной сигнал равен 4. Ищем пересечение входного сигнала 4 и текущего состояния 1, что в результате дает нам состояние 6 и функцию ff. Функция ff ничего не выполняет. В состоянии 6 на экране отобразится подтверждение сброса суточного пробега, влево отказ от сброса, в право подтверждение. Теперь если придёт сигнал 3 на пересечении получим “1,ff”, переход в состояние 1 без выполнения функция, а если придёт сигнал 0 то “1,0”. Переход в состояние 1 и выполнение функции 0. Ниже приведён список функций и их коды.
    Коды функций:
    F_NOP FF Нет действия
    F_Reset 0 Сбросить суточный пробег
    F_IncHour 1 Прибавить 1 час
    F_DecHour 2 Отнять 1 час
    F_IncMin 3 Прибавить 1 минуту
    F_DecMin 4 Отнять 1 минуту
    F_IncDay 5 Прибавить 1 день
    F_DecDay 6 Отнять 1 дени
    F_IncMon 7 Прибавить 1 месяц
    F_DecMon 8 Отнять 1 месяц
    F_IncYear 9 Прибавить 1 год
    F_DecYear A Отнять 1 год
    F_SetTime B Установить новое время
    F_SetDate C Установить новую дату

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

    // Функция перехода по меню и получения номера вызываемой функции
    byte GetFunction(byte InSig){
    // если нажатых лавиш нет
    if (InSig==KeyUnpressed)
    return F_NOP;
    boolean Up=InSig&KeyUpFlag;
    boolean Down=InSig&KeyDownFlag;
    byte sig=InSig&0b00011111;
    // проверяем допустимость
    if(sig>=InSigCount)
    return F_NOP;
    byte func=F_NOP;
    if(Up){
    // читаем вызываемую функцию
    func=menu.funct;
    // читаем новое сотояние
    MenuState=menu.state;
    }
    // Возвращаем номер функции
    return func;
    }

    // Пример выполнения функций
    byte k=ReadKey();
    k=GetFunction(k);
    if(k!=F_NOP){
    if(k==F_Reset){
    Trip=0; // Сбрасыаем суточный пробег
    }else if(k==F_IncHour){
    if(DT.Hour<23){ DT.Hour++; }else{ DT.Hour=0; }
    }else if(k==F_DecHour){
    if(DT.Hour>0){ DT.Hour--; }else{ DT.Hour=23; }
    }else if(k==F_IncMin){
    if(DT.Minute<59){DT.Minute++; }else{DT.Minute=0;}
    }else if(k==F_DecMin){

    Теги: 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; };
    Для каждого файла определим 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 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; //Сишные костылиобертки для обработчика кнопок и диспетчера 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; } }

    Вы можете помочь и перевести немного средств на развитие сайта

    Похожие статьи