В прошлой статье про написание конечных автоматов я обещал упаковать наш гениальный код в виде класса на 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 <Arduino.h> #else #include <WProgram.h> #endif // Ваш код здесь #endif
Расскажу, что здесь зачем. Конструкция ниже позволяет включать вашу библиотеку в код несколько раз без ошибок. Лучше использовать название вашей библиотеки большими буквами. Это не сурово прямо обязательно, но все так делают и вы не выделяйтесь. Задача стоит придумать уникальное слово, в нашем случае MYLIB_H, идентификатор для этого заголовочного файла.
#ifndef MYLIB_H #define MYLIB_H // Ваш код #endif
То есть, в вашем скетче может оказаться несколько таких строк:
#include "MyLib.h"
Вы скажете "тю, да я, да я слежу, да я..." и будете неправы. Лучше один раз написать в одном файле вот такую конструкцию, чем исправлять ваши готовые скетчи, если вдруг вы захотите вложить один в другой или ваша библиотека будет включена в другую итд. Данный код проверяет, определено ли слово MYLIB_H, если нет, то определяет его и включает дальнейший код. Если же слово уже определено, то второй раз код компилировать не нужно.
Следующий важный кусок кода:
#if ARDUINO >= 100 #include <Arduino.h> #else #include <WProgram.h> #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 <Arduino.h> #else #include <WProgram.h> #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 в частности, есть много статей наверняка. Попробуйте поискать. Если не найдёте, я напишу как им пользуюсь я.
