Начинающим на Arduino: Упаковываем конечный автомат в отдельный класс и библиотеку
В прошлой статье про написание конечных автоматов я обещал упаковать наш гениальный код в виде класса на C++ для повторного удобного использования. Делать буду так же на примере своей старой разработки SmartButton. Итак, влезаем в непонятный мир ардуининых библиотек и ООП.
Зачем всё это нужно?
Arduino IDE позволяет использовать синтаксис C++11, оказывается. То есть, там очень развитый объектно-ориентированный язык. Нам же хочется сосредотачиваться на нашем гениальном коде и размазанная по программе лишняя логика частенько мешает сосредоточиться. Взять, например, всякие дисплейчики, кнопочки, датчики и релюшки — у каждого же своя логика, зачем её смешивать с общей логикой программы. Тот же, например, дисплей. У него много полей, статических и изменяемых. Ой, поле — это же класс. Поле может входить в меню (класс меню) или нет, быть часть частью виртуального дисплея (класс), которых на физическом эеране может быть насколько (дисплеи: рабочий, настроек, диагностики и т.п.). Меню, в свою очередь, управляется кнопками (классы кнопок могут быть разными) или джойстиком (класс). Всё это вместе — класс «дисплей», который можно объявить в своей программе как:
#include "Display.h"
Display disp(куча параметров и настроек);
Если вы делаете проект не совсем на коленке не кое как и собираетесь что-то потом менять или повторно использовать какие-то свои наработки — лучше оформить сделанное в виде библиотек Arduino. В идеале, конечно же, положить в Github для других людей, если вам не жалко и вы не против, что кто-то ваш код исправит или дополнит.
Раз уж мы в прошлой статье делали кнопочку, давайте её оформим как класс и библиотеку?
Итак, наша задача сделать так, чтобы мы могли в своих скетчах писать:
#include "myButton.h"
myButton b1(4),b2(5),b3(12); // три кнопки на пинах 4, 5 и 12.
loop() {
b1.run();
b2.run();
b3.run();
// ...
if (b1.clicked()) doSomething(); // так или другим каким способом, есть варианты.
// ...
}
Как сделать библиотеку Arduino?
Это просто!
Сначала надо решить, как ваша библиотека будет называться. Пусть для примера, это будет MyLib.
Найдите, где лежат ваши скетчи на компьютере. Они лежат каждый в своей папочке, а рядом с ними есть папка libraries (библиотеки). Например, на маке /Users/Пользователь/Documents/Arduino/libraries и на виндоусе c:\Users\Пользователь\Документы\Arduino\libraries. Я сам сижу на маке и пути в виндах не знаю. Найдёте.
Вот в этой папке libraries создайте новую папку MyLib, то есть с именем своей библиотеки. Перейдите туда.
В этой новой папке надо создать как минимум один файл MyLib.h, тот, что вы будет включать в ваш проект. Минимальное его содержимое выглядит примерно так:
#ifndef MYLIB_H
#define MYLIB_H
#if ARDUINO >= 100
#include
#else
#include
#endif
// Ваш код здесь
#endif
Расскажу, что здесь зачем. Конструкция ниже позволяет включать вашу библиотеку в код несколько раз без ошибок. Лучше использовать название вашей библиотеки большими буквами. Это не сурово прямо обязательно, но все так делают и вы не выделяйтесь. Задача стоит придумать уникальное слово, в нашем случае MYLIB_H, идентификатор для этого заголовочного файла.
#ifndef MYLIB_H
#define MYLIB_H
// Ваш код
#endif
То есть, в вашем скетче может оказаться несколько таких строк:
#include "MyLib.h"
Вы скажете «тю, да я, да я слежу, да я…» и будете неправы. Лучше один раз написать в одном файле вот такую конструкцию, чем исправлять ваши готовые скетчи, если вдруг вы захотите вложить один в другой или ваша библиотека будет включена в другую итд. Данный код проверяет, определено ли слово MYLIB_H, если нет, то определяет его и включает дальнейший код. Если же слово уже определено, то второй раз код компилировать не нужно.
Следующий важный кусок кода:
#if ARDUINO >= 100
#include
#else
#include
#endif
Включает определения из исполняющей системы Arduino UDE. Без этого ваша библиотека просто не скомпилируется.
Всё. Закройте Arduino IDE, Откройте заново. Создайте новый скетч, пропишите там #include «MyLib.h» и ура, ваша библиотека есть и подключена!
Я смотрел, в библиотеке вроде как много файлов должно быть?
Да, конечно. Мы сделали минимальные действия, чтобы создать библиотеку. Теперь настало время планирования.
Чтобы я мог помещать сюда куски своего кода копипастом, я назову библиотеку SmartButton, ладно? Болванку MyLib можно прибить за ненадобностью.
По аналогии с предыдущим пунктом, создаём папку SmartButton, в ней:
- SmartButton.h — То, что мы будем включать в наши программы. Там будут только определения, без кода.
- SmartButton.cpp — Программный код класса. Это не скетч! Обратите внимание, что расширение файла cpp (C++).
- README.md — Файл описания библиотеки «для людей», то есть, документация. «md» означает MarkDown, то есть с разметкой. Достаточно назвать просто README.
- library.json — описание библиотеки для Arduino IDE в хитром формате JSON.
- examples — папка с примерами, которые будут потом видны в Arduino IDE. В ней должны лежать папки с именами примеров, в, а них с тем же именем файлы с расширением ino — скетчи.
SmartButton.h
#ifndef SMART_BUTTON_H
#define SMART_BUTTON_H
#if ARDUINO >= 100
#include
#else
#include
#endif
// Можно выше до 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
class SmartButton {
// Это внутренние переменный класса.
// Они свои у каждого объекта и конфликта
// за имена переменных не будет.
// не надо выдумывать для каждой кнопки свои названия.
private:
byte btPin;
// Точно, как мы делали в [предыдущей статье про МКА](https://habrahabr.ru/post/345960/)
enum state {Idle, PreClick, Click, Hold, LongHold, ForcedIdle};
enum input {Press, Release, WaitDebounce, WaitHold, WaitLongHold, WaitIdle};
enum state btState = Idle;
enum input btInput = Release;
unsigned long pressTimeStamp;
// Это скрытый метод, его снаружи не видно.
private:
void DoAction(enum input in);
// Это то, чем можно пользоваться.
public:
// Конструкторы и деструкторы.
// То есть то, что создаёт и убивает объект.
SmartButton();
SmartButton(int pin);
SmartButton(int pin, int mode) {btPin=pin; pinMode(pin,mode);}
~SmartButton();
// В стиле Arduino IDE определим метод begin
void begin(int p, int m) {btPin=p; pinMode(p,m);}
// Генератор событий для помещения в loop().
void run();
// Методы для переопределения пользователем.
public:
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.
};
#endif
Давайте поясню суть затеи. Мы не знаем, что нам будет нужно от кнопки. Наш МКА умеет находиться в состояниях Клик, Нажатие, Удержание и СлишкомДолгоеУдержание, а так же выходить из этих состояний в состояние Выключен. Так как мы делаем библиотеку универсальную, то надо предоставить возможность другому программисту вставить свой код в обработчики состояний. В ООП есть для этого замечательное средство — наследование.
Мы делаем класс, у которого есть несколько методов (функций) и они пустые. То есть, они есть, они будут вызываться в нужный момент, но кода в них нет. Зачем это? Затем, что в скетче можно будет создать свой класс на базе нашего, определить там только нужные из методов и наполнить их своим кодом.
Например, мы захотим сделать кнопку-переключатель, то есть, одно нажатие — включено, другое — выключено. Будем зажигать и гасить светодиод и предоставим функцию isOn () для использования в классическом виде в функции loop ().
#include "SmartButton.h"
#define LED_PIN (13)
// Порождаем наш новый класс от SmartButton
class Toggle : public SmartButton {
private:
byte sw = 0; // состояние переключателя
byte led; // нога для лампочки
public:
Toggle(byte bt_pin, byte led_pin) : SmartButton(bt_pin) { // конструктор.
led=led_pin;
};
// Наши методы
// Включена кнопка или нет.
byte isOn() { return sw; }
// Что делать на клик.
virtual void onClick() {
if (sw) {
// Был включен. Выключаем.
digitalWrite(led,LOW);
// Здесь может быть любой ваш код на выключение кнопки.
} else {
// Был выключен. Включаем.
digitalWrite(led,HIGH);
// Здесь может быть любой ваш код на включение кнопки.
}
sw=!sw; // Переключаем состояние.
}
};
// Объявляем переменную bt нашего нового класса. Можно не одну.
Toggle bt(4,LED_PIN); // Нога 4, встроенный светодиод.
Toggle drill(12,8) // Нога 12, светодиод на ноге 8.
void loop() {
bt.run();
drill.run();
if (bt.isOn()) {
// что-то делать
} else {
// что-то другое делать
}
if (drill.isOn()) {
// что-то делать
} else {
// что-то другое делать
}
}
Как видите, нас совершенно здесь не интересует МКА кнопочки из предыдущей статьи, кода этой кнопки нет, он спрятан. Мы добавили свою функциональность к базовому классу и сделали переключатель по клику. Наш новый класс Toggle тоже можно оформить в виде библиотеки, кстати или положить в отдельный файл Toggle.h рядом с вашим скетчем, вам достаточно будет его подключить директивой #include. Мы так же задаём ногу со светодиодом для подсветки кнопки. Обратите внимание, что мы просто создали два объекта (bt и drill) нового класса Toggle, а МКА обработки кнопки для нас скрыт и не заботит.
Основываясь на классе SmartButton можно сделать свои классы, что понимают двойной клик, например, водят курсор по меню или поворачивают пулемётную турель медленно-быстрее в зависимости от времени удержания кнопки. Для этого достаточно определить свои методы, описанные в SmartButton.h как virtual. Все определять не обязательно, только нужные вам.
По просьбе целевой аудитории, вот пример класса PressButton, который предоставляет методы:
- pressed () — кнопка была нажата, можно вызывать много раз.
- ok () — я понял, слушай кнопку дальше, то есть сброс.
#include "SmartButton.h"
#define LED_PIN (13)
// Порождаем наш новый класс от SmartButton
class PressButton : public SmartButton {
private:
byte sw = 0; // состояние переключателя
public:
PressButton(byte bt_pin) : SmartButton(bt_pin) {}; // конструктор.
// Наши методы
// Была кликнута кнопка или нет.
byte pressed() { return sw; };
// Я всё понял, слушаем кнопку дальше.
void ok() { sw=0; };
// Что делать на клик.
virtual void onClick() { sw=1; };
};
// Объявляем переменную bt нашего нового класса. Можно не одну.
PressButton bt(4); // Нога 4.
PressButton drill(12) // Нога 12.
void loop() {
bt.run();
drill.run();
if (bt.pressed()) {
// что-то делать
bt.ok();
} else {
// что-то другое делать
}
if (drill.pressed()) {
// что-то делать
if (какое_то_условие) drill.ok();
} else {
// что-то другое делать
}
}
Таким образом мы получаем две независимо работающие «залипающие» кнопки, которые после нажатия находятся в состоянии pressed пока их не сбросить методом ok ().
Если у вас есть меню, вы можете определить методы onClick () у кнопок «вверх» и «вниз», которые будут вызывать перемещение курсора меню на дисплее с соответствующем направлении. Определение onHold () у них может вызывать перемещение курсора в начало и конец меню, например. У кнопки «ентер» можно определить onClick () как выбор меню, onHold () как выход с сохранением, а onLongHold () как выход без сохранения.
Если вам нужен двойной клик, ну, определите onClick так, чтобы у вас там был счётчик нажатий и время с предыдущего нажатия. Тогда вы сможете различать одинарный и двойной клик.
SmartButton — это просто МКА, это инструмент для реализации поведения ваших кнопок.
Где же скрыта вся магия? Магия кроется в файле SmartButton.cpp
#include "SmartButton.h"
// Конструктор и деструктор пустые.
SmartButton::SmartButton() {}
SmartButton::~SmartButton() {}
// Конструктор с инициализацией.
// Он используется чаще всего.
SmartButton::SmartButton(int pin) {
btPin = pin;
pinMode(pin, INPUT_PULLUP);
}
// Машина конечных автоматов сидит здесь:
// Обратите внимание - это ровно та же функция,
// Что мы писали в [прошлой статье](https://habrahabr.ru/post/345960/).
// Обратите внимание на вызов виртуальных функций on* и off*.
void SmartButton::DoAction(enum input in) {
enum state st=btState;
switch (in) {
case Release:
btState=Idle;
switch (st) {
case Click:
offClick();
break;
case Hold:
offHold();
break;
case LongHold:
offLongHold();
break;
case ForcedIdle:
onIdle();
break;
}
break;
case WaitDebounce:
switch (st) {
case PreClick:
btState=Click;
onClick();
break;
}
break;
case WaitHold:
switch (st) {
case Click:
btState=Hold;
onHold();
break;
}
break;
case WaitLongHold:
switch (st) {
case Hold:
btState=LongHold;
onLongHold();
break;
}
break;
case WaitIdle:
switch (st) {
case LongHold:
btState=ForcedIdle;
break;
}
break;
case Press:
switch (st) {
case Idle:
pressTimeStamp=millis();
btState=PreClick;
break;
}
break;
}
}
// А это наш генератор событий.
// Его надо помещать в loop()
void SmartButton::run() {
unsigned long mls = millis();
if (!digitalRead(btPin)) DoAction(Press);
else DoAction(Release);
if (mls - pressTimeStamp > SmartButton_debounce) DoAction(WaitDebounce);
if (mls - pressTimeStamp > SmartButton_hold) DoAction(WaitHold);
if (mls - pressTimeStamp > SmartButton_long) DoAction(WaitLongHold);
if (mls - pressTimeStamp > SmartButton_idle) DoAction(WaitIdle);
}
Логика местами спорная, я знаю :) Но это работает.
Теперь осталось заполнить файл README описанием вашей библиотеки и заполнить по аналогии файлик library.json, где поля вполне очевидны:
{
"name": "SmartButton",
"keywords": "button, abstract class, oop",
"description": "The SmartButton abstract class for using custom buttons in Arduino sketches.",
"repository": {
"type": "git",
"url": "https://github.com/nw-wind/SmartButton"
},
"version": "1.0.0",
"authors": {
"name": "Sergei Keler",
"url": "https://github.com/nw-wind"
},
"frameworks": "arduino",
"platforms": "*"
}
Если у вас нет репозитория, можно эту секцию не указывать.
Ура! Библиотека готова. Можно запаковать папку в ZIP и раздавать друзьям или копировать на другие свои компьютеры.
По аналогии, можно сделать класс для любой МКА. Принцип общий: вы делаете класс, определяете виртуальные методы, которые потом надо будет переопределить, чтобы вставить свой код или готовые методы, если универсальность не требуется.
Что за Github и зачем он мне?
Github — это огромное сообщество программистов. Да, ваш код будет публично светиться на весь интернет, но… любой человек может предложить свои правки к вашему коду. Мне, например, очень помогли с SmartDelay два человека, один из которых сделал свою подобную библиотеку и мы поподсматривали чуть-чуть код друг у друга. Лучше две хорошие библиотеки, чем две глюкавые, правда?
Чтобы поместить вашу библиотеку в Github надо сделать там аккаунт, сгенерить ключ и создать репозиторий с там же именем, что ваша библиотека (папка). Файлы можно загрузить через web-шнтерфейс.
Для установки библиотеки из Github в Arduino IDE достаточно скопировать URL и воспользоваться утилитой git:
Или загрузить ZIP — это будет как раз библиотека Arduino, как и все прочие библиотеки.
Как пользоваться git вообще и Github в частности, есть много статей наверняка. Попробуйте поискать. Если не найдёте, я напишу как им пользуюсь я.