Быстро собрал макет, циферки бегают, часики ходят, одометры запоминаются в FRAM — красота, но… понадобились кнопочки для управления этой красотой.
Сегодня с расскажу про кнопочки, потом про датчик зажигания, а уже потом про саму приборку, Ладно?
Рисовать на китайском экране 16х2 через i2c просто, датчики скорости и оборотов мотора сели на внешние прерывания, температура читается с аналогового порта, инфа хранится в FRAM, ну и часики тоже китайские воткнуты. Всё это крутится асинхронно примерно как SmartDelay, про который писал недавно здесь.
Да, кнопочки!
Сделать одну кнопку для притормаживания мигания светодиода оказалось легко, как и прочие игрушки. Прилепить же огромную клавиатуру к приборной панели мотоцикла эндуро не получится, нет места. Пришлось поломать голову и ограничиться четырьмя кнопками:
- Режим
- Вверх
- Вниз
- ОК/Сброс
Чтобы вписать в это и меню и управление, надо распознавать тык, тыыык и тыыыык. То есть нажатия на кнопки разной длительности. Я написал большую портянку из switch и if, понял, что прочитать это через пару месяцев я не смогу и взялся снова за плюсы.
Задача оказалась похожа на библиотеку SmartDelay:
- Максимально спрятать код в библиотеку.
- Код обработки кнопок не должен мешать программировать «по делу».
- Должно быть возможно использовать ещё где-то и в других последующих проектах.
- Должно быть красиво, что ли.
Если вы знаете разное похожее, пожалуйста, сообщите в комментариях, чем вы пользуетесь.
Сначала я бумаге нарисовал конечный автомат. С налёту не получилось, без бумаги.
Потом я прогуглил, что можно вместо switch/if сделать табличкой. Я последний раз обращался к теме МКА где-то лет 30 назад, понадобилось освежить в памяти теорию.
В результате я родил абстрактный класс SmartButton. Данное творение прячет внутри себя МКА, слушает цифровые порты и дёргает пустые абстрактные методы на клик, удержание и долгое удержание. Для использования этого класса надо создать свой и переопределить нужные методы.
#include <SmartButton.h>
byte menuMode = 0;
// Новый класс из SmartButton
class modeSmartButton: public SmartButton {
public:
modeSmartButton(int p) : SmartButton(p) {}
virtual void onClick(); // Методы для использования
virtual void offClick(); // В данном случае, лишь два.
};
// Действие на клик: переключаем некий режим меню.
void modeSmartButton::onClick() {
Serial.println("Key pressed.");
if (menuMode) {
Serial.println("Menu mode off.");
} else {
Serial.println("Menu mode on.");
}
menuMode^=1;
}
// Действие на отпускание кнопки после клика. Ничего не делаем.
void modeSmartButton::offClick() {
Serial.println("Key depressed.");
}
// Собственно объект, кнопка на 6 ножке ардуины.
modeSmartButton btMode(6);
void setup() {
Serial.begin(9600);
Serial.println("Ready");
}
void loop() {
btMode.run(); // это должно быть в loop().
}
Видно, что кода чуть, всё более-менее понятно. Нет колбеков прямо вот так явно описанных. В loop() есть только один вызов run() для каждой кнопки, где-то определяется класс и сама кнопка. Можно творить, страшные лестницы МКА для обработки тыков кнопок с стиле C не мешают.
Давайте посмотрим в код. Весь проект лежит на гитхабе.
Не придумав ничего лучше, я сделал доступными настройки временных интервалов снаружи. Вот, соответственно, задержки для клика, удержания, долгого удержания и настолько долгого, что стоит проигнорировать такое нажатие вообще. В SmartButton.h определил эти константы осторожно так, чтобы их можно было переопределить до #include.
#ifndef SmartButton_debounce
#define SmartButton_debounce 10
#endif
#ifndef SmartButton_hold
#define SmartButton_hold 1000
#endif
#ifndef SmartButton_long
#define SmartButton_long 5000
#endif
#ifndef SmartButton_idle
#define SmartButton_idle 10000
#endif
Состояния и воздействия я сделал как enum в частности и потому, что автоматом получил их количества StatesNumber и InputsNumber.
enum state {Idle = 0, PreClick, Click, Hold, LongHold, ForcedIdle, StatesNumber};
enum input {Release = 0, WaitDebounce, WaitHold, WaitLongHold, WaitIdle, Press, InputsNumber};
Слегка поломав голову, нарисовал вот такой тип. Это указатель на метод этого класса. Не смейтесь, плюсы как-то мимо меня прошли, я в них не мастер.
typedef void (SmartButton::*FSM)(enum state st, enum input in);
Вот здесь пришлось повозиться. Это таблица переходов. Возня была со ссылками на методы, как их написать так, чтобы и компилятор не ругался и ссылки были на методы конкретного экземпляра класса. Не на статический метод, не просто левую функцию, а именно на метод, чтобы он имел доступ к приватным переменным класса.
FSM action[StatesNumber][InputsNumber] = {
{NULL, NULL, NULL, NULL, NULL, &SmartButton::ToPreClick},
{&SmartButton::ToIdle, &SmartButton::ToClick, NULL, NULL, NULL, NULL},
{&SmartButton::ToIdle, NULL, &SmartButton::ToHold, NULL, NULL, NULL},
{&SmartButton::ToIdle, NULL, NULL, &SmartButton::ToLongHold, NULL, NULL},
{&SmartButton::ToIdle, NULL, NULL, NULL, &SmartButton::ToForcedIdle, NULL},
{&SmartButton::ToIdle, NULL, NULL, NULL, NULL, NULL}
};
Все методы были объявлены как private, а в public остались лишь run() и пустые заглушки для переопределения в порождённых классах.
inline virtual void onClick() {}; // On click.
inline virtual void onHold() {}; // On hold.
inline virtual void onLongHold() {}; // On long hold.
inline virtual void onIdle() {}; // On timeout with too long key pressing.
inline virtual void offClick() {}; // On depress after click.
inline virtual void offHold() {}; // On depress after hold.
inline virtual void offLongHold() {}; // On depress after long hold.
inline virtual void offIdle() {}; // On depress after too long key pressing.
Я использую режим pinMode(pin,INPUT_PULLUP) так как схема собрана под это, но в ближайшее время собираюсь добавить возможность выбора режима.
Метод run() просто переводит временные интервалы во входные воздействия КА.
void SmartButton::run() {
unsigned long mls = millis();
if (!digitalRead(btPin)) {
if (btState == Idle) {
DoAction(btState, Press);
return;
}
if (mls - pressTimeStamp > SmartButton_debounce) {
DoAction(btState, WaitDebounce);
}
if (mls - pressTimeStamp > SmartButton_hold) {
DoAction(btState, WaitHold);
}
if (mls - pressTimeStamp > SmartButton_long) {
DoAction(btState, WaitLongHold);
}
if (mls - pressTimeStamp > SmartButton_idle) {
DoAction(btState, WaitIdle);
}
return;
} else {
if (btState != Idle) {
DoAction(btState, Release);
return;
}
}
}
Приватный же метод DoAction(состояние, воздействие) просто вызывает функцию из таблицы, если там есть адрес.
void SmartButton::DoAction(enum state st, enum input in) {
if (action[st][in] == NULL) return;
(this->*(action[st][in]))(st, in);
}
Большинство действий выглядят достаточно просто. Там просто устанавливается состояние и вызывается абстрактный метод, который может переопределиться в порождённом классе. Это такой аналог колбека.
void SmartButton::ToClick(enum state st, enum input in) {
btState = Click;
onClick(); // Вот это аналог колбека в плоском С.
}
Самый жирный обработчик получился для состояния Idle потому, что в него приходят из разных других состояний, а сделать абстрактные методы для таких событий хотелось.
void SmartButton::ToIdle(enum state st, enum input in) {
btState = Idle;
switch (st) {
case Click: offClick(); break;
case Hold: offHold(); break;
case LongHold: offLongHold(); break;
case WaitIdle: onIdle(); break;
}
}
С таким инструментом я уже готов порождать классы для упомянутых в начале статьи кнопок выбора режима дисплея, навигации вверх и вниз, перегруженной кнопки выбора/сброса.
Понятно, что мне грозит ещё один КА, намного более сложный. Кнопок мало, а действий много. Если интересно, напишу в следующий раз в качестве примера реального практического применения вот только что описанной библиотеки.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (24)
osipov_dv
11.01.2017 10:02+1Вот из-за излишнего ООП в либах я так и не смог сделать погодную станцию на уно. Тупо оперативки не хватило, слишком жирно откушано переменными в либах RTC и SD. Остается свободными 300 байт и их не хватает. Либо надо оптимизировать чужие либы, выкусывая ненужный мне код и переменные, либо разносить на 2 микроконтроллера.
nwwind
11.01.2017 12:30+1SD толстая либа весьма.
ООП позволяет как раз одной строкой создать и настроить объект. Если их много, это удобно, конечно же. Ну и спрятать логику глубоко можно. Я проверил, вот этот код занимает совсем мало памяти что на ООП, что так.
Я бы на твоём месте подумал о втором контроллере с SD и связал бы их по I2C. Например, Pro Mini размером сам с карточку SD.osipov_dv
11.01.2017 12:53Я хотел сделать все на голом 328, с уходом в спячку и питанием от батареек АА (4 шт). Но с таким раскладом придется делать станцию из двух частей — часть висящая за окном и следящая за погодой за окном, с отправкой по 433 RF, и часть в квартире (не требовательную к питанию), с логгером и RTC. Два рядом вешать смысла не вижу.
ilmarin77
11.01.2017 12:30Есть такая штуковина: State Map Compiler — на специальном языке описываются состояния машины и переходы, и компилятор генерирует код — на C, C++, Java и т.д.
Может так проще будет?nwwind
11.01.2017 12:30Спасибо, почитаю сейчас. Для следующего шага мне как раз что-то подобное надо.
iig
11.01.2017 12:50Контрастность у дешевого дисплея lcd 1602, как по мне, отвратительная. Для outdoor я бы не использовал.
Ну, и кодировать таблицу состояний можно разными способами, ваш оригинальный.
Gryphon88
11.01.2017 12:53автомат очень удобно делается с помощью xmacros.
Подскажи кто-нибудь пример, как грамотно реализовать работу с «шифтами» как на геймпаде к PS3: нажатие кнопки — одно действие, кнопка+шифт — другое, кнопка+оба шифта — третье. Если проверять состояние шифтов перед обработкой, код уж очень некрасиво обрастает if/switch'аимnwwind
11.01.2017 12:59Там много кнопок и есть специфика своя, я бы сделал для шифт класс управляющий кнопок, а их состояние менял в классе onClick/offClick. ставя / убирая бит соответствующий в некой глобальной переменной shiftState.
nwwind
18.01.2017 11:19В печали пребываю.
С++ говно.
Размер объекта 157 байтов. На кнопку! Это перебор уж. Указатель на метод класса — 4 байта, на просто функцию — 2. Переписал в итоге внутри на Цэ плоском старом добром саму МКА. Интерфейс оставил старый, так что, всё прошло незаметно. В итоге 13 байтов на объект, уже ничего.
Эх, таблицей, конечно же, красиво, но в ембедеде… Жрёт память таблица, да и вообще оопность негуманно её расходует.
Закоммитил.
VT100
Считаю, что с точки зрения UserInterface «тыыык и тыыыык» неудобны. Более удобны были-бы длинные или сдвоенные нажатия, меняющие отклик всех или остальных кнопок.
nwwind
Это мотоцикл. Руки в перчатках, время есть тк все тыки идут на стоящем моте.
В проекте пока что так:
Два экрана: нормальный и спортивный. Переключаются сликом по кнопке режим (1).
В спортивном кнопка ввода (4) даёт старт/стоп круга по клику. Удержание сбрасявыет всю статистику гонки.
В нормальном режиме клики вверх/вниз (2/3) преключают одометры. Долгое удержание ввода (4) сбрасывает текущий одометр. Удержание ввода (4) переводит экран в меню и можно ходить по полям: одометр, моточасы, часы. Ходить кнопками вверх/вниз. Клик на ввод приводит к редактированию поля, если можно. Можно только часы портить. Клик ввода завершает редактирование и выходит из меню. Долгое удержание выводит из меню. Очень долгое обнуляет поле.
Думаю добавить в протокол класса флаг для слишком долгого удержания на замену его многокликом. Достаточно внутри просто вызвать переход к toIdle. Это полезно для настройки часиков.
DrZlodberg
Vapor, вроде, всего 3мя кнопками обходится.
Для спортивного режима кнопку старт/стопа удобнее на руль тогда выводить. Если круги короткие и без остановки — погрешность с запуском с приборки будет приличная.
Но вообще идея интересная. По размерам большая получилась? Ну и корпус стоит делать не убиваемый. С таким катанием при приземлении на руль приборку можно в кашу превратить.
Офтоп: Чтобы так не летать рекомендую держать палец на сцеплении. Иногда реально спасает.
nwwind
На руль обязательно. Но это будет параллельно — на руле и на приборке.
На КТМ, более того, это штатная фича и разъёмы уже есть.